// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"github.com/gorilla/mux"
graphql "github.com/graph-gophers/graphql-go"
_ "github.com/mattermost/go-i18n/i18n"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
)
type Routes struct {
Root *mux.Router // ''
APIRoot *mux.Router // 'api/v4'
APIRoot5 *mux.Router // 'api/v5'
Users *mux.Router // 'api/v4/users'
User *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}'
UserByUsername *mux.Router // 'api/v4/users/username/{username:[A-Za-z0-9\\_\\-\\.]+}'
UserByEmail *mux.Router // 'api/v4/users/email/{email:.+}'
Bots *mux.Router // 'api/v4/bots'
Bot *mux.Router // 'api/v4/bots/{bot_user_id:[A-Za-z0-9]+}'
Teams *mux.Router // 'api/v4/teams'
TeamsForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams'
Team *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}'
TeamForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}'
UserThreads *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/threads'
UserThread *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/threads/{thread_id:[A-Za-z0-9]+}'
TeamByName *mux.Router // 'api/v4/teams/name/{team_name:[A-Za-z0-9_-]+}'
TeamMembers *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/members'
TeamMember *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/members/{user_id:[A-Za-z0-9]+}'
TeamMembersForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/members'
Channels *mux.Router // 'api/v4/channels'
Channel *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}'
ChannelForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}'
ChannelByName *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/channels/name/{channel_name:[A-Za-z0-9_-]+}'
ChannelByNameForTeamName *mux.Router // 'api/v4/teams/name/{team_name:[A-Za-z0-9_-]+}/channels/name/{channel_name:[A-Za-z0-9_-]+}'
ChannelsForTeam *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/channels'
ChannelMembers *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/members'
ChannelMember *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/members/{user_id:[A-Za-z0-9]+}'
ChannelMembersForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/channels/members'
ChannelModerations *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/moderations'
ChannelCategories *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/channels/categories'
Posts *mux.Router // 'api/v4/posts'
Post *mux.Router // 'api/v4/posts/{post_id:[A-Za-z0-9]+}'
PostsForChannel *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/posts'
PostsForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/posts'
PostForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/posts/{post_id:[A-Za-z0-9]+}'
Files *mux.Router // 'api/v4/files'
File *mux.Router // 'api/v4/files/{file_id:[A-Za-z0-9]+}'
Uploads *mux.Router // 'api/v4/uploads'
Upload *mux.Router // 'api/v4/uploads/{upload_id:[A-Za-z0-9]+}'
Plugins *mux.Router // 'api/v4/plugins'
Plugin *mux.Router // 'api/v4/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}'
PublicFile *mux.Router // '/files/{file_id:[A-Za-z0-9]+}/public'
Commands *mux.Router // 'api/v4/commands'
Command *mux.Router // 'api/v4/commands/{command_id:[A-Za-z0-9]+}'
Hooks *mux.Router // 'api/v4/hooks'
IncomingHooks *mux.Router // 'api/v4/hooks/incoming'
IncomingHook *mux.Router // 'api/v4/hooks/incoming/{hook_id:[A-Za-z0-9]+}'
OutgoingHooks *mux.Router // 'api/v4/hooks/outgoing'
OutgoingHook *mux.Router // 'api/v4/hooks/outgoing/{hook_id:[A-Za-z0-9]+}'
OAuth *mux.Router // 'api/v4/oauth'
OAuthApps *mux.Router // 'api/v4/oauth/apps'
OAuthApp *mux.Router // 'api/v4/oauth/apps/{app_id:[A-Za-z0-9]+}'
OpenGraph *mux.Router // 'api/v4/opengraph'
SAML *mux.Router // 'api/v4/saml'
Compliance *mux.Router // 'api/v4/compliance'
Cluster *mux.Router // 'api/v4/cluster'
Image *mux.Router // 'api/v4/image'
LDAP *mux.Router // 'api/v4/ldap'
Elasticsearch *mux.Router // 'api/v4/elasticsearch'
Bleve *mux.Router // 'api/v4/bleve'
DataRetention *mux.Router // 'api/v4/data_retention'
Brand *mux.Router // 'api/v4/brand'
System *mux.Router // 'api/v4/system'
Jobs *mux.Router // 'api/v4/jobs'
Preferences *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/preferences'
License *mux.Router // 'api/v4/license'
Public *mux.Router // 'api/v4/public'
Reactions *mux.Router // 'api/v4/reactions'
Roles *mux.Router // 'api/v4/roles'
Schemes *mux.Router // 'api/v4/schemes'
Emojis *mux.Router // 'api/v4/emoji'
Emoji *mux.Router // 'api/v4/emoji/{emoji_id:[A-Za-z0-9]+}'
EmojiByName *mux.Router // 'api/v4/emoji/name/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}'
ReactionByNameForPostForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/posts/{post_id:[A-Za-z0-9]+}/reactions/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}'
TermsOfService *mux.Router // 'api/v4/terms_of_service'
Groups *mux.Router // 'api/v4/groups'
Cloud *mux.Router // 'api/v4/cloud'
Imports *mux.Router // 'api/v4/imports'
Exports *mux.Router // 'api/v4/exports'
Export *mux.Router // 'api/v4/exports/{export_name:.+\\.zip}'
RemoteCluster *mux.Router // 'api/v4/remotecluster'
SharedChannels *mux.Router // 'api/v4/sharedchannels'
Permissions *mux.Router // 'api/v4/permissions'
InsightsForTeam *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/top'
InsightsForUser *mux.Router // 'api/v4/users/me/top'
Usage *mux.Router // 'api/v4/usage'
WorkTemplates *mux.Router // 'api/v4/worktemplates'
HostedCustomer *mux.Router // 'api/v4/hosted_customer'
Drafts *mux.Router // 'api/v4/drafts'
}
type API struct {
srv *app.Server
schema *graphql.Schema
BaseRoutes *Routes
}
func Init(srv *app.Server) (*API, error) {
api := &API{
srv: srv,
BaseRoutes: &Routes{},
}
api.BaseRoutes.Root = srv.Router
api.BaseRoutes.APIRoot = srv.Router.PathPrefix(model.APIURLSuffix).Subrouter()
api.BaseRoutes.APIRoot5 = srv.Router.PathPrefix(model.APIURLSuffixV5).Subrouter()
api.BaseRoutes.Users = api.BaseRoutes.APIRoot.PathPrefix("/users").Subrouter()
api.BaseRoutes.User = api.BaseRoutes.APIRoot.PathPrefix("/users/{user_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.UserByUsername = api.BaseRoutes.Users.PathPrefix("/username/{username:[A-Za-z0-9\\_\\-\\.]+}").Subrouter()
api.BaseRoutes.UserByEmail = api.BaseRoutes.Users.PathPrefix("/email/{email:.+}").Subrouter()
api.BaseRoutes.Bots = api.BaseRoutes.APIRoot.PathPrefix("/bots").Subrouter()
api.BaseRoutes.Bot = api.BaseRoutes.APIRoot.PathPrefix("/bots/{bot_user_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Teams = api.BaseRoutes.APIRoot.PathPrefix("/teams").Subrouter()
api.BaseRoutes.TeamsForUser = api.BaseRoutes.User.PathPrefix("/teams").Subrouter()
api.BaseRoutes.Team = api.BaseRoutes.Teams.PathPrefix("/{team_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.TeamForUser = api.BaseRoutes.TeamsForUser.PathPrefix("/{team_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.UserThreads = api.BaseRoutes.TeamForUser.PathPrefix("/threads").Subrouter()
api.BaseRoutes.UserThread = api.BaseRoutes.TeamForUser.PathPrefix("/threads/{thread_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.TeamByName = api.BaseRoutes.Teams.PathPrefix("/name/{team_name:[A-Za-z0-9_-]+}").Subrouter()
api.BaseRoutes.TeamMembers = api.BaseRoutes.Team.PathPrefix("/members").Subrouter()
api.BaseRoutes.TeamMember = api.BaseRoutes.TeamMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.TeamMembersForUser = api.BaseRoutes.User.PathPrefix("/teams/members").Subrouter()
api.BaseRoutes.Channels = api.BaseRoutes.APIRoot.PathPrefix("/channels").Subrouter()
api.BaseRoutes.Channel = api.BaseRoutes.Channels.PathPrefix("/{channel_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.ChannelForUser = api.BaseRoutes.User.PathPrefix("/channels/{channel_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.ChannelByName = api.BaseRoutes.Team.PathPrefix("/channels/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter()
api.BaseRoutes.ChannelByNameForTeamName = api.BaseRoutes.TeamByName.PathPrefix("/channels/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter()
api.BaseRoutes.ChannelsForTeam = api.BaseRoutes.Team.PathPrefix("/channels").Subrouter()
api.BaseRoutes.ChannelMembers = api.BaseRoutes.Channel.PathPrefix("/members").Subrouter()
api.BaseRoutes.ChannelMember = api.BaseRoutes.ChannelMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.ChannelMembersForUser = api.BaseRoutes.User.PathPrefix("/teams/{team_id:[A-Za-z0-9]+}/channels/members").Subrouter()
api.BaseRoutes.ChannelModerations = api.BaseRoutes.Channel.PathPrefix("/moderations").Subrouter()
api.BaseRoutes.ChannelCategories = api.BaseRoutes.User.PathPrefix("/teams/{team_id:[A-Za-z0-9]+}/channels/categories").Subrouter()
api.BaseRoutes.Posts = api.BaseRoutes.APIRoot.PathPrefix("/posts").Subrouter()
api.BaseRoutes.Post = api.BaseRoutes.Posts.PathPrefix("/{post_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.PostsForChannel = api.BaseRoutes.Channel.PathPrefix("/posts").Subrouter()
api.BaseRoutes.PostsForUser = api.BaseRoutes.User.PathPrefix("/posts").Subrouter()
api.BaseRoutes.PostForUser = api.BaseRoutes.PostsForUser.PathPrefix("/{post_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Files = api.BaseRoutes.APIRoot.PathPrefix("/files").Subrouter()
api.BaseRoutes.File = api.BaseRoutes.Files.PathPrefix("/{file_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.PublicFile = api.BaseRoutes.Root.PathPrefix("/files/{file_id:[A-Za-z0-9]+}/public").Subrouter()
api.BaseRoutes.Uploads = api.BaseRoutes.APIRoot.PathPrefix("/uploads").Subrouter()
api.BaseRoutes.Upload = api.BaseRoutes.Uploads.PathPrefix("/{upload_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Plugins = api.BaseRoutes.APIRoot.PathPrefix("/plugins").Subrouter()
api.BaseRoutes.Plugin = api.BaseRoutes.Plugins.PathPrefix("/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}").Subrouter()
api.BaseRoutes.Commands = api.BaseRoutes.APIRoot.PathPrefix("/commands").Subrouter()
api.BaseRoutes.Command = api.BaseRoutes.Commands.PathPrefix("/{command_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Hooks = api.BaseRoutes.APIRoot.PathPrefix("/hooks").Subrouter()
api.BaseRoutes.IncomingHooks = api.BaseRoutes.Hooks.PathPrefix("/incoming").Subrouter()
api.BaseRoutes.IncomingHook = api.BaseRoutes.IncomingHooks.PathPrefix("/{hook_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.OutgoingHooks = api.BaseRoutes.Hooks.PathPrefix("/outgoing").Subrouter()
api.BaseRoutes.OutgoingHook = api.BaseRoutes.OutgoingHooks.PathPrefix("/{hook_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.SAML = api.BaseRoutes.APIRoot.PathPrefix("/saml").Subrouter()
api.BaseRoutes.OAuth = api.BaseRoutes.APIRoot.PathPrefix("/oauth").Subrouter()
api.BaseRoutes.OAuthApps = api.BaseRoutes.OAuth.PathPrefix("/apps").Subrouter()
api.BaseRoutes.OAuthApp = api.BaseRoutes.OAuthApps.PathPrefix("/{app_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Compliance = api.BaseRoutes.APIRoot.PathPrefix("/compliance").Subrouter()
api.BaseRoutes.Cluster = api.BaseRoutes.APIRoot.PathPrefix("/cluster").Subrouter()
api.BaseRoutes.LDAP = api.BaseRoutes.APIRoot.PathPrefix("/ldap").Subrouter()
api.BaseRoutes.Brand = api.BaseRoutes.APIRoot.PathPrefix("/brand").Subrouter()
api.BaseRoutes.System = api.BaseRoutes.APIRoot.PathPrefix("/system").Subrouter()
api.BaseRoutes.Preferences = api.BaseRoutes.User.PathPrefix("/preferences").Subrouter()
api.BaseRoutes.License = api.BaseRoutes.APIRoot.PathPrefix("/license").Subrouter()
api.BaseRoutes.Public = api.BaseRoutes.APIRoot.PathPrefix("/public").Subrouter()
api.BaseRoutes.Reactions = api.BaseRoutes.APIRoot.PathPrefix("/reactions").Subrouter()
api.BaseRoutes.Jobs = api.BaseRoutes.APIRoot.PathPrefix("/jobs").Subrouter()
api.BaseRoutes.Elasticsearch = api.BaseRoutes.APIRoot.PathPrefix("/elasticsearch").Subrouter()
api.BaseRoutes.Bleve = api.BaseRoutes.APIRoot.PathPrefix("/bleve").Subrouter()
api.BaseRoutes.DataRetention = api.BaseRoutes.APIRoot.PathPrefix("/data_retention").Subrouter()
api.BaseRoutes.Emojis = api.BaseRoutes.APIRoot.PathPrefix("/emoji").Subrouter()
api.BaseRoutes.Emoji = api.BaseRoutes.APIRoot.PathPrefix("/emoji/{emoji_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.EmojiByName = api.BaseRoutes.Emojis.PathPrefix("/name/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}").Subrouter()
api.BaseRoutes.ReactionByNameForPostForUser = api.BaseRoutes.PostForUser.PathPrefix("/reactions/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}").Subrouter()
api.BaseRoutes.OpenGraph = api.BaseRoutes.APIRoot.PathPrefix("/opengraph").Subrouter()
api.BaseRoutes.Roles = api.BaseRoutes.APIRoot.PathPrefix("/roles").Subrouter()
api.BaseRoutes.Schemes = api.BaseRoutes.APIRoot.PathPrefix("/schemes").Subrouter()
api.BaseRoutes.Image = api.BaseRoutes.APIRoot.PathPrefix("/image").Subrouter()
api.BaseRoutes.TermsOfService = api.BaseRoutes.APIRoot.PathPrefix("/terms_of_service").Subrouter()
api.BaseRoutes.Groups = api.BaseRoutes.APIRoot.PathPrefix("/groups").Subrouter()
api.BaseRoutes.Cloud = api.BaseRoutes.APIRoot.PathPrefix("/cloud").Subrouter()
api.BaseRoutes.Imports = api.BaseRoutes.APIRoot.PathPrefix("/imports").Subrouter()
api.BaseRoutes.Exports = api.BaseRoutes.APIRoot.PathPrefix("/exports").Subrouter()
api.BaseRoutes.Export = api.BaseRoutes.Exports.PathPrefix("/{export_name:.+\\.zip}").Subrouter()
api.BaseRoutes.RemoteCluster = api.BaseRoutes.APIRoot.PathPrefix("/remotecluster").Subrouter()
api.BaseRoutes.SharedChannels = api.BaseRoutes.APIRoot.PathPrefix("/sharedchannels").Subrouter()
api.BaseRoutes.Permissions = api.BaseRoutes.APIRoot.PathPrefix("/permissions").Subrouter()
api.BaseRoutes.InsightsForTeam = api.BaseRoutes.Team.PathPrefix("/top").Subrouter()
api.BaseRoutes.InsightsForUser = api.BaseRoutes.Users.PathPrefix("/me/top").Subrouter()
api.BaseRoutes.Usage = api.BaseRoutes.APIRoot.PathPrefix("/usage").Subrouter()
api.BaseRoutes.WorkTemplates = api.BaseRoutes.APIRoot.PathPrefix("/worktemplates").Subrouter()
api.BaseRoutes.HostedCustomer = api.BaseRoutes.APIRoot.PathPrefix("/hosted_customer").Subrouter()
api.BaseRoutes.Drafts = api.BaseRoutes.APIRoot.PathPrefix("/drafts").Subrouter()
api.InitUser()
api.InitBot()
api.InitTeam()
api.InitChannel()
api.InitPost()
api.InitFile()
api.InitUpload()
api.InitSystem()
api.InitLicense()
api.InitConfig()
api.InitWebhook()
api.InitPreference()
api.InitSaml()
api.InitCompliance()
api.InitCluster()
api.InitLdap()
api.InitElasticsearch()
api.InitBleve()
api.InitDataRetention()
api.InitBrand()
api.InitJob()
api.InitCommand()
api.InitStatus()
api.InitWebSocket()
api.InitEmoji()
api.InitOAuth()
api.InitReaction()
api.InitOpenGraph()
api.InitPlugin()
api.InitRole()
api.InitScheme()
api.InitImage()
api.InitTermsOfService()
api.InitGroup()
api.InitAction()
api.InitCloud()
api.InitImport()
api.InitRemoteCluster()
api.InitSharedChannels()
api.InitPermissions()
api.InitExport()
api.InitInsights()
api.InitUsage()
api.InitWorkTemplate()
api.InitHostedCustomer()
api.InitDrafts()
if err := api.InitGraphQL(); err != nil {
return nil, err
}
srv.Router.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))
InitLocal(srv)
return api, nil
}
func InitLocal(srv *app.Server) *API {
api := &API{
srv: srv,
BaseRoutes: &Routes{},
}
api.BaseRoutes.Root = srv.LocalRouter
api.BaseRoutes.APIRoot = srv.LocalRouter.PathPrefix(model.APIURLSuffix).Subrouter()
api.BaseRoutes.Users = api.BaseRoutes.APIRoot.PathPrefix("/users").Subrouter()
api.BaseRoutes.User = api.BaseRoutes.Users.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.UserByUsername = api.BaseRoutes.Users.PathPrefix("/username/{username:[A-Za-z0-9\\_\\-\\.]+}").Subrouter()
api.BaseRoutes.UserByEmail = api.BaseRoutes.Users.PathPrefix("/email/{email:.+}").Subrouter()
api.BaseRoutes.Bots = api.BaseRoutes.APIRoot.PathPrefix("/bots").Subrouter()
api.BaseRoutes.Bot = api.BaseRoutes.APIRoot.PathPrefix("/bots/{bot_user_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Teams = api.BaseRoutes.APIRoot.PathPrefix("/teams").Subrouter()
api.BaseRoutes.Team = api.BaseRoutes.Teams.PathPrefix("/{team_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.TeamByName = api.BaseRoutes.Teams.PathPrefix("/name/{team_name:[A-Za-z0-9_-]+}").Subrouter()
api.BaseRoutes.TeamMembers = api.BaseRoutes.Team.PathPrefix("/members").Subrouter()
api.BaseRoutes.TeamMember = api.BaseRoutes.TeamMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Channels = api.BaseRoutes.APIRoot.PathPrefix("/channels").Subrouter()
api.BaseRoutes.Channel = api.BaseRoutes.Channels.PathPrefix("/{channel_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.ChannelByName = api.BaseRoutes.Team.PathPrefix("/channels/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter()
api.BaseRoutes.ChannelByNameForTeamName = api.BaseRoutes.TeamByName.PathPrefix("/channels/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter()
api.BaseRoutes.ChannelsForTeam = api.BaseRoutes.Team.PathPrefix("/channels").Subrouter()
api.BaseRoutes.ChannelMembers = api.BaseRoutes.Channel.PathPrefix("/members").Subrouter()
api.BaseRoutes.ChannelMember = api.BaseRoutes.ChannelMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.ChannelMembersForUser = api.BaseRoutes.User.PathPrefix("/teams/{team_id:[A-Za-z0-9]+}/channels/members").Subrouter()
api.BaseRoutes.Plugins = api.BaseRoutes.APIRoot.PathPrefix("/plugins").Subrouter()
api.BaseRoutes.Plugin = api.BaseRoutes.Plugins.PathPrefix("/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}").Subrouter()
api.BaseRoutes.Commands = api.BaseRoutes.APIRoot.PathPrefix("/commands").Subrouter()
api.BaseRoutes.Command = api.BaseRoutes.Commands.PathPrefix("/{command_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Hooks = api.BaseRoutes.APIRoot.PathPrefix("/hooks").Subrouter()
api.BaseRoutes.IncomingHooks = api.BaseRoutes.Hooks.PathPrefix("/incoming").Subrouter()
api.BaseRoutes.IncomingHook = api.BaseRoutes.IncomingHooks.PathPrefix("/{hook_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.OutgoingHooks = api.BaseRoutes.Hooks.PathPrefix("/outgoing").Subrouter()
api.BaseRoutes.OutgoingHook = api.BaseRoutes.OutgoingHooks.PathPrefix("/{hook_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.License = api.BaseRoutes.APIRoot.PathPrefix("/license").Subrouter()
api.BaseRoutes.Groups = api.BaseRoutes.APIRoot.PathPrefix("/groups").Subrouter()
api.BaseRoutes.LDAP = api.BaseRoutes.APIRoot.PathPrefix("/ldap").Subrouter()
api.BaseRoutes.System = api.BaseRoutes.APIRoot.PathPrefix("/system").Subrouter()
api.BaseRoutes.Posts = api.BaseRoutes.APIRoot.PathPrefix("/posts").Subrouter()
api.BaseRoutes.Post = api.BaseRoutes.Posts.PathPrefix("/{post_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.PostsForChannel = api.BaseRoutes.Channel.PathPrefix("/posts").Subrouter()
api.BaseRoutes.Roles = api.BaseRoutes.APIRoot.PathPrefix("/roles").Subrouter()
api.BaseRoutes.Uploads = api.BaseRoutes.APIRoot.PathPrefix("/uploads").Subrouter()
api.BaseRoutes.Upload = api.BaseRoutes.Uploads.PathPrefix("/{upload_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Imports = api.BaseRoutes.APIRoot.PathPrefix("/imports").Subrouter()
api.BaseRoutes.Exports = api.BaseRoutes.APIRoot.PathPrefix("/exports").Subrouter()
api.BaseRoutes.Export = api.BaseRoutes.Exports.PathPrefix("/{export_name:.+\\.zip}").Subrouter()
api.BaseRoutes.Jobs = api.BaseRoutes.APIRoot.PathPrefix("/jobs").Subrouter()
api.BaseRoutes.SAML = api.BaseRoutes.APIRoot.PathPrefix("/saml").Subrouter()
api.InitUserLocal()
api.InitTeamLocal()
api.InitChannelLocal()
api.InitConfigLocal()
api.InitWebhookLocal()
api.InitPluginLocal()
api.InitCommandLocal()
api.InitLicenseLocal()
api.InitBotLocal()
api.InitGroupLocal()
api.InitLdapLocal()
api.InitSystemLocal()
api.InitPostLocal()
api.InitRoleLocal()
api.InitUploadLocal()
api.InitImportLocal()
api.InitExportLocal()
api.InitJobLocal()
api.InitSamlLocal()
srv.LocalRouter.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))
return api
}
func (api *API) Handle404(w http.ResponseWriter, r *http.Request) {
app := app.New(app.ServerConnector(api.srv.Channels()))
web.Handle404(app, w, r)
}
var ReturnStatusOK = web.ReturnStatusOK
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"math/rand"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/gorilla/websocket"
graphql "github.com/graph-gophers/graphql-go"
s3 "github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
"github.com/mattermost/mattermost-server/v6/server/channels/testlib"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
"github.com/mattermost/mattermost-server/v6/server/channels/wsapi"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type TestHelper struct {
App *app.App
Server *app.Server
ConfigStore *config.Store
Context *request.Context
Client *model.Client4
GraphQLClient *graphQLClient
BasicUser *model.User
BasicUser2 *model.User
TeamAdminUser *model.User
BasicTeam *model.Team
BasicChannel *model.Channel
BasicPrivateChannel *model.Channel
BasicPrivateChannel2 *model.Channel
BasicDeletedChannel *model.Channel
BasicChannel2 *model.Channel
BasicPost *model.Post
Group *model.Group
SystemAdminClient *model.Client4
SystemAdminUser *model.User
tempWorkspace string
SystemManagerClient *model.Client4
SystemManagerUser *model.User
LocalClient *model.Client4
IncludeCacheLayer bool
LogBuffer *mlog.Buffer
TestLogger *mlog.Logger
}
var mainHelper *testlib.MainHelper
func SetMainHelper(mh *testlib.MainHelper) {
mainHelper = mh
}
func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, enterprise bool, includeCache bool,
updateConfig func(*model.Config), options []app.Option) *TestHelper {
tempWorkspace, err := os.MkdirTemp("", "apptest")
if err != nil {
panic(err)
}
memoryStore, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{IgnoreEnvironmentOverrides: true})
if err != nil {
panic("failed to initialize memory store: " + err.Error())
}
memoryConfig := &model.Config{}
memoryConfig.SetDefaults()
*memoryConfig.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins")
*memoryConfig.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp")
memoryConfig.ServiceSettings.EnableLocalMode = model.NewBool(true)
*memoryConfig.ServiceSettings.LocalModeSocketLocation = filepath.Join(tempWorkspace, "mattermost_local.sock")
*memoryConfig.AnnouncementSettings.AdminNoticesEnabled = false
*memoryConfig.AnnouncementSettings.UserNoticesEnabled = false
*memoryConfig.PluginSettings.AutomaticPrepackagedPlugins = false
if updateConfig != nil {
updateConfig(memoryConfig)
}
memoryStore.Set(memoryConfig)
configStore, err := config.NewStoreFromBacking(memoryStore, nil, false)
if err != nil {
panic(err)
}
options = append(options, app.ConfigStore(configStore))
if includeCache {
// Adds the cache layer to the test store
options = append(options, app.StoreOverrideWithCache(dbStore))
} else {
options = append(options, app.StoreOverride(dbStore))
}
buffer := &mlog.Buffer{}
testLogger, _ := mlog.NewLogger()
logCfg, _ := config.MloggerConfigFromLoggerConfig(&memoryConfig.LogSettings, nil, config.GetLogFileLocation)
if errCfg := testLogger.ConfigureTargets(logCfg, nil); errCfg != nil {
panic("failed to configure test logger: " + errCfg.Error())
}
if errW := mlog.AddWriterTarget(testLogger, buffer, true, mlog.StdAll...); errW != nil {
panic("failed to add writer target to test logger: " + errW.Error())
}
// lock logger config so server init cannot override it during testing.
testLogger.LockConfiguration()
options = append(options, app.SetLogger(testLogger))
s, err := app.NewServer(options...)
if err != nil {
panic(err)
}
th := &TestHelper{
App: app.New(app.ServerConnector(s.Channels())),
Server: s,
ConfigStore: configStore,
IncludeCacheLayer: includeCache,
Context: request.EmptyContext(testLogger),
TestLogger: testLogger,
LogBuffer: buffer,
}
th.Context.SetLogger(testLogger)
if s.Platform().SearchEngine != nil && s.Platform().SearchEngine.BleveEngine != nil && searchEngine != nil {
searchEngine.BleveEngine = s.Platform().SearchEngine.BleveEngine
}
if searchEngine != nil {
th.App.SetSearchEngine(searchEngine)
}
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.TeamSettings.MaxUsersPerTeam = 50
*cfg.RateLimitSettings.Enable = false
*cfg.EmailSettings.SendEmailNotifications = true
*cfg.ServiceSettings.SiteURL = ""
// Disable sniffing, otherwise elastic client fails to connect to docker node
// More details: https://github.com/olivere/elastic/wiki/Sniffing
*cfg.ElasticsearchSettings.Sniff = false
*cfg.TeamSettings.EnableOpenServer = true
// Disable strict password requirements for test
*cfg.PasswordSettings.MinimumLength = 5
*cfg.PasswordSettings.Lowercase = false
*cfg.PasswordSettings.Uppercase = false
*cfg.PasswordSettings.Symbol = false
*cfg.PasswordSettings.Number = false
*cfg.ServiceSettings.ListenAddress = ":0"
})
if err := th.Server.Start(); err != nil {
panic(err)
}
Init(th.App.Srv())
web.New(th.App.Srv())
wsapi.Init(th.App.Srv())
if enterprise {
th.App.Srv().SetLicense(model.NewTestLicense())
} else {
th.App.Srv().SetLicense(nil)
}
th.Client = th.CreateClient()
th.GraphQLClient = newGraphQLClient(fmt.Sprintf("http://localhost:%v", th.App.Srv().ListenAddr.Port))
th.SystemAdminClient = th.CreateClient()
th.SystemManagerClient = th.CreateClient()
// Verify handling of the supported true/false values by randomizing on each run.
rand.Seed(time.Now().UTC().UnixNano())
trueValues := []string{"1", "t", "T", "TRUE", "true", "True"}
falseValues := []string{"0", "f", "F", "FALSE", "false", "False"}
trueString := trueValues[rand.Intn(len(trueValues))]
falseString := falseValues[rand.Intn(len(falseValues))]
mlog.Debug("Configured Client4 bool string values", mlog.String("true", trueString), mlog.String("false", falseString))
th.Client.SetBoolString(true, trueString)
th.Client.SetBoolString(false, falseString)
th.LocalClient = th.CreateLocalClient(*memoryConfig.ServiceSettings.LocalModeSocketLocation)
if th.tempWorkspace == "" {
th.tempWorkspace = tempWorkspace
}
return th
}
func SetupEnterprise(tb testing.TB, options ...app.Option) *TestHelper {
if testing.Short() {
tb.SkipNow()
}
if mainHelper == nil {
tb.SkipNow()
}
dbStore := mainHelper.GetStore()
dbStore.DropAllTables()
dbStore.MarkSystemRanUnitTests()
mainHelper.PreloadMigrations()
searchEngine := mainHelper.GetSearchEngine()
th := setupTestHelper(dbStore, searchEngine, true, true, nil, options)
th.InitLogin()
return th
}
func Setup(tb testing.TB) *TestHelper {
if testing.Short() {
tb.SkipNow()
}
if mainHelper == nil {
tb.SkipNow()
}
dbStore := mainHelper.GetStore()
dbStore.DropAllTables()
dbStore.MarkSystemRanUnitTests()
mainHelper.PreloadMigrations()
searchEngine := mainHelper.GetSearchEngine()
th := setupTestHelper(dbStore, searchEngine, false, true, nil, nil)
th.InitLogin()
return th
}
func SetupAndApplyConfigBeforeLogin(tb testing.TB, updateConfig func(cfg *model.Config)) *TestHelper {
if testing.Short() {
tb.SkipNow()
}
if mainHelper == nil {
tb.SkipNow()
}
dbStore := mainHelper.GetStore()
dbStore.DropAllTables()
dbStore.MarkSystemRanUnitTests()
mainHelper.PreloadMigrations()
searchEngine := mainHelper.GetSearchEngine()
th := setupTestHelper(dbStore, searchEngine, false, true, nil, nil)
th.App.UpdateConfig(updateConfig)
th.InitLogin()
return th
}
func SetupConfig(tb testing.TB, updateConfig func(cfg *model.Config)) *TestHelper {
if testing.Short() {
tb.SkipNow()
}
if mainHelper == nil {
tb.SkipNow()
}
dbStore := mainHelper.GetStore()
dbStore.DropAllTables()
dbStore.MarkSystemRanUnitTests()
searchEngine := mainHelper.GetSearchEngine()
th := setupTestHelper(dbStore, searchEngine, false, true, updateConfig, nil)
th.InitLogin()
return th
}
func SetupConfigWithStoreMock(tb testing.TB, updateConfig func(cfg *model.Config)) *TestHelper {
th := setupTestHelper(testlib.GetMockStoreForSetupFunctions(), nil, false, false, updateConfig, nil)
statusMock := mocks.StatusStore{}
statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil)
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil)
statusMock.On("UpdateLastActivityAt", "user1", mock.Anything).Return(nil)
statusMock.On("SaveOrUpdate", mock.AnythingOfType("*model.Status")).Return(nil)
emptyMockStore := mocks.Store{}
emptyMockStore.On("Close").Return(nil)
emptyMockStore.On("Status").Return(&statusMock)
th.App.Srv().SetStore(&emptyMockStore)
return th
}
func SetupWithStoreMock(tb testing.TB) *TestHelper {
th := setupTestHelper(testlib.GetMockStoreForSetupFunctions(), nil, false, false, nil, nil)
statusMock := mocks.StatusStore{}
statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil)
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil)
statusMock.On("UpdateLastActivityAt", "user1", mock.Anything).Return(nil)
statusMock.On("SaveOrUpdate", mock.AnythingOfType("*model.Status")).Return(nil)
emptyMockStore := mocks.Store{}
emptyMockStore.On("Close").Return(nil)
emptyMockStore.On("Status").Return(&statusMock)
th.App.Srv().SetStore(&emptyMockStore)
return th
}
func SetupEnterpriseWithStoreMock(tb testing.TB, options ...app.Option) *TestHelper {
th := setupTestHelper(testlib.GetMockStoreForSetupFunctions(), nil, true, false, nil, options)
statusMock := mocks.StatusStore{}
statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil)
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil)
statusMock.On("UpdateLastActivityAt", "user1", mock.Anything).Return(nil)
statusMock.On("SaveOrUpdate", mock.AnythingOfType("*model.Status")).Return(nil)
emptyMockStore := mocks.Store{}
emptyMockStore.On("Close").Return(nil)
emptyMockStore.On("Status").Return(&statusMock)
th.App.Srv().SetStore(&emptyMockStore)
return th
}
func SetupWithServerOptions(tb testing.TB, options []app.Option) *TestHelper {
if testing.Short() {
tb.SkipNow()
}
if mainHelper == nil {
tb.SkipNow()
}
dbStore := mainHelper.GetStore()
dbStore.DropAllTables()
dbStore.MarkSystemRanUnitTests()
mainHelper.PreloadMigrations()
searchEngine := mainHelper.GetSearchEngine()
th := setupTestHelper(dbStore, searchEngine, false, true, nil, options)
th.InitLogin()
return th
}
func (th *TestHelper) ShutdownApp() {
done := make(chan bool)
go func() {
th.Server.Shutdown()
close(done)
}()
select {
case <-done:
case <-time.After(30 * time.Second):
// panic instead of fatal to terminate all tests in this package, otherwise the
// still running App could spuriously fail subsequent tests.
panic("failed to shutdown App within 30 seconds")
}
}
func (th *TestHelper) TearDown() {
if th.IncludeCacheLayer {
// Clean all the caches
th.App.Srv().InvalidateAllCaches()
}
th.ShutdownApp()
}
func closeBody(r *http.Response) {
if r.Body != nil {
_, _ = io.Copy(io.Discard, r.Body)
_ = r.Body.Close()
}
}
var initBasicOnce sync.Once
var userCache struct {
SystemAdminUser *model.User
SystemManagerUser *model.User
TeamAdminUser *model.User
BasicUser *model.User
BasicUser2 *model.User
}
func (th *TestHelper) InitLogin() *TestHelper {
th.waitForConnectivity()
// create users once and cache them because password hashing is slow
initBasicOnce.Do(func() {
th.SystemAdminUser = th.CreateUser()
th.App.UpdateUserRoles(th.Context, th.SystemAdminUser.Id, model.SystemUserRoleId+" "+model.SystemAdminRoleId, false)
th.SystemAdminUser, _ = th.App.GetUser(th.SystemAdminUser.Id)
userCache.SystemAdminUser = th.SystemAdminUser.DeepCopy()
th.SystemManagerUser = th.CreateUser()
th.App.UpdateUserRoles(th.Context, th.SystemManagerUser.Id, model.SystemUserRoleId+" "+model.SystemManagerRoleId, false)
th.SystemManagerUser, _ = th.App.GetUser(th.SystemManagerUser.Id)
userCache.SystemManagerUser = th.SystemManagerUser.DeepCopy()
th.TeamAdminUser = th.CreateUser()
th.App.UpdateUserRoles(th.Context, th.TeamAdminUser.Id, model.SystemUserRoleId, false)
th.TeamAdminUser, _ = th.App.GetUser(th.TeamAdminUser.Id)
userCache.TeamAdminUser = th.TeamAdminUser.DeepCopy()
th.BasicUser = th.CreateUser()
th.BasicUser, _ = th.App.GetUser(th.BasicUser.Id)
userCache.BasicUser = th.BasicUser.DeepCopy()
th.BasicUser2 = th.CreateUser()
th.BasicUser2, _ = th.App.GetUser(th.BasicUser2.Id)
userCache.BasicUser2 = th.BasicUser2.DeepCopy()
})
// restore cached users
th.SystemAdminUser = userCache.SystemAdminUser.DeepCopy()
th.SystemManagerUser = userCache.SystemManagerUser.DeepCopy()
th.TeamAdminUser = userCache.TeamAdminUser.DeepCopy()
th.BasicUser = userCache.BasicUser.DeepCopy()
th.BasicUser2 = userCache.BasicUser2.DeepCopy()
users := []*model.User{th.SystemAdminUser, th.TeamAdminUser, th.BasicUser, th.BasicUser2, th.SystemManagerUser}
mainHelper.GetSQLStore().User().InsertUsers(users)
// restore non hashed password for login
th.SystemAdminUser.Password = "Pa$$word11"
th.TeamAdminUser.Password = "Pa$$word11"
th.BasicUser.Password = "Pa$$word11"
th.BasicUser2.Password = "Pa$$word11"
th.SystemManagerUser.Password = "Pa$$word11"
var wg sync.WaitGroup
wg.Add(2)
go func() {
th.LoginSystemAdmin()
wg.Done()
}()
go func() {
th.LoginTeamAdmin()
wg.Done()
}()
wg.Wait()
return th
}
func (th *TestHelper) InitBasic() *TestHelper {
th.BasicTeam = th.CreateTeam()
th.BasicChannel = th.CreatePublicChannel()
th.BasicPrivateChannel = th.CreatePrivateChannel()
th.BasicPrivateChannel2 = th.CreatePrivateChannel()
th.BasicDeletedChannel = th.CreatePublicChannel()
th.BasicChannel2 = th.CreatePublicChannel()
th.BasicPost = th.CreatePost()
th.LinkUserToTeam(th.BasicUser, th.BasicTeam)
th.LinkUserToTeam(th.BasicUser2, th.BasicTeam)
th.App.AddUserToChannel(th.Context, th.BasicUser, th.BasicChannel, false)
th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicChannel, false)
th.App.AddUserToChannel(th.Context, th.BasicUser, th.BasicChannel2, false)
th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicChannel2, false)
th.App.AddUserToChannel(th.Context, th.BasicUser, th.BasicPrivateChannel, false)
th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicPrivateChannel, false)
th.App.AddUserToChannel(th.Context, th.BasicUser, th.BasicDeletedChannel, false)
th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicDeletedChannel, false)
th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId, false)
th.Client.DeleteChannel(th.BasicDeletedChannel.Id)
th.LoginBasic()
th.Group = th.CreateGroup()
return th
}
func (th *TestHelper) waitForConnectivity() {
for i := 0; i < 1000; i++ {
conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%v", th.App.Srv().ListenAddr.Port))
if err == nil {
conn.Close()
return
}
time.Sleep(time.Millisecond * 20)
}
panic("unable to connect")
}
func (th *TestHelper) CreateClient() *model.Client4 {
return model.NewAPIv4Client(fmt.Sprintf("http://localhost:%v", th.App.Srv().ListenAddr.Port))
}
// ToDo: maybe move this to NewAPIv4SocketClient and reuse it in mmctl
func (th *TestHelper) CreateLocalClient(socketPath string) *model.Client4 {
httpClient := &http.Client{
Transport: &http.Transport{
Dial: func(network, addr string) (net.Conn, error) {
return net.Dial("unix", socketPath)
},
},
}
return &model.Client4{
APIURL: "http://_" + model.APIURLSuffix,
HTTPClient: httpClient,
}
}
func (th *TestHelper) CreateWebSocketClient() (*model.WebSocketClient, error) {
return model.NewWebSocketClient4(fmt.Sprintf("ws://localhost:%v", th.App.Srv().ListenAddr.Port), th.Client.AuthToken)
}
func (th *TestHelper) CreateReliableWebSocketClient(connID string, seqNo int) (*model.WebSocketClient, error) {
return model.NewReliableWebSocketClientWithDialer(websocket.DefaultDialer, fmt.Sprintf("ws://localhost:%v", th.App.Srv().ListenAddr.Port), th.Client.AuthToken, connID, seqNo, true)
}
func (th *TestHelper) CreateWebSocketSystemAdminClient() (*model.WebSocketClient, error) {
return model.NewWebSocketClient4(fmt.Sprintf("ws://localhost:%v", th.App.Srv().ListenAddr.Port), th.SystemAdminClient.AuthToken)
}
func (th *TestHelper) CreateWebSocketClientWithClient(client *model.Client4) (*model.WebSocketClient, error) {
return model.NewWebSocketClient4(fmt.Sprintf("ws://localhost:%v", th.App.Srv().ListenAddr.Port), client.AuthToken)
}
func (th *TestHelper) CreateBotWithSystemAdminClient() *model.Bot {
return th.CreateBotWithClient((th.SystemAdminClient))
}
func (th *TestHelper) CreateBotWithClient(client *model.Client4) *model.Bot {
bot := &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "a bot",
Description: "bot",
}
rbot, _, err := client.CreateBot(bot)
if err != nil {
panic(err)
}
return rbot
}
func (th *TestHelper) CreateUser() *model.User {
return th.CreateUserWithClient(th.Client)
}
func (th *TestHelper) CreateTeam() *model.Team {
return th.CreateTeamWithClient(th.Client)
}
func (th *TestHelper) CreateTeamWithClient(client *model.Client4) *model.Team {
id := model.NewId()
team := &model.Team{
DisplayName: "dn_" + id,
Name: GenerateTestTeamName(),
Email: th.GenerateTestEmail(),
Type: model.TeamOpen,
}
rteam, _, err := client.CreateTeam(team)
if err != nil {
panic(err)
}
return rteam
}
func (th *TestHelper) CreateUserWithClient(client *model.Client4) *model.User {
id := model.NewId()
user := &model.User{
Email: th.GenerateTestEmail(),
Username: GenerateTestUsername(),
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Pa$$word11",
}
ruser, _, err := client.CreateUser(user)
if err != nil {
panic(err)
}
ruser.Password = "Pa$$word11"
_, err = th.App.Srv().Store().User().VerifyEmail(ruser.Id, ruser.Email)
if err != nil {
return nil
}
return ruser
}
func (th *TestHelper) CreateUserWithAuth(authService string) *model.User {
id := model.NewId()
user := &model.User{
Email: "success+" + id + "@simulator.amazonses.com",
Username: "un_" + id,
Nickname: "nn_" + id,
EmailVerified: true,
AuthService: authService,
}
user, err := th.App.CreateUser(th.Context, user)
if err != nil {
panic(err)
}
return user
}
func (th *TestHelper) SetupLdapConfig() {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableMultifactorAuthentication = true
*cfg.LdapSettings.Enable = true
*cfg.LdapSettings.EnableSync = true
*cfg.LdapSettings.LdapServer = "dockerhost"
*cfg.LdapSettings.BaseDN = "dc=mm,dc=test,dc=com"
*cfg.LdapSettings.BindUsername = "cn=admin,dc=mm,dc=test,dc=com"
*cfg.LdapSettings.BindPassword = "mostest"
*cfg.LdapSettings.FirstNameAttribute = "cn"
*cfg.LdapSettings.LastNameAttribute = "sn"
*cfg.LdapSettings.NicknameAttribute = "cn"
*cfg.LdapSettings.EmailAttribute = "mail"
*cfg.LdapSettings.UsernameAttribute = "uid"
*cfg.LdapSettings.IdAttribute = "cn"
*cfg.LdapSettings.LoginIdAttribute = "uid"
*cfg.LdapSettings.SkipCertificateVerification = true
*cfg.LdapSettings.GroupFilter = ""
*cfg.LdapSettings.GroupDisplayNameAttribute = "cN"
*cfg.LdapSettings.GroupIdAttribute = "entRyUuId"
*cfg.LdapSettings.MaxPageSize = 0
})
th.App.Srv().SetLicense(model.NewTestLicense("ldap"))
}
func (th *TestHelper) SetupSamlConfig() {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.SamlSettings.Enable = true
*cfg.SamlSettings.Verify = false
*cfg.SamlSettings.Encrypt = false
*cfg.SamlSettings.IdpURL = "https://does.notmatter.example"
*cfg.SamlSettings.IdpDescriptorURL = "https://localhost/adfs/services/trust"
*cfg.SamlSettings.AssertionConsumerServiceURL = "https://localhost/login/sso/saml"
*cfg.SamlSettings.ServiceProviderIdentifier = "https://localhost/login/sso/saml"
*cfg.SamlSettings.IdpCertificateFile = app.SamlIdpCertificateName
*cfg.SamlSettings.PrivateKeyFile = app.SamlPrivateKeyName
*cfg.SamlSettings.PublicCertificateFile = app.SamlPublicCertificateName
*cfg.SamlSettings.EmailAttribute = "Email"
*cfg.SamlSettings.UsernameAttribute = "Username"
*cfg.SamlSettings.FirstNameAttribute = "FirstName"
*cfg.SamlSettings.LastNameAttribute = "LastName"
*cfg.SamlSettings.NicknameAttribute = ""
*cfg.SamlSettings.PositionAttribute = ""
*cfg.SamlSettings.LocaleAttribute = ""
*cfg.SamlSettings.SignatureAlgorithm = model.SamlSettingsSignatureAlgorithmSha256
*cfg.SamlSettings.CanonicalAlgorithm = model.SamlSettingsCanonicalAlgorithmC14n11
})
th.App.Srv().SetLicense(model.NewTestLicense("saml"))
}
func (th *TestHelper) CreatePublicChannel() *model.Channel {
return th.CreateChannelWithClient(th.Client, model.ChannelTypeOpen)
}
func (th *TestHelper) CreatePrivateChannel() *model.Channel {
return th.CreateChannelWithClient(th.Client, model.ChannelTypePrivate)
}
func (th *TestHelper) CreateChannelWithClient(client *model.Client4, channelType model.ChannelType) *model.Channel {
return th.CreateChannelWithClientAndTeam(client, channelType, th.BasicTeam.Id)
}
func (th *TestHelper) CreateChannelWithClientAndTeam(client *model.Client4, channelType model.ChannelType, teamId string) *model.Channel {
id := model.NewId()
channel := &model.Channel{
DisplayName: "dn_" + id,
Name: GenerateTestChannelName(),
Type: channelType,
TeamId: teamId,
}
rchannel, _, err := client.CreateChannel(channel)
if err != nil {
panic(err)
}
return rchannel
}
func (th *TestHelper) CreatePost() *model.Post {
return th.CreatePostWithClient(th.Client, th.BasicChannel)
}
func (th *TestHelper) CreatePinnedPost() *model.Post {
return th.CreatePinnedPostWithClient(th.Client, th.BasicChannel)
}
func (th *TestHelper) CreateMessagePost(message string) *model.Post {
return th.CreateMessagePostWithClient(th.Client, th.BasicChannel, message)
}
func (th *TestHelper) CreatePostWithFiles(files ...*model.FileInfo) *model.Post {
return th.CreatePostWithFilesWithClient(th.Client, th.BasicChannel, files...)
}
func (th *TestHelper) CreatePostInChannelWithFiles(channel *model.Channel, files ...*model.FileInfo) *model.Post {
return th.CreatePostWithFilesWithClient(th.Client, channel, files...)
}
func (th *TestHelper) CreatePostWithFilesWithClient(client *model.Client4, channel *model.Channel, files ...*model.FileInfo) *model.Post {
var fileIds model.StringArray
for i := range files {
fileIds = append(fileIds, files[i].Id)
}
post := &model.Post{
ChannelId: channel.Id,
Message: "message_" + model.NewId(),
FileIds: fileIds,
}
rpost, _, err := client.CreatePost(post)
if err != nil {
panic(err)
}
return rpost
}
func (th *TestHelper) CreatePostWithClient(client *model.Client4, channel *model.Channel) *model.Post {
id := model.NewId()
post := &model.Post{
ChannelId: channel.Id,
Message: "message_" + id,
}
rpost, _, err := client.CreatePost(post)
if err != nil {
panic(err)
}
return rpost
}
func (th *TestHelper) CreatePinnedPostWithClient(client *model.Client4, channel *model.Channel) *model.Post {
id := model.NewId()
post := &model.Post{
ChannelId: channel.Id,
Message: "message_" + id,
IsPinned: true,
}
rpost, _, err := client.CreatePost(post)
if err != nil {
panic(err)
}
return rpost
}
func (th *TestHelper) CreateMessagePostWithClient(client *model.Client4, channel *model.Channel, message string) *model.Post {
post := &model.Post{
ChannelId: channel.Id,
Message: message,
}
rpost, _, err := client.CreatePost(post)
if err != nil {
panic(err)
}
return rpost
}
func (th *TestHelper) CreateMessagePostNoClient(channel *model.Channel, message string, createAtTime int64) *model.Post {
post, err := th.App.Srv().Store().Post().Save(&model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
Message: message,
CreateAt: createAtTime,
})
if err != nil {
panic(err)
}
return post
}
func (th *TestHelper) CreateDmChannel(user *model.User) *model.Channel {
var err *model.AppError
var channel *model.Channel
if channel, err = th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, user.Id); err != nil {
panic(err)
}
return channel
}
func (th *TestHelper) LoginBasic() {
th.LoginBasicWithClient(th.Client)
if os.Getenv("MM_FEATUREFLAGS_GRAPHQL") == "true" {
th.LoginBasicWithGraphQL()
}
}
func (th *TestHelper) LoginBasic2() {
th.LoginBasic2WithClient(th.Client)
if os.Getenv("MM_FEATUREFLAGS_GRAPHQL") == "true" {
th.LoginBasicWithGraphQL()
}
}
func (th *TestHelper) LoginTeamAdmin() {
th.LoginTeamAdminWithClient(th.Client)
}
func (th *TestHelper) LoginSystemAdmin() {
th.LoginSystemAdminWithClient(th.SystemAdminClient)
}
func (th *TestHelper) LoginSystemManager() {
th.LoginSystemManagerWithClient(th.SystemManagerClient)
}
func (th *TestHelper) LoginBasicWithClient(client *model.Client4) {
_, _, err := client.Login(th.BasicUser.Email, th.BasicUser.Password)
if err != nil {
panic(err)
}
}
func (th *TestHelper) LoginBasicWithGraphQL() {
_, _, err := th.GraphQLClient.login(th.BasicUser.Email, th.BasicUser.Password)
if err != nil {
panic(err)
}
}
func (th *TestHelper) LoginBasic2WithClient(client *model.Client4) {
_, _, err := client.Login(th.BasicUser2.Email, th.BasicUser2.Password)
if err != nil {
panic(err)
}
}
func (th *TestHelper) LoginTeamAdminWithClient(client *model.Client4) {
_, _, err := client.Login(th.TeamAdminUser.Email, th.TeamAdminUser.Password)
if err != nil {
panic(err)
}
}
func (th *TestHelper) LoginSystemManagerWithClient(client *model.Client4) {
_, _, err := client.Login(th.SystemManagerUser.Email, th.SystemManagerUser.Password)
if err != nil {
panic(err)
}
}
func (th *TestHelper) LoginSystemAdminWithClient(client *model.Client4) {
_, _, err := client.Login(th.SystemAdminUser.Email, th.SystemAdminUser.Password)
if err != nil {
panic(err)
}
}
func (th *TestHelper) UpdateActiveUser(user *model.User, active bool) {
_, err := th.App.UpdateActive(th.Context, user, active)
if err != nil {
panic(err)
}
}
func (th *TestHelper) LinkUserToTeam(user *model.User, team *model.Team) {
_, err := th.App.JoinUserToTeam(th.Context, team, user, "")
if err != nil {
panic(err)
}
}
func (th *TestHelper) UnlinkUserFromTeam(user *model.User, team *model.Team) {
err := th.App.RemoveUserFromTeam(th.Context, team.Id, user.Id, "")
if err != nil {
panic(err)
}
}
func (th *TestHelper) AddUserToChannel(user *model.User, channel *model.Channel) *model.ChannelMember {
member, err := th.App.AddUserToChannel(th.Context, user, channel, false)
if err != nil {
panic(err)
}
return member
}
func (th *TestHelper) RemoveUserFromChannel(user *model.User, channel *model.Channel) {
err := th.App.RemoveUserFromChannel(th.Context, user.Id, "", channel)
if err != nil {
panic(err)
}
}
func (th *TestHelper) GenerateTestEmail() string {
if *th.App.Config().EmailSettings.SMTPServer != "localhost" && os.Getenv("CI_INBUCKET_PORT") == "" {
return strings.ToLower("success+" + model.NewId() + "@simulator.amazonses.com")
}
return strings.ToLower(model.NewId() + "@localhost")
}
func (th *TestHelper) CreateGroup() *model.Group {
id := model.NewId()
group := &model.Group{
Name: model.NewString("n-" + id),
DisplayName: "dn_" + id,
Source: model.GroupSourceLdap,
RemoteId: model.NewString("ri_" + model.NewId()),
}
group, err := th.App.CreateGroup(group)
if err != nil {
panic(err)
}
return group
}
// TestForSystemAdminAndLocal runs a test function for both
// SystemAdmin and Local clients. Several endpoints work in the same
// way when used by a fully privileged user and through the local
// mode, so this helper facilitates checking both
func (th *TestHelper) TestForSystemAdminAndLocal(t *testing.T, f func(*testing.T, *model.Client4), name ...string) {
var testName string
if len(name) > 0 {
testName = name[0] + "/"
}
t.Run(testName+"SystemAdminClient", func(t *testing.T) {
f(t, th.SystemAdminClient)
})
t.Run(testName+"LocalClient", func(t *testing.T) {
f(t, th.LocalClient)
})
}
// TestForAllClients runs a test function for all the clients
// registered in the TestHelper
func (th *TestHelper) TestForAllClients(t *testing.T, f func(*testing.T, *model.Client4), name ...string) {
var testName string
if len(name) > 0 {
testName = name[0] + "/"
}
t.Run(testName+"Client", func(t *testing.T) {
f(t, th.Client)
})
t.Run(testName+"SystemAdminClient", func(t *testing.T) {
f(t, th.SystemAdminClient)
})
t.Run(testName+"LocalClient", func(t *testing.T) {
f(t, th.LocalClient)
})
}
func GenerateTestUsername() string {
return "fakeuser" + model.NewRandomString(10)
}
func GenerateTestTeamName() string {
return "faketeam" + model.NewRandomString(6)
}
func GenerateTestChannelName() string {
return "fakechannel" + model.NewRandomString(10)
}
func GenerateTestAppName() string {
return "fakeoauthapp" + model.NewRandomString(10)
}
func GenerateTestId() string {
return model.NewId()
}
func CheckUserSanitization(tb testing.TB, user *model.User) {
tb.Helper()
require.Equal(tb, "", user.Password, "password wasn't blank")
require.Empty(tb, user.AuthData, "auth data wasn't blank")
require.Equal(tb, "", user.MfaSecret, "mfa secret wasn't blank")
}
func CheckEtag(tb testing.TB, data any, resp *model.Response) {
tb.Helper()
require.Empty(tb, data)
require.Equal(tb, http.StatusNotModified, resp.StatusCode, "wrong status code for etag")
}
func checkHTTPStatus(tb testing.TB, resp *model.Response, expectedStatus int) {
tb.Helper()
require.NotNilf(tb, resp, "Unexpected nil response, expected http status:%v", expectedStatus)
require.Equalf(tb, expectedStatus, resp.StatusCode, "Expected http status:%v, got %v", expectedStatus, resp.StatusCode)
}
func CheckOKStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusOK)
}
func CheckCreatedStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusCreated)
}
func CheckForbiddenStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusForbidden)
}
func CheckUnauthorizedStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusUnauthorized)
}
func CheckNotFoundStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusNotFound)
}
func CheckBadRequestStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusBadRequest)
}
func CheckNotImplementedStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusNotImplemented)
}
func CheckRequestEntityTooLargeStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusRequestEntityTooLarge)
}
func CheckInternalErrorStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusInternalServerError)
}
func CheckServiceUnavailableStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusServiceUnavailable)
}
func CheckErrorID(tb testing.TB, err error, errorId string) {
tb.Helper()
require.Error(tb, err, "should have errored with id: %s", errorId)
var appError *model.AppError
ok := errors.As(err, &appError)
require.True(tb, ok, "should have been a model.AppError")
require.Equalf(tb, errorId, appError.Id, "incorrect error id, actual: %s, expected: %s", appError.Id, errorId)
}
func CheckErrorMessage(tb testing.TB, err error, message string) {
tb.Helper()
require.Error(tb, err, "should have errored with message: %s", message)
var appError *model.AppError
ok := errors.As(err, &appError)
require.True(tb, ok, "should have been a model.AppError")
require.Equalf(tb, message, appError.Message, "incorrect error message, actual: %s, expected: %s", appError.Id, message)
}
func CheckStartsWith(tb testing.TB, value, prefix, message string) {
tb.Helper()
require.True(tb, strings.HasPrefix(value, prefix), message, value)
}
// Similar to s3.New() but allows initialization of signature v2 or signature v4 client.
// If signV2 input is false, function always returns signature v4.
//
// Additionally this function also takes a user defined region, if set
// disables automatic region lookup.
func s3New(endpoint, accessKey, secretKey string, secure bool, signV2 bool, region string) (*s3.Client, error) {
var creds *credentials.Credentials
if signV2 {
creds = credentials.NewStatic(accessKey, secretKey, "", credentials.SignatureV2)
} else {
creds = credentials.NewStatic(accessKey, secretKey, "", credentials.SignatureV4)
}
opts := s3.Options{
Creds: creds,
Secure: secure,
Region: region,
}
return s3.New(endpoint, &opts)
}
func (th *TestHelper) cleanupTestFile(info *model.FileInfo) error {
cfg := th.App.Config()
if *cfg.FileSettings.DriverName == model.ImageDriverS3 {
endpoint := *cfg.FileSettings.AmazonS3Endpoint
accessKey := *cfg.FileSettings.AmazonS3AccessKeyId
secretKey := *cfg.FileSettings.AmazonS3SecretAccessKey
secure := *cfg.FileSettings.AmazonS3SSL
signV2 := *cfg.FileSettings.AmazonS3SignV2
region := *cfg.FileSettings.AmazonS3Region
s3Clnt, err := s3New(endpoint, accessKey, secretKey, secure, signV2, region)
if err != nil {
return err
}
bucket := *cfg.FileSettings.AmazonS3Bucket
if err := s3Clnt.RemoveObject(context.Background(), bucket, info.Path, s3.RemoveObjectOptions{}); err != nil {
return err
}
if info.ThumbnailPath != "" {
if err := s3Clnt.RemoveObject(context.Background(), bucket, info.ThumbnailPath, s3.RemoveObjectOptions{}); err != nil {
return err
}
}
if info.PreviewPath != "" {
if err := s3Clnt.RemoveObject(context.Background(), bucket, info.PreviewPath, s3.RemoveObjectOptions{}); err != nil {
return err
}
}
} else if *cfg.FileSettings.DriverName == model.ImageDriverLocal {
if err := os.Remove(*cfg.FileSettings.Directory + info.Path); err != nil {
return err
}
if info.ThumbnailPath != "" {
if err := os.Remove(*cfg.FileSettings.Directory + info.ThumbnailPath); err != nil {
return err
}
}
if info.PreviewPath != "" {
if err := os.Remove(*cfg.FileSettings.Directory + info.PreviewPath); err != nil {
return err
}
}
}
return nil
}
func (th *TestHelper) MakeUserChannelAdmin(user *model.User, channel *model.Channel) {
if cm, err := th.App.Srv().Store().Channel().GetMember(context.Background(), channel.Id, user.Id); err == nil {
cm.SchemeAdmin = true
if _, err = th.App.Srv().Store().Channel().UpdateMember(cm); err != nil {
panic(err)
}
} else {
panic(err)
}
}
func (th *TestHelper) UpdateUserToTeamAdmin(user *model.User, team *model.Team) {
if tm, err := th.App.Srv().Store().Team().GetMember(context.Background(), team.Id, user.Id); err == nil {
tm.SchemeAdmin = true
if _, err = th.App.Srv().Store().Team().UpdateMember(tm); err != nil {
panic(err)
}
} else {
panic(err)
}
}
func (th *TestHelper) UpdateUserToNonTeamAdmin(user *model.User, team *model.Team) {
if tm, err := th.App.Srv().Store().Team().GetMember(context.Background(), team.Id, user.Id); err == nil {
tm.SchemeAdmin = false
if _, err = th.App.Srv().Store().Team().UpdateMember(tm); err != nil {
panic(err)
}
} else {
panic(err)
}
}
func (th *TestHelper) SaveDefaultRolePermissions() map[string][]string {
results := make(map[string][]string)
for _, roleName := range []string{
"system_user",
"system_admin",
"team_user",
"team_admin",
"channel_user",
"channel_admin",
} {
role, err1 := th.App.GetRoleByName(context.Background(), roleName)
if err1 != nil {
panic(err1)
}
results[roleName] = role.Permissions
}
return results
}
func (th *TestHelper) RestoreDefaultRolePermissions(data map[string][]string) {
for roleName, permissions := range data {
role, err1 := th.App.GetRoleByName(context.Background(), roleName)
if err1 != nil {
panic(err1)
}
if strings.Join(role.Permissions, " ") == strings.Join(permissions, " ") {
continue
}
role.Permissions = permissions
_, err2 := th.App.UpdateRole(role)
if err2 != nil {
panic(err2)
}
}
}
func (th *TestHelper) RemovePermissionFromRole(permission string, roleName string) {
role, err1 := th.App.GetRoleByName(context.Background(), roleName)
if err1 != nil {
panic(err1)
}
var newPermissions []string
for _, p := range role.Permissions {
if p != permission {
newPermissions = append(newPermissions, p)
}
}
if strings.Join(role.Permissions, " ") == strings.Join(newPermissions, " ") {
return
}
role.Permissions = newPermissions
_, err2 := th.App.UpdateRole(role)
if err2 != nil {
panic(err2)
}
}
func (th *TestHelper) AddPermissionToRole(permission string, roleName string) {
role, err1 := th.App.GetRoleByName(context.Background(), roleName)
if err1 != nil {
panic(err1)
}
for _, existingPermission := range role.Permissions {
if existingPermission == permission {
return
}
}
role.Permissions = append(role.Permissions, permission)
_, err2 := th.App.UpdateRole(role)
if err2 != nil {
panic(err2)
}
}
func (th *TestHelper) SetupTeamScheme() *model.Scheme {
return th.SetupScheme(model.SchemeScopeTeam)
}
func (th *TestHelper) SetupChannelScheme() *model.Scheme {
return th.SetupScheme(model.SchemeScopeChannel)
}
func (th *TestHelper) SetupScheme(scope string) *model.Scheme {
scheme, err := th.App.CreateScheme(&model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Scope: scope,
})
if err != nil {
panic(err)
}
return scheme
}
func (th *TestHelper) MakeGraphQLRequest(input *graphQLInput) (*graphql.Response, error) {
url := fmt.Sprintf("http://localhost:%v", th.App.Srv().ListenAddr.Port) + model.APIURLSuffixV5 + "/graphql"
buf, err := json.Marshal(input)
if err != nil {
panic(err)
}
resp, err := th.GraphQLClient.doAPIRequest("POST", url, bytes.NewReader(buf), map[string]string{})
if err != nil {
panic(err)
}
defer closeBody(resp)
var gqlResp *graphql.Response
err = json.NewDecoder(resp.Body).Decode(&gqlResp)
return gqlResp, err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
)
func (api *API) InitBleve() {
api.BaseRoutes.Bleve.Handle("/purge_indexes", api.APISessionRequired(purgeBleveIndexes)).Methods("POST")
}
func purgeBleveIndexes(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("purgeBleveIndexes", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionPurgeBleveIndexes) {
c.SetPermissionError(model.PermissionPurgeBleveIndexes)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("purgeBleveIndexes", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
if err := c.App.PurgeBleveIndexes(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"strconv"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitBot() {
api.BaseRoutes.Bots.Handle("", api.APISessionRequired(createBot)).Methods("POST")
api.BaseRoutes.Bot.Handle("", api.APISessionRequired(patchBot)).Methods("PUT")
api.BaseRoutes.Bot.Handle("", api.APISessionRequired(getBot)).Methods("GET")
api.BaseRoutes.Bots.Handle("", api.APISessionRequired(getBots)).Methods("GET")
api.BaseRoutes.Bot.Handle("/disable", api.APISessionRequired(disableBot)).Methods("POST")
api.BaseRoutes.Bot.Handle("/enable", api.APISessionRequired(enableBot)).Methods("POST")
api.BaseRoutes.Bot.Handle("/convert_to_user", api.APISessionRequired(convertBotToUser)).Methods("POST")
api.BaseRoutes.Bot.Handle("/assign/{user_id:[A-Za-z0-9]+}", api.APISessionRequired(assignBot)).Methods("POST")
}
func createBot(c *Context, w http.ResponseWriter, r *http.Request) {
var botPatch *model.BotPatch
err := json.NewDecoder(r.Body).Decode(&botPatch)
if err != nil {
c.SetInvalidParamWithErr("bot", err)
return
}
bot := &model.Bot{
OwnerId: c.AppContext.Session().UserId,
}
bot.Patch(botPatch)
auditRec := c.MakeAuditRecord("createBot", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "bot", bot)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateBot) {
c.SetPermissionError(model.PermissionCreateBot)
return
}
if user, err := c.App.GetUser(c.AppContext.Session().UserId); err == nil {
if user.IsBot {
c.SetPermissionError(model.PermissionCreateBot)
return
}
}
if !*c.App.Config().ServiceSettings.EnableBotAccountCreation {
c.Err = model.NewAppError("createBot", "api.bot.create_disabled", nil, "", http.StatusForbidden)
return
}
createdBot, appErr := c.App.CreateBot(c.AppContext, bot)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventObjectType("bot")
auditRec.AddEventResultState(createdBot) // overwrite meta
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(createdBot); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func patchBot(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireBotUserId()
if c.Err != nil {
return
}
botUserId := c.Params.BotUserId
var botPatch *model.BotPatch
err := json.NewDecoder(r.Body).Decode(&botPatch)
if err != nil {
c.SetInvalidParamWithErr("bot", err)
return
}
auditRec := c.MakeAuditRecord("patchBot", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "id", botUserId)
audit.AddEventParameterAuditable(auditRec, "bot", botPatch)
if err := c.App.SessionHasPermissionToManageBot(*c.AppContext.Session(), botUserId); err != nil {
c.Err = err
return
}
updatedBot, appErr := c.App.PatchBot(botUserId, botPatch)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventResultState(updatedBot)
auditRec.AddEventObjectType("bot")
if err := json.NewEncoder(w).Encode(updatedBot); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getBot(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireBotUserId()
if c.Err != nil {
return
}
botUserId := c.Params.BotUserId
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
bot, appErr := c.App.GetBot(botUserId, includeDeleted)
if appErr != nil {
c.Err = appErr
return
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadOthersBots) {
// Allow access to any bot.
} else if bot.OwnerId == c.AppContext.Session().UserId {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadBots) {
// Pretend like the bot doesn't exist at all to avoid revealing that the
// user is a bot. It's kind of silly in this case, sine we created the bot,
// but we don't have read bot permissions.
c.Err = model.MakeBotNotFoundError(botUserId)
return
}
} else {
// Pretend like the bot doesn't exist at all, to avoid revealing that the
// user is a bot.
c.Err = model.MakeBotNotFoundError(botUserId)
return
}
if c.HandleEtag(bot.Etag(), "Get Bot", w, r) {
return
}
if err := json.NewEncoder(w).Encode(bot); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getBots(c *Context, w http.ResponseWriter, r *http.Request) {
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
onlyOrphaned, _ := strconv.ParseBool(r.URL.Query().Get("only_orphaned"))
var OwnerId string
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadOthersBots) {
// Get bots created by any user.
OwnerId = ""
} else if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadBots) {
// Only get bots created by this user.
OwnerId = c.AppContext.Session().UserId
} else {
c.SetPermissionError(model.PermissionReadBots)
return
}
bots, appErr := c.App.GetBots(&model.BotGetOptions{
Page: c.Params.Page,
PerPage: c.Params.PerPage,
OwnerId: OwnerId,
IncludeDeleted: includeDeleted,
OnlyOrphaned: onlyOrphaned,
})
if appErr != nil {
c.Err = appErr
return
}
if c.HandleEtag(bots.Etag(), "Get Bots", w, r) {
return
}
if err := json.NewEncoder(w).Encode(bots); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func disableBot(c *Context, w http.ResponseWriter, _ *http.Request) {
updateBotActive(c, w, false)
}
func enableBot(c *Context, w http.ResponseWriter, _ *http.Request) {
updateBotActive(c, w, true)
}
func updateBotActive(c *Context, w http.ResponseWriter, active bool) {
c.RequireBotUserId()
if c.Err != nil {
return
}
botUserId := c.Params.BotUserId
auditRec := c.MakeAuditRecord("updateBotActive", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "id", botUserId)
audit.AddEventParameter(auditRec, "enable", active)
if err := c.App.SessionHasPermissionToManageBot(*c.AppContext.Session(), botUserId); err != nil {
c.Err = err
return
}
bot, err := c.App.UpdateBotActive(c.AppContext, botUserId, active)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(bot)
auditRec.AddEventObjectType("bot")
if err := json.NewEncoder(w).Encode(bot); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func assignBot(c *Context, w http.ResponseWriter, _ *http.Request) {
c.RequireUserId()
c.RequireBotUserId()
if c.Err != nil {
return
}
botUserId := c.Params.BotUserId
userId := c.Params.UserId
auditRec := c.MakeAuditRecord("assignBot", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "id", botUserId)
audit.AddEventParameter(auditRec, "user_id", userId)
if err := c.App.SessionHasPermissionToManageBot(*c.AppContext.Session(), botUserId); err != nil {
c.Err = err
return
}
if user, err := c.App.GetUser(userId); err == nil {
if user.IsBot {
c.SetPermissionError(model.PermissionAssignBot)
return
}
}
bot, err := c.App.UpdateBotOwner(botUserId, userId)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(bot)
auditRec.AddEventObjectType("bot")
if err := json.NewEncoder(w).Encode(bot); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func convertBotToUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireBotUserId()
if c.Err != nil {
return
}
bot, err := c.App.GetBot(c.Params.BotUserId, false)
if err != nil {
c.Err = err
return
}
var userPatch model.UserPatch
jsonErr := json.NewDecoder(r.Body).Decode(&userPatch)
if jsonErr != nil || userPatch.Password == nil || *userPatch.Password == "" {
c.SetInvalidParamWithErr("userPatch", jsonErr)
return
}
systemAdmin, _ := strconv.ParseBool(r.URL.Query().Get("set_system_admin"))
auditRec := c.MakeAuditRecord("convertBotToUser", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "bot", bot)
audit.AddEventParameterAuditable(auditRec, "user_patch", &userPatch)
audit.AddEventParameter(auditRec, "set_system_admin", systemAdmin)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
user, err := c.App.ConvertBotToUser(c.AppContext, bot, &userPatch, systemAdmin)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(user)
auditRec.AddEventObjectType("user")
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitBotLocal() {
api.BaseRoutes.Bot.Handle("", api.APILocal(getBot)).Methods("GET")
api.BaseRoutes.Bot.Handle("", api.APILocal(patchBot)).Methods("PUT")
api.BaseRoutes.Bot.Handle("/disable", api.APILocal(disableBot)).Methods("POST")
api.BaseRoutes.Bot.Handle("/enable", api.APILocal(enableBot)).Methods("POST")
api.BaseRoutes.Bot.Handle("/convert_to_user", api.APILocal(convertBotToUser)).Methods("POST")
api.BaseRoutes.Bot.Handle("/assign/{user_id:[A-Za-z0-9]+}", api.APILocal(assignBot)).Methods("POST")
api.BaseRoutes.Bots.Handle("", api.APILocal(getBots)).Methods("GET")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"io"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
)
func (api *API) InitBrand() {
api.BaseRoutes.Brand.Handle("/image", api.APIHandlerTrustRequester(getBrandImage)).Methods("GET")
api.BaseRoutes.Brand.Handle("/image", api.APISessionRequired(uploadBrandImage)).Methods("POST")
api.BaseRoutes.Brand.Handle("/image", api.APISessionRequired(deleteBrandImage)).Methods("DELETE")
}
func getBrandImage(c *Context, w http.ResponseWriter, r *http.Request) {
// No permission check required
img, err := c.App.GetBrandImage()
if err != nil {
w.WriteHeader(http.StatusNotFound)
w.Write(nil)
return
}
w.Header().Set("Content-Type", "image/png")
w.Write(img)
}
func uploadBrandImage(c *Context, w http.ResponseWriter, r *http.Request) {
defer io.Copy(io.Discard, r.Body)
if r.ContentLength > *c.App.Config().FileSettings.MaxFileSize {
c.Err = model.NewAppError("uploadBrandImage", "api.admin.upload_brand_image.too_large.app_error", nil, "", http.StatusRequestEntityTooLarge)
return
}
if err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize); err != nil {
c.Err = model.NewAppError("uploadBrandImage", "api.admin.upload_brand_image.parse.app_error", nil, "", http.StatusBadRequest)
return
}
m := r.MultipartForm
imageArray, ok := m.File["image"]
if !ok {
c.Err = model.NewAppError("uploadBrandImage", "api.admin.upload_brand_image.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(imageArray) <= 0 {
c.Err = model.NewAppError("uploadBrandImage", "api.admin.upload_brand_image.array.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("uploadBrandImage", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionEditBrand) {
c.SetPermissionError(model.PermissionEditBrand)
return
}
if err := c.App.SaveBrandImage(imageArray[0]); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
w.WriteHeader(http.StatusCreated)
ReturnStatusOK(w)
}
func deleteBrandImage(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("deleteBrandImage", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionEditBrand) {
c.SetPermissionError(model.PermissionEditBrand)
return
}
if err := c.App.DeleteBrandImage(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"encoding/json"
"net/http"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitChannel() {
api.BaseRoutes.Channels.Handle("", api.APISessionRequired(getAllChannels)).Methods("GET")
api.BaseRoutes.Channels.Handle("", api.APISessionRequired(createChannel)).Methods("POST")
api.BaseRoutes.Channels.Handle("/direct", api.APISessionRequired(createDirectChannel)).Methods("POST")
api.BaseRoutes.Channels.Handle("/search", api.APISessionRequiredDisableWhenBusy(searchAllChannels)).Methods("POST")
api.BaseRoutes.Channels.Handle("/group/search", api.APISessionRequiredDisableWhenBusy(searchGroupChannels)).Methods("POST")
api.BaseRoutes.Channels.Handle("/group", api.APISessionRequired(createGroupChannel)).Methods("POST")
api.BaseRoutes.Channels.Handle("/members/{user_id:[A-Za-z0-9]+}/view", api.APISessionRequired(viewChannel)).Methods("POST")
api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/scheme", api.APISessionRequired(updateChannelScheme)).Methods("PUT")
api.BaseRoutes.ChannelsForTeam.Handle("", api.APISessionRequired(getPublicChannelsForTeam)).Methods("GET")
api.BaseRoutes.ChannelsForTeam.Handle("/deleted", api.APISessionRequired(getDeletedChannelsForTeam)).Methods("GET")
api.BaseRoutes.ChannelsForTeam.Handle("/private", api.APISessionRequired(getPrivateChannelsForTeam)).Methods("GET")
api.BaseRoutes.ChannelsForTeam.Handle("/ids", api.APISessionRequired(getPublicChannelsByIdsForTeam)).Methods("POST")
api.BaseRoutes.ChannelsForTeam.Handle("/search", api.APISessionRequiredDisableWhenBusy(searchChannelsForTeam)).Methods("POST")
api.BaseRoutes.ChannelsForTeam.Handle("/search_archived", api.APISessionRequiredDisableWhenBusy(searchArchivedChannelsForTeam)).Methods("POST")
api.BaseRoutes.ChannelsForTeam.Handle("/autocomplete", api.APISessionRequired(autocompleteChannelsForTeam)).Methods("GET")
api.BaseRoutes.ChannelsForTeam.Handle("/search_autocomplete", api.APISessionRequired(autocompleteChannelsForTeamForSearch)).Methods("GET")
api.BaseRoutes.User.Handle("/teams/{team_id:[A-Za-z0-9]+}/channels", api.APISessionRequired(getChannelsForTeamForUser)).Methods("GET")
api.BaseRoutes.User.Handle("/channels", api.APISessionRequired(getChannelsForUser)).Methods("GET")
api.BaseRoutes.ChannelCategories.Handle("", api.APISessionRequired(getCategoriesForTeamForUser)).Methods("GET")
api.BaseRoutes.ChannelCategories.Handle("", api.APISessionRequired(createCategoryForTeamForUser)).Methods("POST")
api.BaseRoutes.ChannelCategories.Handle("", api.APISessionRequired(updateCategoriesForTeamForUser)).Methods("PUT")
api.BaseRoutes.ChannelCategories.Handle("/order", api.APISessionRequired(getCategoryOrderForTeamForUser)).Methods("GET")
api.BaseRoutes.ChannelCategories.Handle("/order", api.APISessionRequired(updateCategoryOrderForTeamForUser)).Methods("PUT")
api.BaseRoutes.ChannelCategories.Handle("/{category_id:[A-Za-z0-9_-]+}", api.APISessionRequired(getCategoryForTeamForUser)).Methods("GET")
api.BaseRoutes.ChannelCategories.Handle("/{category_id:[A-Za-z0-9_-]+}", api.APISessionRequired(updateCategoryForTeamForUser)).Methods("PUT")
api.BaseRoutes.ChannelCategories.Handle("/{category_id:[A-Za-z0-9_-]+}", api.APISessionRequired(deleteCategoryForTeamForUser)).Methods("DELETE")
api.BaseRoutes.Channel.Handle("", api.APISessionRequired(getChannel)).Methods("GET")
api.BaseRoutes.Channel.Handle("", api.APISessionRequired(updateChannel)).Methods("PUT")
api.BaseRoutes.Channel.Handle("/patch", api.APISessionRequired(patchChannel)).Methods("PUT")
api.BaseRoutes.Channel.Handle("/privacy", api.APISessionRequired(updateChannelPrivacy)).Methods("PUT")
api.BaseRoutes.Channel.Handle("/restore", api.APISessionRequired(restoreChannel)).Methods("POST")
api.BaseRoutes.Channel.Handle("", api.APISessionRequired(deleteChannel)).Methods("DELETE")
api.BaseRoutes.Channel.Handle("/stats", api.APISessionRequired(getChannelStats)).Methods("GET")
api.BaseRoutes.Channel.Handle("/pinned", api.APISessionRequired(getPinnedPosts)).Methods("GET")
api.BaseRoutes.Channel.Handle("/timezones", api.APISessionRequired(getChannelMembersTimezones)).Methods("GET")
api.BaseRoutes.Channel.Handle("/members_minus_group_members", api.APISessionRequired(channelMembersMinusGroupMembers)).Methods("GET")
api.BaseRoutes.Channel.Handle("/move", api.APISessionRequired(moveChannel)).Methods("POST")
api.BaseRoutes.Channel.Handle("/member_counts_by_group", api.APISessionRequired(channelMemberCountsByGroup)).Methods("GET")
api.BaseRoutes.ChannelForUser.Handle("/unread", api.APISessionRequired(getChannelUnread)).Methods("GET")
api.BaseRoutes.ChannelByName.Handle("", api.APISessionRequired(getChannelByName)).Methods("GET")
api.BaseRoutes.ChannelByNameForTeamName.Handle("", api.APISessionRequired(getChannelByNameForTeamName)).Methods("GET")
api.BaseRoutes.ChannelMembers.Handle("", api.APISessionRequired(getChannelMembers)).Methods("GET")
api.BaseRoutes.ChannelMembers.Handle("/ids", api.APISessionRequired(getChannelMembersByIds)).Methods("POST")
api.BaseRoutes.ChannelMembers.Handle("", api.APISessionRequired(addChannelMember)).Methods("POST")
api.BaseRoutes.ChannelMembersForUser.Handle("", api.APISessionRequired(getChannelMembersForTeamForUser)).Methods("GET")
api.BaseRoutes.ChannelMember.Handle("", api.APISessionRequired(getChannelMember)).Methods("GET")
api.BaseRoutes.ChannelMember.Handle("", api.APISessionRequired(removeChannelMember)).Methods("DELETE")
api.BaseRoutes.ChannelMember.Handle("/roles", api.APISessionRequired(updateChannelMemberRoles)).Methods("PUT")
api.BaseRoutes.ChannelMember.Handle("/schemeRoles", api.APISessionRequired(updateChannelMemberSchemeRoles)).Methods("PUT")
api.BaseRoutes.ChannelMember.Handle("/notify_props", api.APISessionRequired(updateChannelMemberNotifyProps)).Methods("PUT")
api.BaseRoutes.ChannelModerations.Handle("", api.APISessionRequired(getChannelModerations)).Methods("GET")
api.BaseRoutes.ChannelModerations.Handle("/patch", api.APISessionRequired(patchChannelModerations)).Methods("PUT")
}
func createChannel(c *Context, w http.ResponseWriter, r *http.Request) {
var channel *model.Channel
err := json.NewDecoder(r.Body).Decode(&channel)
if err != nil {
c.SetInvalidParamWithErr("channel", err)
return
}
auditRec := c.MakeAuditRecord("createChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "channel", channel)
if channel.Type == model.ChannelTypeOpen && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionCreatePublicChannel) {
c.SetPermissionError(model.PermissionCreatePublicChannel)
return
}
if channel.Type == model.ChannelTypePrivate && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionCreatePrivateChannel) {
c.SetPermissionError(model.PermissionCreatePrivateChannel)
return
}
sc, appErr := c.App.CreateChannelWithUser(c.AppContext, channel, c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventResultState(sc)
auditRec.AddEventObjectType("channel")
c.LogAudit("name=" + channel.Name)
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(sc); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
var channel *model.Channel
err := json.NewDecoder(r.Body).Decode(&channel)
if err != nil {
c.SetInvalidParamWithErr("channel", err)
return
}
// The channel being updated in the payload must be the same one as indicated in the URL.
if channel.Id != c.Params.ChannelId {
c.SetInvalidParam("channel_id")
return
}
auditRec := c.MakeAuditRecord("updateChannel", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "channel", channel)
defer c.LogAuditRec(auditRec)
originalOldChannel, appErr := c.App.GetChannel(c.AppContext, channel.Id)
if appErr != nil {
c.Err = appErr
return
}
oldChannel := originalOldChannel.DeepCopy()
auditRec.AddEventPriorState(oldChannel)
switch oldChannel.Type {
case model.ChannelTypeOpen:
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePublicChannelProperties) {
c.SetPermissionError(model.PermissionManagePublicChannelProperties)
return
}
case model.ChannelTypePrivate:
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePrivateChannelProperties) {
c.SetPermissionError(model.PermissionManagePrivateChannelProperties)
return
}
case model.ChannelTypeGroup, model.ChannelTypeDirect:
// Modifying the header is not linked to any specific permission for group/dm channels, so just check for membership.
if _, errGet := c.App.GetChannelMember(c.AppContext, channel.Id, c.AppContext.Session().UserId); errGet != nil {
c.Err = model.NewAppError("updateChannel", "api.channel.patch_update_channel.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
default:
c.Err = model.NewAppError("updateChannel", "api.channel.patch_update_channel.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
if oldChannel.DeleteAt > 0 {
c.Err = model.NewAppError("updateChannel", "api.channel.update_channel.deleted.app_error", nil, "", http.StatusBadRequest)
return
}
if channel.Type != "" && channel.Type != oldChannel.Type {
c.Err = model.NewAppError("updateChannel", "api.channel.update_channel.typechange.app_error", nil, "", http.StatusBadRequest)
return
}
if oldChannel.Name == model.DefaultChannelName {
if channel.Name != "" && channel.Name != oldChannel.Name {
c.Err = model.NewAppError("updateChannel", "api.channel.update_channel.tried.app_error", map[string]any{"Channel": model.DefaultChannelName}, "", http.StatusBadRequest)
return
}
}
oldChannel.Header = channel.Header
oldChannel.Purpose = channel.Purpose
oldChannelDisplayName := oldChannel.DisplayName
if channel.DisplayName != "" {
oldChannel.DisplayName = channel.DisplayName
}
if channel.Name != "" {
oldChannel.Name = channel.Name
audit.AddEventParameter(auditRec, "new_channel_name", oldChannel.Name)
}
if channel.GroupConstrained != nil {
oldChannel.GroupConstrained = channel.GroupConstrained
}
updatedChannel, appErr := c.App.UpdateChannel(c.AppContext, oldChannel)
if appErr != nil {
c.Err = appErr
return
}
if oldChannelDisplayName != channel.DisplayName {
if err := c.App.PostUpdateChannelDisplayNameMessage(c.AppContext, c.AppContext.Session().UserId, channel, oldChannelDisplayName, channel.DisplayName); err != nil {
c.Logger.Warn("Error while posting channel display name message", mlog.Err(err))
}
}
auditRec.AddEventResultState(updatedChannel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
c.LogAudit("name=" + channel.Name)
if err := json.NewEncoder(w).Encode(oldChannel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateChannelPrivacy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("updateChannelPrivacy", audit.Fail)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
defer c.LogAuditRec(auditRec)
props := model.StringInterfaceFromJSON(r.Body)
privacy, ok := props["privacy"].(string)
if !ok || (model.ChannelType(privacy) != model.ChannelTypeOpen && model.ChannelType(privacy) != model.ChannelTypePrivate) {
c.SetInvalidParam("privacy")
return
}
audit.AddEventParameter(auditRec, "privacy", privacy)
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(channel)
if model.ChannelType(privacy) == model.ChannelTypeOpen && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionConvertPrivateChannelToPublic) {
c.SetPermissionError(model.PermissionConvertPrivateChannelToPublic)
return
}
if model.ChannelType(privacy) == model.ChannelTypePrivate && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionConvertPublicChannelToPrivate) {
c.SetPermissionError(model.PermissionConvertPublicChannelToPrivate)
return
}
if channel.Name == model.DefaultChannelName && model.ChannelType(privacy) == model.ChannelTypePrivate {
c.Err = model.NewAppError("updateChannelPrivacy", "api.channel.update_channel_privacy.default_channel_error", nil, "", http.StatusBadRequest)
return
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
channel.Type = model.ChannelType(privacy)
updatedChannel, err := c.App.UpdateChannelPrivacy(c.AppContext, channel, user)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(updatedChannel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
c.LogAudit("name=" + updatedChannel.Name)
if err := json.NewEncoder(w).Encode(updatedChannel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func patchChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
var patch *model.ChannelPatch
err := json.NewDecoder(r.Body).Decode(&patch)
if err != nil {
c.SetInvalidParamWithErr("channel", err)
return
}
originalOldChannel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if appErr != nil {
c.Err = appErr
return
}
oldChannel := originalOldChannel.DeepCopy()
auditRec := c.MakeAuditRecord("patchChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "channel", patch)
auditRec.AddEventPriorState(oldChannel)
switch oldChannel.Type {
case model.ChannelTypeOpen:
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePublicChannelProperties) {
c.SetPermissionError(model.PermissionManagePublicChannelProperties)
return
}
case model.ChannelTypePrivate:
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePrivateChannelProperties) {
c.SetPermissionError(model.PermissionManagePrivateChannelProperties)
return
}
case model.ChannelTypeGroup, model.ChannelTypeDirect:
// Modifying the header is not linked to any specific permission for group/dm channels, so just check for membership.
if _, appErr = c.App.GetChannelMember(c.AppContext, c.Params.ChannelId, c.AppContext.Session().UserId); appErr != nil {
c.Err = model.NewAppError("patchChannel", "api.channel.patch_update_channel.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
default:
c.Err = model.NewAppError("patchChannel", "api.channel.patch_update_channel.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
if oldChannel.Name == model.DefaultChannelName {
if patch.Name != nil && *patch.Name != oldChannel.Name {
c.Err = model.NewAppError("patchChannel", "api.channel.update_channel.tried.app_error", map[string]any{"Channel": model.DefaultChannelName}, "", http.StatusBadRequest)
return
}
}
rchannel, appErr := c.App.PatchChannel(c.AppContext, oldChannel, patch, c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
appErr = c.App.FillInChannelProps(c.AppContext, rchannel)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(rchannel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
c.LogAudit("")
if err := json.NewEncoder(w).Encode(rchannel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func restoreChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
teamId := channel.TeamId
auditRec := c.MakeAuditRecord("restoreChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
auditRec.AddEventPriorState(channel)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageTeam) {
c.SetPermissionError(model.PermissionManageTeam)
return
}
channel, err = c.App.RestoreChannel(c.AppContext, channel, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(channel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
c.LogAudit("name=" + channel.Name)
if err := json.NewEncoder(w).Encode(channel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) {
userIds := model.ArrayFromJSON(r.Body)
allowed := false
if len(userIds) != 2 {
c.SetInvalidParam("user_ids")
return
}
for _, id := range userIds {
if !model.IsValidId(id) {
c.SetInvalidParam("user_id")
return
}
if id == c.AppContext.Session().UserId {
allowed = true
}
}
auditRec := c.MakeAuditRecord("createDirectChannel", audit.Fail)
audit.AddEventParameter(auditRec, "user_ids", userIds)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateDirectChannel) {
c.SetPermissionError(model.PermissionCreateDirectChannel)
return
}
if !allowed && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
otherUserId := userIds[0]
if c.AppContext.Session().UserId == otherUserId {
otherUserId = userIds[1]
}
audit.AddEventParameter(auditRec, "user_id", otherUserId)
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, otherUserId)
if err != nil {
c.Err = err
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
sc, err := c.App.GetOrCreateDirectChannel(c.AppContext, userIds[0], userIds[1])
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(sc)
auditRec.AddEventObjectType("channel")
auditRec.Success()
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(sc); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func searchGroupChannels(c *Context, w http.ResponseWriter, r *http.Request) {
var props *model.ChannelSearch
err := json.NewDecoder(r.Body).Decode(&props)
if err != nil {
c.SetInvalidParamWithErr("channel_search", err)
return
}
groupChannels, appErr := c.App.SearchGroupChannels(c.AppContext, c.AppContext.Session().UserId, props.Term)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(groupChannels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func createGroupChannel(c *Context, w http.ResponseWriter, r *http.Request) {
userIds := model.ArrayFromJSON(r.Body)
if len(userIds) == 0 {
c.SetInvalidParam("user_ids")
return
}
found := false
for _, id := range userIds {
if !model.IsValidId(id) {
c.SetInvalidParam("user_id")
return
}
if id == c.AppContext.Session().UserId {
found = true
}
}
if !found {
userIds = append(userIds, c.AppContext.Session().UserId)
}
auditRec := c.MakeAuditRecord("createGroupChannel", audit.Fail)
audit.AddEventParameter(auditRec, "user_ids", userIds)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateGroupChannel) {
c.SetPermissionError(model.PermissionCreateGroupChannel)
return
}
canSeeAll := true
for _, id := range userIds {
if c.AppContext.Session().UserId != id {
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, id)
if err != nil {
c.Err = err
return
}
if !canSee {
canSeeAll = false
}
}
}
if !canSeeAll {
c.SetPermissionError(model.PermissionViewMembers)
return
}
groupChannel, err := c.App.CreateGroupChannel(c.AppContext, userIds, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(groupChannel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(groupChannel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
if channel.Type == model.ChannelTypeOpen {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionReadPublicChannel) && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadPublicChannel)
return
}
} else {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
}
err = c.App.FillInChannelProps(c.AppContext, channel)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(channel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannelUnread(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId().RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
channelUnread, err := c.App.GetChannelUnread(c.AppContext, c.Params.ChannelId, c.Params.UserId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(channelUnread); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannelStats(c *Context, w http.ResponseWriter, r *http.Request) {
excludeFilesCount := r.URL.Query().Get("exclude_files_count")
excludeFilesCountBool, _ := strconv.ParseBool(excludeFilesCount)
c.RequireChannelId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
memberCount, err := c.App.GetChannelMemberCount(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
guestCount, err := c.App.GetChannelGuestCount(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
pinnedPostCount, err := c.App.GetChannelPinnedPostCount(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
filesCount := int64(-1)
if !excludeFilesCountBool {
filesCount, err = c.App.GetChannelFileCount(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
}
stats := model.ChannelStats{
ChannelId: c.Params.ChannelId,
MemberCount: memberCount,
GuestCount: guestCount,
PinnedPostCount: pinnedPostCount,
FilesCount: filesCount,
}
if err := json.NewEncoder(w).Encode(stats); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPinnedPosts(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
posts, err := c.App.GetPinnedPosts(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
if c.HandleEtag(posts.Etag(), "Get Pinned Posts", w, r) {
return
}
clientPostList := c.App.PreparePostListForClient(c.AppContext, posts)
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
w.Header().Set(model.HeaderEtagServer, clientPostList.Etag())
if err := clientPostList.EncodeJSON(w); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getAllChannels(c *Context, w http.ResponseWriter, r *http.Request) {
permissions := []*model.Permission{
model.PermissionSysconsoleReadUserManagementGroups,
model.PermissionSysconsoleReadUserManagementChannels,
}
if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), permissions) {
c.SetPermissionError(permissions...)
return
}
// Only system managers may use the ExcludePolicyConstrained parameter
if c.Params.ExcludePolicyConstrained && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
opts := model.ChannelSearchOpts{
NotAssociatedToGroup: c.Params.NotAssociatedToGroup,
ExcludeDefaultChannels: c.Params.ExcludeDefaultChannels,
IncludeDeleted: c.Params.IncludeDeleted,
ExcludePolicyConstrained: c.Params.ExcludePolicyConstrained,
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
opts.IncludePolicyID = true
}
channels, err := c.App.GetAllChannels(c.AppContext, c.Params.Page, c.Params.PerPage, opts)
if err != nil {
c.Err = err
return
}
if c.Params.IncludeTotalCount {
totalCount, err := c.App.GetAllChannelsCount(c.AppContext, opts)
if err != nil {
c.Err = err
return
}
cwc := &model.ChannelsWithCount{
Channels: channels,
TotalCount: totalCount,
}
if err := json.NewEncoder(w).Encode(cwc); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
return
}
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPublicChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionListTeamChannels) {
c.SetPermissionError(model.PermissionListTeamChannels)
return
}
channels, err := c.App.GetPublicChannelsForTeam(c.AppContext, c.Params.TeamId, c.Params.Page*c.Params.PerPage, c.Params.PerPage)
if err != nil {
c.Err = err
return
}
err = c.App.FillInChannelsProps(c.AppContext, channels)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getDeletedChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
channels, err := c.App.GetDeletedChannels(c.AppContext, c.Params.TeamId, c.Params.Page*c.Params.PerPage, c.Params.PerPage, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
err = c.App.FillInChannelsProps(c.AppContext, channels)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPrivateChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
channels, err := c.App.GetPrivateChannelsForTeam(c.AppContext, c.Params.TeamId, c.Params.Page*c.Params.PerPage, c.Params.PerPage)
if err != nil {
c.Err = err
return
}
err = c.App.FillInChannelsProps(c.AppContext, channels)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPublicChannelsByIdsForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
channelIds := model.ArrayFromJSON(r.Body)
if len(channelIds) == 0 {
c.SetInvalidParam("channel_ids")
return
}
for _, cid := range channelIds {
if !model.IsValidId(cid) {
c.SetInvalidParam("channel_id")
return
}
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
channels, err := c.App.GetPublicChannelsByIdsForTeam(c.AppContext, c.Params.TeamId, channelIds)
if err != nil {
c.Err = err
return
}
err = c.App.FillInChannelsProps(c.AppContext, channels)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannelsForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
query := r.URL.Query()
lastDeleteAt, nErr := strconv.Atoi(query.Get("last_delete_at"))
if nErr != nil {
lastDeleteAt = 0
}
if lastDeleteAt < 0 {
c.SetInvalidURLParam("last_delete_at")
return
}
channels, err := c.App.GetChannelsForTeamForUser(c.AppContext, c.Params.TeamId, c.Params.UserId, &model.ChannelSearchOpts{
IncludeDeleted: c.Params.IncludeDeleted,
LastDeleteAt: lastDeleteAt,
})
if err != nil {
c.Err = err
return
}
if c.HandleEtag(channels.Etag(), "Get Channels", w, r) {
return
}
err = c.App.FillInChannelsProps(c.AppContext, channels)
if err != nil {
c.Err = err
return
}
w.Header().Set(model.HeaderEtagServer, channels.Etag())
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannelsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
query := r.URL.Query()
lastDeleteAt, nErr := strconv.Atoi(query.Get("last_delete_at"))
if nErr != nil {
lastDeleteAt = 0
}
if lastDeleteAt < 0 {
c.SetInvalidURLParam("last_delete_at")
return
}
pageSize := 100
fromChannelID := ""
// We have to write `[` and `]` separately because we want to stream the response.
// The internal API is paginated, but the client always needs to get the full data.
// Therefore, to avoid forcing the client to go through all the pages,
// we stream the full data from server side itself.
//
// Note that this means if an error occurs in mid-stream, the response won't be
// fully JSON.
w.Write([]byte(`[`))
enc := json.NewEncoder(w)
for {
channels, err := c.App.GetChannelsForUser(c.AppContext, c.Params.UserId, c.Params.IncludeDeleted, lastDeleteAt, pageSize, fromChannelID)
if err != nil {
// If the page size was a perfect multiple of the total number of results,
// then the last query will always return zero results.
if fromChannelID != "" && err.Id == "app.channel.get_channels.not_found.app_error" {
break
}
c.Err = err
return
}
err = c.App.FillInChannelsProps(c.AppContext, channels)
if err != nil {
c.Err = err
return
}
// intermediary comma between sets
if fromChannelID != "" {
w.Write([]byte(`,`))
}
for i, ch := range channels {
if err := enc.Encode(ch); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
if i < len(channels)-1 {
w.Write([]byte(`,`))
}
}
if len(channels) < pageSize {
break
}
fromChannelID = channels[len(channels)-1].Id
}
w.Write([]byte(`]`))
}
func autocompleteChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionListTeamChannels) {
c.SetPermissionError(model.PermissionListTeamChannels)
return
}
name := r.URL.Query().Get("name")
channels, err := c.App.AutocompleteChannelsForTeam(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, name)
if err != nil {
c.Err = err
return
}
// Don't fill in channels props, since unused by client and potentially expensive.
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func autocompleteChannelsForTeamForSearch(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
name := r.URL.Query().Get("name")
channels, err := c.App.AutocompleteChannelsForSearch(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, name)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func searchChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
var props *model.ChannelSearch
err := json.NewDecoder(r.Body).Decode(&props)
if err != nil {
c.SetInvalidParamWithErr("channel_search", err)
return
}
var channels model.ChannelList
var appErr *model.AppError
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionListTeamChannels) {
channels, appErr = c.App.SearchChannels(c.AppContext, c.Params.TeamId, props.Term)
} else {
// If the user is not a team member, return a 404
if _, appErr = c.App.GetTeamMember(c.Params.TeamId, c.AppContext.Session().UserId); appErr != nil {
c.Err = appErr
return
}
channels, appErr = c.App.SearchChannelsForUser(c.AppContext, c.AppContext.Session().UserId, c.Params.TeamId, props.Term)
}
if appErr != nil {
c.Err = appErr
return
}
// Don't fill in channels props, since unused by client and potentially expensive.
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func searchArchivedChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
var props *model.ChannelSearch
err := json.NewDecoder(r.Body).Decode(&props)
if err != nil {
c.SetInvalidParamWithErr("channel_search", err)
return
}
var channels model.ChannelList
var appErr *model.AppError
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionListTeamChannels) {
channels, appErr = c.App.SearchArchivedChannels(c.AppContext, c.Params.TeamId, props.Term, c.AppContext.Session().UserId)
} else {
// If the user is not a team member, return a 404
if _, appErr = c.App.GetTeamMember(c.Params.TeamId, c.AppContext.Session().UserId); appErr != nil {
c.Err = appErr
return
}
channels, appErr = c.App.SearchArchivedChannels(c.AppContext, c.Params.TeamId, props.Term, c.AppContext.Session().UserId)
}
if appErr != nil {
c.Err = appErr
return
}
// Don't fill in channels props, since unused by client and potentially expensive.
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func searchAllChannels(c *Context, w http.ResponseWriter, r *http.Request) {
var props *model.ChannelSearch
err := json.NewDecoder(r.Body).Decode(&props)
if err != nil {
c.SetInvalidParamWithErr("channel_search", err)
return
}
fromSysConsole := true
if val := r.URL.Query().Get("system_console"); val != "" {
fromSysConsole, err = strconv.ParseBool(val)
if err != nil {
c.SetInvalidParam("system_console")
return
}
}
if !fromSysConsole {
// If the request is not coming from system_console, only show the user level channels
// from all teams.
channels, err := c.App.AutocompleteChannels(c.AppContext, c.AppContext.Session().UserId, props.Term)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
return
}
// Only system managers may use the ExcludePolicyConstrained field
if props.ExcludePolicyConstrained && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementChannels) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementChannels)
return
}
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
includeDeleted = includeDeleted || props.IncludeDeleted
opts := model.ChannelSearchOpts{
NotAssociatedToGroup: props.NotAssociatedToGroup,
ExcludeDefaultChannels: props.ExcludeDefaultChannels,
TeamIds: props.TeamIds,
GroupConstrained: props.GroupConstrained,
ExcludeGroupConstrained: props.ExcludeGroupConstrained,
ExcludePolicyConstrained: props.ExcludePolicyConstrained,
IncludeSearchById: props.IncludeSearchById,
Public: props.Public,
Private: props.Private,
IncludeDeleted: includeDeleted,
Deleted: props.Deleted,
Page: props.Page,
PerPage: props.PerPage,
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
opts.IncludePolicyID = true
}
channels, totalCount, appErr := c.App.SearchAllChannels(c.AppContext, props.Term, opts)
if appErr != nil {
c.Err = appErr
return
}
// Don't fill in channels props, since unused by client and potentially expensive.
if props.Page != nil && props.PerPage != nil {
data := model.ChannelsWithCount{Channels: channels, TotalCount: totalCount}
if err := json.NewEncoder(w).Encode(data); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
return
}
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("deleteChannel", audit.Fail)
audit.AddEventParameter(auditRec, "id", c.Params.ChannelId)
auditRec.AddEventPriorState(channel)
defer c.LogAuditRec(auditRec)
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
c.Err = model.NewAppError("deleteChannel", "api.channel.delete_channel.type.invalid", nil, "", http.StatusBadRequest)
return
}
if channel.Type == model.ChannelTypeOpen && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionDeletePublicChannel) {
c.SetPermissionError(model.PermissionDeletePublicChannel)
return
}
if channel.Type == model.ChannelTypePrivate && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionDeletePrivateChannel) {
c.SetPermissionError(model.PermissionDeletePrivateChannel)
return
}
if c.Params.Permanent {
if *c.App.Config().ServiceSettings.EnableAPIChannelDeletion {
err = c.App.PermanentDeleteChannel(c.AppContext, channel)
} else {
err = model.NewAppError("deleteChannel", "api.user.delete_channel.not_enabled.app_error", nil, "channelId="+c.Params.ChannelId, http.StatusUnauthorized)
}
} else {
err = c.App.DeleteChannel(c.AppContext, channel, c.AppContext.Session().UserId)
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("name=" + channel.Name)
ReturnStatusOK(w)
}
func getChannelByName(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId().RequireChannelName()
if c.Err != nil {
return
}
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
channel, appErr := c.App.GetChannelByName(c.AppContext, c.Params.ChannelName, c.Params.TeamId, includeDeleted)
if appErr != nil {
c.Err = appErr
return
}
if channel.Type == model.ChannelTypeOpen {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionReadPublicChannel) && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadPublicChannel)
return
}
} else {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionReadChannel) {
c.Err = model.NewAppError("getChannelByName", "app.channel.get_by_name.missing.app_error", nil, "teamId="+channel.TeamId+", "+"name="+channel.Name+"", http.StatusNotFound)
return
}
}
appErr = c.App.FillInChannelProps(c.AppContext, channel)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(channel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannelByNameForTeamName(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamName().RequireChannelName()
if c.Err != nil {
return
}
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
channel, appErr := c.App.GetChannelByNameForTeamName(c.AppContext, c.Params.ChannelName, c.Params.TeamName, includeDeleted)
if appErr != nil {
c.Err = appErr
return
}
teamOk := c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionReadPublicChannel)
channelOk := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionReadChannel)
if channel.Type == model.ChannelTypeOpen {
if !teamOk && !channelOk {
c.SetPermissionError(model.PermissionReadPublicChannel)
return
}
} else if !channelOk {
c.Err = model.NewAppError("getChannelByNameForTeamName", "app.channel.get_by_name.missing.app_error", nil, "teamId="+channel.TeamId+", "+"name="+channel.Name+"", http.StatusNotFound)
return
}
appErr = c.App.FillInChannelProps(c.AppContext, channel)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(channel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannelMembers(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
members, err := c.App.GetChannelMembersPage(c.AppContext, c.Params.ChannelId, c.Params.Page, c.Params.PerPage)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(members); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannelMembersTimezones(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
membersTimezones, err := c.App.GetChannelMembersTimezones(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
w.Write([]byte(model.ArrayToJSON(membersTimezones)))
}
func getChannelMembersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
userIds := model.ArrayFromJSON(r.Body)
if len(userIds) == 0 {
c.SetInvalidParam("user_ids")
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
members, err := c.App.GetChannelMembersByIds(c.AppContext, c.Params.ChannelId, userIds)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(members); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId().RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
ctx := c.AppContext
ctx.SetContext(app.WithMaster(ctx.Context()))
member, err := c.App.GetChannelMember(ctx, c.Params.ChannelId, c.Params.UserId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(member); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannelMembersForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
if c.AppContext.Session().UserId != c.Params.UserId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
members, err := c.App.GetChannelMembersForUser(c.AppContext, c.Params.TeamId, c.Params.UserId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(members); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func viewChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
var view model.ChannelView
if jsonErr := json.NewDecoder(r.Body).Decode(&view); jsonErr != nil {
c.SetInvalidParamWithErr("channel_view", jsonErr)
return
}
// Validate view struct
// Check IDs are valid or blank. Blank IDs are used to denote focus loss or initial channel view.
if view.ChannelId != "" && !model.IsValidId(view.ChannelId) {
c.SetInvalidParam("channel_view.channel_id")
return
}
if view.PrevChannelId != "" && !model.IsValidId(view.PrevChannelId) {
c.SetInvalidParam("channel_view.prev_channel_id")
return
}
times, err := c.App.ViewChannel(c.AppContext, &view, c.Params.UserId, c.AppContext.Session().Id, view.CollapsedThreadsSupported)
if err != nil {
c.Err = err
return
}
c.App.Srv().Platform().UpdateLastActivityAtIfNeeded(*c.AppContext.Session())
c.ExtendSessionExpiryIfNeeded(w, r)
// Returning {"status": "OK", ...} for backwards compatibility
resp := &model.ChannelViewResponse{
Status: "OK",
LastViewedAtTimes: times,
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateChannelMemberRoles(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId().RequireUserId()
if c.Err != nil {
return
}
props := model.MapFromJSON(r.Body)
newRoles := props["roles"]
if !(model.IsValidUserRoles(newRoles)) {
c.SetInvalidParam("roles")
return
}
auditRec := c.MakeAuditRecord("updateChannelMemberRoles", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "props", props)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManageChannelRoles) {
c.SetPermissionError(model.PermissionManageChannelRoles)
return
}
if _, err := c.App.UpdateChannelMemberRoles(c.AppContext, c.Params.ChannelId, c.Params.UserId, newRoles); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func updateChannelMemberSchemeRoles(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId().RequireUserId()
if c.Err != nil {
return
}
var schemeRoles model.SchemeRoles
if jsonErr := json.NewDecoder(r.Body).Decode(&schemeRoles); jsonErr != nil {
c.SetInvalidParamWithErr("scheme_roles", jsonErr)
return
}
auditRec := c.MakeAuditRecord("updateChannelMemberSchemeRoles", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
audit.AddEventParameterAuditable(auditRec, "roles", &schemeRoles)
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManageChannelRoles) {
c.SetPermissionError(model.PermissionManageChannelRoles)
return
}
if _, err := c.App.UpdateChannelMemberSchemeRoles(c.AppContext, c.Params.ChannelId, c.Params.UserId, schemeRoles.SchemeGuest, schemeRoles.SchemeUser, schemeRoles.SchemeAdmin); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func updateChannelMemberNotifyProps(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId().RequireUserId()
if c.Err != nil {
return
}
props := model.MapFromJSON(r.Body)
if props == nil {
c.SetInvalidParam("notify_props")
return
}
auditRec := c.MakeAuditRecord("updateChannelMemberNotifyProps", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
audit.AddEventParameter(auditRec, "props", props)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
_, err := c.App.UpdateChannelMemberNotifyProps(c.AppContext, props, c.Params.ChannelId, c.Params.UserId)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func addChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
props := model.StringInterfaceFromJSON(r.Body)
userId, ok := props["user_id"].(string)
if !ok || !model.IsValidId(userId) {
c.SetInvalidParam("user_id")
return
}
auditRec := c.MakeAuditRecord("addChannelMember", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "user_id", userId)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
member := &model.ChannelMember{
ChannelId: c.Params.ChannelId,
UserId: userId,
}
postRootId, ok := props["post_root_id"].(string)
if ok && postRootId != "" && !model.IsValidId(postRootId) {
c.SetInvalidParam("post_root_id")
return
}
audit.AddEventParameter(auditRec, "post_root_id", postRootId)
if ok && len(postRootId) == 26 {
rootPost, err := c.App.GetSinglePost(postRootId, false)
if err != nil {
c.Err = err
return
}
if rootPost.ChannelId != member.ChannelId {
c.SetInvalidParam("post_root_id")
return
}
}
channel, err := c.App.GetChannel(c.AppContext, member.ChannelId)
if err != nil {
c.Err = err
return
}
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
c.Err = model.NewAppError("addUserToChannel", "api.channel.add_user_to_channel.type.app_error", nil, "", http.StatusBadRequest)
return
}
isNewMembership := false
if _, err = c.App.GetChannelMember(c.AppContext, member.ChannelId, member.UserId); err != nil {
if err.Id == app.MissingChannelMemberError {
isNewMembership = true
} else {
c.Err = err
return
}
}
isSelfAdd := member.UserId == c.AppContext.Session().UserId
if channel.Type == model.ChannelTypeOpen {
if isSelfAdd && isNewMembership {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionJoinPublicChannels) {
c.SetPermissionError(model.PermissionJoinPublicChannels)
return
}
} else if isSelfAdd && !isNewMembership {
// nothing to do, since already in the channel
} else if !isSelfAdd {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePublicChannelMembers) {
c.SetPermissionError(model.PermissionManagePublicChannelMembers)
return
}
}
}
if channel.Type == model.ChannelTypePrivate {
if isSelfAdd && isNewMembership {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePrivateChannelMembers) {
c.SetPermissionError(model.PermissionManagePrivateChannelMembers)
return
}
} else if isSelfAdd && !isNewMembership {
// nothing to do, since already in the channel
} else if !isSelfAdd {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePrivateChannelMembers) {
c.SetPermissionError(model.PermissionManagePrivateChannelMembers)
return
}
}
}
if channel.IsGroupConstrained() {
nonMembers, err := c.App.FilterNonGroupChannelMembers([]string{member.UserId}, channel)
if err != nil {
if v, ok := err.(*model.AppError); ok {
c.Err = v
} else {
c.Err = model.NewAppError("addChannelMember", "api.channel.add_members.error", nil, err.Error(), http.StatusBadRequest)
}
return
}
if len(nonMembers) > 0 {
c.Err = model.NewAppError("addChannelMember", "api.channel.add_members.user_denied", map[string]any{"UserIDs": nonMembers}, "", http.StatusBadRequest)
return
}
}
cm, err := c.App.AddChannelMember(c.AppContext, member.UserId, channel, app.ChannelMemberOpts{
UserRequestorID: c.AppContext.Session().UserId,
PostRootID: postRootId,
})
if err != nil {
c.Err = err
return
}
if postRootId != "" {
err := c.App.UpdateThreadFollowForUserFromChannelAdd(c.AppContext, cm.UserId, channel.TeamId, postRootId)
if err != nil {
c.Err = err
return
}
}
auditRec.Success()
auditRec.AddEventResultState(cm)
auditRec.AddEventObjectType("channel_member")
auditRec.AddMeta("add_user_id", cm.UserId)
c.LogAudit("name=" + channel.Name + " user_id=" + cm.UserId)
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(cm); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func removeChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId().RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("removeChannelMember", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
if !(channel.Type == model.ChannelTypeOpen || channel.Type == model.ChannelTypePrivate) {
c.Err = model.NewAppError("removeChannelMember", "api.channel.remove_channel_member.type.app_error", nil, "", http.StatusBadRequest)
return
}
if channel.IsGroupConstrained() && (c.Params.UserId != c.AppContext.Session().UserId) && !user.IsBot {
c.Err = model.NewAppError("removeChannelMember", "api.channel.remove_member.group_constrained.app_error", nil, "", http.StatusBadRequest)
return
}
if c.Params.UserId != c.AppContext.Session().UserId {
if channel.Type == model.ChannelTypeOpen && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePublicChannelMembers) {
c.SetPermissionError(model.PermissionManagePublicChannelMembers)
return
}
if channel.Type == model.ChannelTypePrivate && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePrivateChannelMembers) {
c.SetPermissionError(model.PermissionManagePrivateChannelMembers)
return
}
}
if err = c.App.RemoveUserFromChannel(c.AppContext, c.Params.UserId, c.AppContext.Session().UserId, channel); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("name=" + channel.Name + " user_id=" + c.Params.UserId)
ReturnStatusOK(w)
}
func updateChannelScheme(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("updateChannelScheme", audit.Fail)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
defer c.LogAuditRec(auditRec)
var p model.SchemeIDPatch
if jsonErr := json.NewDecoder(r.Body).Decode(&p); jsonErr != nil || p.SchemeID == nil || !model.IsValidId(*p.SchemeID) {
c.SetInvalidParamWithErr("scheme_id", jsonErr)
return
}
schemeID := p.SchemeID
audit.AddEventParameter(auditRec, "scheme_id", *schemeID)
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("Api4.UpdateChannelScheme", "api.channel.update_channel_scheme.license.error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
scheme, err := c.App.GetScheme(*schemeID)
if err != nil {
c.Err = err
return
}
if scheme.Scope != model.SchemeScopeChannel {
c.Err = model.NewAppError("Api4.UpdateChannelScheme", "api.channel.update_channel_scheme.scheme_scope.error", nil, "", http.StatusBadRequest)
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(channel)
channel.SchemeId = &scheme.Id
updatedChannel, err := c.App.UpdateChannelScheme(c.AppContext, channel)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(updatedChannel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
ReturnStatusOK(w)
}
func channelMembersMinusGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
groupIDsParam := groupIDsQueryParamRegex.ReplaceAllString(c.Params.GroupIDs, "")
if len(groupIDsParam) < 26 {
c.SetInvalidParam("group_ids")
return
}
groupIDs := []string{}
for _, gid := range strings.Split(c.Params.GroupIDs, ",") {
if !model.IsValidId(gid) {
c.SetInvalidParam("group_ids")
return
}
groupIDs = append(groupIDs, gid)
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementChannels) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementChannels)
return
}
users, totalCount, appErr := c.App.ChannelMembersMinusGroupMembers(
c.Params.ChannelId,
groupIDs,
c.Params.Page,
c.Params.PerPage,
)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(&model.UsersWithGroupsAndCount{
Users: users,
Count: totalCount,
})
if err != nil {
c.Err = model.NewAppError("Api4.channelMembersMinusGroupMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func channelMemberCountsByGroup(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("Api4.channelMemberCountsByGroup", "api.channel.channel_member_counts_by_group.license.error", nil, "", http.StatusForbidden)
return
}
c.RequireChannelId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
includeTimezones := r.URL.Query().Get("include_timezones") == "true"
channelMemberCounts, appErr := c.App.GetMemberCountsByGroup(app.WithMaster(context.Background()), c.Params.ChannelId, includeTimezones)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(channelMemberCounts)
if err != nil {
c.Err = model.NewAppError("Api4.channelMemberCountsByGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func getChannelModerations(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("Api4.GetChannelModerations", "api.channel.get_channel_moderations.license.error", nil, "", http.StatusForbidden)
return
}
c.RequireChannelId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementChannels) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementChannels)
return
}
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if appErr != nil {
c.Err = appErr
return
}
channelModerations, appErr := c.App.GetChannelModerationsForChannel(c.AppContext, channel)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(channelModerations)
if err != nil {
c.Err = model.NewAppError("Api4.getChannelModerations", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func patchChannelModerations(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("Api4.patchChannelModerations", "api.channel.patch_channel_moderations.license.error", nil, "", http.StatusForbidden)
return
}
c.RequireChannelId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("patchChannelModerations", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementChannels) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementChannels)
return
}
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if appErr != nil {
c.Err = appErr
return
}
audit.AddEventParameterAuditable(auditRec, "channel", channel)
var channelModerationsPatch []*model.ChannelModerationPatch
err := json.NewDecoder(r.Body).Decode(&channelModerationsPatch)
if err != nil {
c.Err = model.NewAppError("Api4.patchChannelModerations", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
channelModerations, appErr := c.App.PatchChannelModerationsForChannel(c.AppContext, channel, channelModerationsPatch)
if appErr != nil {
c.Err = appErr
return
}
audit.AddEventParameterAuditableArray(auditRec, "channel_moderations_patch", channelModerationsPatch)
b, err := json.Marshal(channelModerations)
if err != nil {
c.Err = model.NewAppError("Api4.patchChannelModerations", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(b)
}
func moveChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
props := model.StringInterfaceFromJSON(r.Body)
teamId, ok := props["team_id"].(string)
if !ok {
c.SetInvalidParam("team_id")
return
}
force, ok := props["force"].(bool)
if !ok {
c.SetInvalidParam("force")
return
}
team, err := c.App.GetTeam(teamId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("moveChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
audit.AddEventParameter(auditRec, "team_id", teamId)
audit.AddEventParameter(auditRec, "force", force)
auditRec.AddEventPriorState(channel)
// TODO check and verify if the below three things are parameters or prior state if any
auditRec.AddMeta("channel_name", channel.Name)
auditRec.AddMeta("team_id", team.Id)
auditRec.AddMeta("team_name", team.Name)
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
c.Err = model.NewAppError("moveChannel", "api.channel.move_channel.type.invalid", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
err = c.App.RemoveAllDeactivatedMembersFromChannel(c.AppContext, channel)
if err != nil {
c.Err = err
return
}
if force {
err = c.App.RemoveUsersFromChannelNotMemberOfTeam(c.AppContext, user, channel, team)
if err != nil {
c.Err = err
return
}
}
err = c.App.MoveChannel(c.AppContext, team, channel, user)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(channel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
c.LogAudit("channel=" + channel.Name)
c.LogAudit("team=" + team.Name)
if err := json.NewEncoder(w).Encode(channel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func getCategoriesForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
categories, appErr := c.App.GetSidebarCategoriesForTeamForUser(c.AppContext, c.Params.UserId, c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
categoriesJSON, err := json.Marshal(categories)
if err != nil {
c.Err = model.NewAppError("getCategoriesForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(categoriesJSON)
}
func createCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
auditRec := c.MakeAuditRecord("createCategoryForTeamForUser", audit.Fail)
defer c.LogAuditRec(auditRec)
var categoryCreateRequest model.SidebarCategoryWithChannels
err := json.NewDecoder(r.Body).Decode(&categoryCreateRequest)
if err != nil || c.Params.UserId != categoryCreateRequest.UserId || c.Params.TeamId != categoryCreateRequest.TeamId {
c.SetInvalidParamWithErr("category", err)
return
}
if appErr := validateSidebarCategory(c, c.Params.TeamId, c.Params.UserId, &categoryCreateRequest); appErr != nil {
c.Err = appErr
return
}
category, appErr := c.App.CreateSidebarCategory(c.AppContext, c.Params.UserId, c.Params.TeamId, &categoryCreateRequest)
if appErr != nil {
c.Err = appErr
return
}
categoryJSON, err := json.Marshal(category)
if err != nil {
c.Err = model.NewAppError("createCategoryForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(categoryJSON)
}
func getCategoryOrderForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
order, appErr := c.App.GetSidebarCategoryOrder(c.AppContext, c.Params.UserId, c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
err := json.NewEncoder(w).Encode(order)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func updateCategoryOrderForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
auditRec := c.MakeAuditRecord("updateCategoryOrderForTeamForUser", audit.Fail)
defer c.LogAuditRec(auditRec)
categoryOrder := model.ArrayFromJSON(r.Body)
for _, categoryId := range categoryOrder {
if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, categoryId) {
c.SetInvalidParam("category")
return
}
}
err := c.App.UpdateSidebarCategoryOrder(c.AppContext, c.Params.UserId, c.Params.TeamId, categoryOrder)
if err != nil {
c.Err = err
return
}
auditRec.Success()
w.Write([]byte(model.ArrayToJSON(categoryOrder)))
}
func getCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId().RequireCategoryId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, c.Params.CategoryId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
categories, appErr := c.App.GetSidebarCategory(c.AppContext, c.Params.CategoryId)
if appErr != nil {
c.Err = appErr
return
}
categoriesJSON, err := json.Marshal(categories)
if err != nil {
c.Err = model.NewAppError("getCategoryForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(categoriesJSON)
}
func updateCategoriesForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
auditRec := c.MakeAuditRecord("updateCategoriesForTeamForUser", audit.Fail)
defer c.LogAuditRec(auditRec)
var categoriesUpdateRequest []*model.SidebarCategoryWithChannels
err := json.NewDecoder(r.Body).Decode(&categoriesUpdateRequest)
if err != nil {
c.SetInvalidParamWithErr("category", err)
return
}
for _, category := range categoriesUpdateRequest {
if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, category.Id) {
c.SetInvalidParam("category")
return
}
}
if appErr := validateSidebarCategories(c, c.Params.TeamId, c.Params.UserId, categoriesUpdateRequest); appErr != nil {
c.Err = appErr
return
}
categories, appErr := c.App.UpdateSidebarCategories(c.AppContext, c.Params.UserId, c.Params.TeamId, categoriesUpdateRequest)
if appErr != nil {
c.Err = appErr
return
}
categoriesJSON, err := json.Marshal(categories)
if err != nil {
c.Err = model.NewAppError("updateCategoriesForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(categoriesJSON)
}
func validateSidebarCategory(c *Context, teamId, userId string, category *model.SidebarCategoryWithChannels) *model.AppError {
channels, appErr := c.App.GetChannelsForTeamForUser(c.AppContext, teamId, userId, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 0,
})
if appErr != nil {
return model.NewAppError("validateSidebarCategory", "api.invalid_channel", nil, "", http.StatusBadRequest).Wrap(appErr)
}
category.Channels = validateSidebarCategoryChannels(c, userId, category.Channels, channels)
return nil
}
func validateSidebarCategories(c *Context, teamId, userId string, categories []*model.SidebarCategoryWithChannels) *model.AppError {
channels, err := c.App.GetChannelsForTeamForUser(c.AppContext, teamId, userId, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 0,
})
if err != nil {
return model.NewAppError("validateSidebarCategory", "api.invalid_channel", nil, err.Error(), http.StatusBadRequest)
}
for _, category := range categories {
category.Channels = validateSidebarCategoryChannels(c, userId, category.Channels, channels)
}
return nil
}
func validateSidebarCategoryChannels(c *Context, userId string, channelIds []string, channels model.ChannelList) []string {
var filtered []string
for _, channelId := range channelIds {
found := false
for _, channel := range channels {
if channel.Id == channelId {
found = true
break
}
}
if found {
filtered = append(filtered, channelId)
} else {
c.Logger.Info("Stopping user from adding channel to their sidebar when they are not a member", mlog.String("user_id", userId), mlog.String("channel_id", channelId))
}
}
return filtered
}
func updateCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId().RequireCategoryId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, c.Params.CategoryId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
auditRec := c.MakeAuditRecord("updateCategoryForTeamForUser", audit.Fail)
defer c.LogAuditRec(auditRec)
var categoryUpdateRequest model.SidebarCategoryWithChannels
err := json.NewDecoder(r.Body).Decode(&categoryUpdateRequest)
if err != nil || categoryUpdateRequest.TeamId != c.Params.TeamId || categoryUpdateRequest.UserId != c.Params.UserId {
c.SetInvalidParamWithErr("category", err)
return
}
if appErr := validateSidebarCategory(c, c.Params.TeamId, c.Params.UserId, &categoryUpdateRequest); appErr != nil {
c.Err = appErr
return
}
categoryUpdateRequest.Id = c.Params.CategoryId
categories, appErr := c.App.UpdateSidebarCategories(c.AppContext, c.Params.UserId, c.Params.TeamId, []*model.SidebarCategoryWithChannels{&categoryUpdateRequest})
if appErr != nil {
c.Err = appErr
return
}
categoryJSON, err := json.Marshal(categories[0])
if err != nil {
c.Err = model.NewAppError("updateCategoryForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(categoryJSON)
}
func deleteCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId().RequireCategoryId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, c.Params.CategoryId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
auditRec := c.MakeAuditRecord("deleteCategoryForTeamForUser", audit.Fail)
defer c.LogAuditRec(auditRec)
appErr := c.App.DeleteSidebarCategory(c.AppContext, c.Params.UserId, c.Params.TeamId, c.Params.CategoryId)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitChannelLocal() {
api.BaseRoutes.Channels.Handle("", api.APILocal(getAllChannels)).Methods("GET")
api.BaseRoutes.Channels.Handle("", api.APILocal(localCreateChannel)).Methods("POST")
api.BaseRoutes.Channel.Handle("", api.APILocal(getChannel)).Methods("GET")
api.BaseRoutes.ChannelByName.Handle("", api.APILocal(getChannelByName)).Methods("GET")
api.BaseRoutes.Channel.Handle("", api.APILocal(localDeleteChannel)).Methods("DELETE")
api.BaseRoutes.Channel.Handle("/patch", api.APILocal(localPatchChannel)).Methods("PUT")
api.BaseRoutes.Channel.Handle("/move", api.APILocal(localMoveChannel)).Methods("POST")
api.BaseRoutes.Channel.Handle("/privacy", api.APILocal(localUpdateChannelPrivacy)).Methods("PUT")
api.BaseRoutes.Channel.Handle("/restore", api.APILocal(localRestoreChannel)).Methods("POST")
api.BaseRoutes.ChannelMember.Handle("", api.APILocal(localRemoveChannelMember)).Methods("DELETE")
api.BaseRoutes.ChannelMember.Handle("", api.APILocal(getChannelMember)).Methods("GET")
api.BaseRoutes.ChannelMembers.Handle("", api.APILocal(localAddChannelMember)).Methods("POST")
api.BaseRoutes.ChannelMembers.Handle("", api.APILocal(getChannelMembers)).Methods("GET")
api.BaseRoutes.ChannelsForTeam.Handle("", api.APILocal(getPublicChannelsForTeam)).Methods("GET")
api.BaseRoutes.ChannelsForTeam.Handle("/deleted", api.APILocal(getDeletedChannelsForTeam)).Methods("GET")
api.BaseRoutes.ChannelsForTeam.Handle("/private", api.APILocal(getPrivateChannelsForTeam)).Methods("GET")
api.BaseRoutes.ChannelByName.Handle("", api.APILocal(getChannelByName)).Methods("GET")
api.BaseRoutes.ChannelByNameForTeamName.Handle("", api.APILocal(getChannelByNameForTeamName)).Methods("GET")
}
func localCreateChannel(c *Context, w http.ResponseWriter, r *http.Request) {
var channel *model.Channel
err := json.NewDecoder(r.Body).Decode(&channel)
if err != nil {
c.SetInvalidParamWithErr("channel", err)
return
}
auditRec := c.MakeAuditRecord("localCreateChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "channel", channel)
sc, appErr := c.App.CreateChannel(c.AppContext, channel, false)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventResultState(sc)
auditRec.AddEventObjectType("channel")
c.LogAudit("name=" + channel.Name)
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(sc); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localUpdateChannelPrivacy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
props := model.StringInterfaceFromJSON(r.Body)
privacy, ok := props["privacy"].(string)
if !ok || (model.ChannelType(privacy) != model.ChannelTypeOpen && model.ChannelType(privacy) != model.ChannelTypePrivate) {
c.SetInvalidParam("privacy")
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("localUpdateChannelPrivacy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "privacy", privacy)
if channel.Name == model.DefaultChannelName && model.ChannelType(privacy) == model.ChannelTypePrivate {
c.Err = model.NewAppError("updateChannelPrivacy", "api.channel.update_channel_privacy.default_channel_error", nil, "", http.StatusBadRequest)
return
}
channel.Type = model.ChannelType(privacy)
updatedChannel, err := c.App.UpdateChannelPrivacy(c.AppContext, channel, nil)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(channel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
c.LogAudit("name=" + updatedChannel.Name)
if err := json.NewEncoder(w).Encode(updatedChannel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localRestoreChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("localRestoreChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
channel, err = c.App.RestoreChannel(c.AppContext, channel, "")
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(channel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
c.LogAudit("name=" + channel.Name)
if err := json.NewEncoder(w).Encode(channel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localAddChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("localAddChannelMember", audit.Fail)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
defer c.LogAuditRec(auditRec)
props := model.StringInterfaceFromJSON(r.Body)
userId, ok := props["user_id"].(string)
if !ok || !model.IsValidId(userId) {
c.SetInvalidParam("user_id")
return
}
audit.AddEventParameter(auditRec, "user_id", userId)
member := &model.ChannelMember{
ChannelId: c.Params.ChannelId,
UserId: userId,
}
postRootId, ok := props["post_root_id"].(string)
if ok && postRootId != "" && !model.IsValidId(postRootId) {
c.SetInvalidParam("post_root_id")
return
}
audit.AddEventParameter(auditRec, "post_root_id", postRootId)
if ok && len(postRootId) == 26 {
rootPost, err := c.App.GetSinglePost(postRootId, false)
if err != nil {
c.Err = err
return
}
if rootPost.ChannelId != member.ChannelId {
c.SetInvalidParam("post_root_id")
return
}
}
channel, err := c.App.GetChannel(c.AppContext, member.ChannelId)
if err != nil {
c.Err = err
return
}
audit.AddEventParameterAuditable(auditRec, "channel", channel)
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
c.Err = model.NewAppError("localAddChannelMember", "api.channel.add_user_to_channel.type.app_error", nil, "", http.StatusBadRequest)
return
}
if channel.IsGroupConstrained() {
nonMembers, err := c.App.FilterNonGroupChannelMembers([]string{member.UserId}, channel)
if err != nil {
if v, ok := err.(*model.AppError); ok {
c.Err = v
} else {
c.Err = model.NewAppError("localAddChannelMember", "api.channel.add_members.error", nil, err.Error(), http.StatusBadRequest)
}
return
}
if len(nonMembers) > 0 {
c.Err = model.NewAppError("localAddChannelMember", "api.channel.add_members.user_denied", map[string]any{"UserIDs": nonMembers}, "", http.StatusBadRequest)
return
}
}
cm, err := c.App.AddChannelMember(c.AppContext, member.UserId, channel, app.ChannelMemberOpts{
PostRootID: postRootId,
})
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddMeta("add_user_id", cm.UserId)
auditRec.AddEventResultState(cm)
auditRec.AddEventObjectType("channel_member")
c.LogAudit("name=" + channel.Name + " user_id=" + cm.UserId)
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(cm); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localRemoveChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId().RequireUserId()
if c.Err != nil {
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
if !(channel.Type == model.ChannelTypeOpen || channel.Type == model.ChannelTypePrivate) {
c.Err = model.NewAppError("removeChannelMember", "api.channel.remove_channel_member.type.app_error", nil, "", http.StatusBadRequest)
return
}
if channel.IsGroupConstrained() && !user.IsBot {
c.Err = model.NewAppError("removeChannelMember", "api.channel.remove_member.group_constrained.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("localRemoveChannelMember", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
audit.AddEventParameter(auditRec, "remove_user_id", c.Params.UserId)
if err = c.App.RemoveUserFromChannel(c.AppContext, c.Params.UserId, "", channel); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("name=" + channel.Name + " user_id=" + c.Params.UserId)
ReturnStatusOK(w)
}
func localPatchChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
var patch *model.ChannelPatch
err := json.NewDecoder(r.Body).Decode(&patch)
if err != nil {
c.SetInvalidParamWithErr("channel", err)
return
}
originalOldChannel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if appErr != nil {
c.Err = appErr
return
}
channel := originalOldChannel.DeepCopy()
auditRec := c.MakeAuditRecord("localPatchChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "channel_patch", patch)
channel.Patch(patch)
rchannel, appErr := c.App.UpdateChannel(c.AppContext, channel)
if appErr != nil {
c.Err = appErr
return
}
appErr = c.App.FillInChannelProps(c.AppContext, rchannel)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
c.LogAudit("")
auditRec.AddEventResultState(rchannel)
auditRec.AddEventObjectType("channel")
if err := json.NewEncoder(w).Encode(rchannel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localMoveChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
props := model.StringInterfaceFromJSON(r.Body)
teamId, ok := props["team_id"].(string)
if !ok {
c.SetInvalidParam("team_id")
return
}
force, ok := props["force"].(bool)
if !ok {
c.SetInvalidParam("force")
return
}
team, err := c.App.GetTeam(teamId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("localMoveChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "team_id", teamId)
audit.AddEventParameter(auditRec, "force", force)
// TODO do we need these?
auditRec.AddMeta("channel_id", channel.Id)
auditRec.AddMeta("channel_name", channel.Name)
auditRec.AddMeta("team_id", team.Id)
auditRec.AddMeta("team_name", team.Name)
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
c.Err = model.NewAppError("moveChannel", "api.channel.move_channel.type.invalid", nil, "", http.StatusForbidden)
return
}
err = c.App.RemoveAllDeactivatedMembersFromChannel(c.AppContext, channel)
if err != nil {
c.Err = err
return
}
if force {
err = c.App.RemoveUsersFromChannelNotMemberOfTeam(c.AppContext, nil, channel, team)
if err != nil {
c.Err = err
return
}
}
err = c.App.MoveChannel(c.AppContext, team, channel, nil)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(channel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
c.LogAudit("channel=" + channel.Name)
c.LogAudit("team=" + team.Name)
if err := json.NewEncoder(w).Encode(channel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localDeleteChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("localDeleteChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
auditRec.AddEventPriorState(channel)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
c.Err = model.NewAppError("localDeleteChannel", "api.channel.delete_channel.type.invalid", nil, "", http.StatusBadRequest)
return
}
if c.Params.Permanent {
err = c.App.PermanentDeleteChannel(c.AppContext, channel)
} else {
err = c.App.DeleteChannel(c.AppContext, channel, "")
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(channel)
auditRec.AddEventObjectType("channel")
c.LogAudit("name=" + channel.Name)
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"encoding/binary"
"encoding/json"
"io"
"net/http"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/web"
)
func (api *API) InitCloud() {
// GET /api/v4/cloud/products
api.BaseRoutes.Cloud.Handle("/products", api.APISessionRequired(getCloudProducts)).Methods("GET")
// GET /api/v4/cloud/limits
api.BaseRoutes.Cloud.Handle("/limits", api.APISessionRequired(getCloudLimits)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/products/selfhosted", api.APISessionRequired(getSelfHostedProducts)).Methods("GET")
// POST /api/v4/cloud/payment
// POST /api/v4/cloud/payment/confirm
api.BaseRoutes.Cloud.Handle("/payment", api.APISessionRequired(createCustomerPayment)).Methods("POST")
api.BaseRoutes.Cloud.Handle("/payment/confirm", api.APISessionRequired(confirmCustomerPayment)).Methods("POST")
// GET /api/v4/cloud/customer
// PUT /api/v4/cloud/customer
// PUT /api/v4/cloud/customer/address
api.BaseRoutes.Cloud.Handle("/customer", api.APISessionRequired(getCloudCustomer)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/customer", api.APISessionRequired(updateCloudCustomer)).Methods("PUT")
api.BaseRoutes.Cloud.Handle("/customer/address", api.APISessionRequired(updateCloudCustomerAddress)).Methods("PUT")
// GET /api/v4/cloud/subscription
api.BaseRoutes.Cloud.Handle("/subscription", api.APISessionRequired(getSubscription)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription/invoices", api.APISessionRequired(getInvoicesForSubscription)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription/invoices/{invoice_id:[_A-Za-z0-9]+}/pdf", api.APISessionRequired(getSubscriptionInvoicePDF)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription/self-serve-status", api.APISessionRequired(getLicenseSelfServeStatus)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription", api.APISessionRequired(changeSubscription)).Methods("PUT")
// GET /api/v4/cloud/request-trial
api.BaseRoutes.Cloud.Handle("/request-trial", api.APISessionRequired(requestCloudTrial)).Methods("PUT")
// GET /api/v4/cloud/validate-business-email
api.BaseRoutes.Cloud.Handle("/validate-business-email", api.APISessionRequired(validateBusinessEmail)).Methods("POST")
api.BaseRoutes.Cloud.Handle("/validate-workspace-business-email", api.APISessionRequired(validateWorkspaceBusinessEmail)).Methods("POST")
// POST /api/v4/cloud/webhook
api.BaseRoutes.Cloud.Handle("/webhook", api.CloudAPIKeyRequired(handleCWSWebhook)).Methods("POST")
// GET /api/v4/cloud/cws-health-check
api.BaseRoutes.Cloud.Handle("/check-cws-connection", api.APIHandler(handleCheckCWSConnection)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/delete-workspace", api.APISessionRequired(selfServeDeleteWorkspace)).Methods(http.MethodDelete)
}
func getSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getSubscription", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
subscription, err := c.App.Cloud().GetSubscription(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.getSubscription", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError)
return
}
// if it is an end user, return basic subscription data without sensitive information
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
subscription = &model.Subscription{
ID: subscription.ID,
ProductID: subscription.ProductID,
IsFreeTrial: subscription.IsFreeTrial,
TrialEndAt: subscription.TrialEndAt,
CustomerID: "",
AddOns: []string{},
StartAt: 0,
EndAt: 0,
CreateAt: 0,
Seats: 0,
Status: "",
DNS: "",
LastInvoice: &model.Invoice{},
DelinquentSince: subscription.DelinquentSince,
}
}
json, err := json.Marshal(subscription)
if err != nil {
c.Err = model.NewAppError("Api4.getSubscription", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func changeSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
userId := c.AppContext.Session().UserId
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.license_error", nil, "", http.StatusInternalServerError)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
var subscriptionChange *model.SubscriptionChange
if err = json.Unmarshal(bodyBytes, &subscriptionChange); err != nil {
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
currentSubscription, appErr := c.App.Cloud().GetSubscription(userId)
if appErr != nil {
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
return
}
changedSub, err := c.App.Cloud().ChangeSubscription(userId, currentSubscription.ID, subscriptionChange)
if err != nil {
appErr := model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
if err.Error() == "compliance-failed" {
c.Logger.Error("Compliance check failed", mlog.Err(err))
appErr.StatusCode = http.StatusUnprocessableEntity
}
c.Err = appErr
return
}
if subscriptionChange.Feedback != nil {
c.App.Srv().GetTelemetryService().SendTelemetry("downgrade_feedback", subscriptionChange.Feedback.ToMap())
}
json, err := json.Marshal(changedSub)
if err != nil {
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
product, err := c.App.Cloud().GetCloudProduct(c.AppContext.Session().UserId, subscriptionChange.ProductID)
if err != nil || product == nil {
c.Logger.Error("Error finding the new cloud product", mlog.Err(err))
}
if product.SKU == string(model.SkuCloudStarter) {
w.Write(json)
return
}
isYearly := product.IsYearly()
// Log failures for purchase confirmation email, but don't show an error to the user so as not to confuse them
// At this point, the upgrade is complete.
if appErr := c.App.SendUpgradeConfirmationEmail(isYearly); appErr != nil {
c.Logger.Error("Error sending purchase confirmation email", mlog.Err(appErr))
}
w.Write(json)
}
func requestCloudTrial(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
// check if the email needs to be set
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
// this value will not be empty when both emails (user admin and CWS customer) are not business email and
// a new business email was provided via the request business email modal
var startTrialRequest *model.StartCloudTrialRequest
if err = json.Unmarshal(bodyBytes, &startTrialRequest); err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
changedSub, err := c.App.Cloud().RequestCloudTrial(c.AppContext.Session().UserId, startTrialRequest.SubscriptionID, startTrialRequest.Email)
if err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(changedSub)
if err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
defer c.App.Srv().Cloud.InvalidateCaches()
w.Write(json)
}
func validateBusinessEmail(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.request_error", nil, "", http.StatusForbidden).Wrap(appErr)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
var emailToValidate *model.ValidateBusinessEmailRequest
err = json.Unmarshal(bodyBytes, &emailToValidate)
if err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
err = c.App.Cloud().ValidateBusinessEmail(user.Id, emailToValidate.Email)
if err != nil {
c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.request_error", nil, "", http.StatusForbidden).Wrap(err)
emailResp := model.ValidateBusinessEmailResponse{IsValid: false}
if err := json.NewEncoder(w).Encode(emailResp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
return
}
emailResp := model.ValidateBusinessEmailResponse{IsValid: true}
if err := json.NewEncoder(w).Encode(emailResp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func validateWorkspaceBusinessEmail(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
user, userErr := c.App.GetUser(c.AppContext.Session().UserId)
if userErr != nil {
c.Err = userErr
return
}
// get the cloud customer email to validate if is a valid business email
cloudCustomer, err := c.App.Cloud().GetCloudCustomer(user.Id)
if err != nil {
c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.request_error", nil, err.Error(), http.StatusBadRequest)
return
}
emailErr := c.App.Cloud().ValidateBusinessEmail(user.Id, cloudCustomer.Email)
// if the current workspace email is not a valid business email
if emailErr != nil {
// grab the current admin email and validate it
errValidatingAdminEmail := c.App.Cloud().ValidateBusinessEmail(user.Id, user.Email)
if errValidatingAdminEmail != nil {
c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.request_error", nil, errValidatingAdminEmail.Error(), http.StatusForbidden)
emailResp := model.ValidateBusinessEmailResponse{IsValid: false}
if err := json.NewEncoder(w).Encode(emailResp); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
return
}
}
// if any of the emails is valid, return ok
emailResp := model.ValidateBusinessEmailResponse{IsValid: true}
if err := json.NewEncoder(w).Encode(emailResp); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func getSelfHostedProducts(c *Context, w http.ResponseWriter, r *http.Request) {
products, err := c.App.Cloud().GetSelfHostedProducts(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.getSelfHostedProducts", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
byteProductsData, err := json.Marshal(products)
if err != nil {
c.Err = model.NewAppError("Api4.getSelfHostedProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
sanitizedProducts := []model.UserFacingProduct{}
err = json.Unmarshal(byteProductsData, &sanitizedProducts)
if err != nil {
c.Err = model.NewAppError("Api4.getSelfHostedProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
byteSanitizedProductsData, err := json.Marshal(sanitizedProducts)
if err != nil {
c.Err = model.NewAppError("Api4.getSelfHostedProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(byteSanitizedProductsData)
return
}
w.Write(byteProductsData)
}
func getCloudProducts(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
includeLegacyProducts := r.URL.Query().Get("include_legacy") == "true"
products, err := c.App.Cloud().GetCloudProducts(c.AppContext.Session().UserId, includeLegacyProducts)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
byteProductsData, err := json.Marshal(products)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
sanitizedProducts := []model.UserFacingProduct{}
err = json.Unmarshal(byteProductsData, &sanitizedProducts)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
byteSanitizedProductsData, err := json.Marshal(sanitizedProducts)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(byteSanitizedProductsData)
return
}
w.Write(byteProductsData)
}
func getCloudLimits(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getCloudLimits", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
limits, err := c.App.Cloud().GetCloudLimits(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudLimits", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(limits)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudLimits", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func getCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getCloudCustomer", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
c.SetPermissionError(model.PermissionSysconsoleReadBilling)
return
}
customer, err := c.App.Cloud().GetCloudCustomer(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudCustomer", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(customer)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudCustomer", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
// getLicenseSelfServeStatus makes check for the license in the CWS self-serve portal and establishes if the license is renewable, expandable etc.
func getLicenseSelfServeStatus(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
_, token, err := c.App.Srv().GenerateLicenseRenewalLink()
if err != nil {
c.Err = err
return
}
status, cloudErr := c.App.Cloud().GetLicenseSelfServeStatus(c.AppContext.Session().UserId, token)
if cloudErr != nil {
c.Err = model.NewAppError("Api4.getLicenseSelfServeStatus", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(cloudErr)
return
}
json, jsonErr := json.Marshal(status)
if jsonErr != nil {
c.Err = model.NewAppError("Api4.getLicenseSelfServeStatus", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
return
}
w.Write(json)
}
func updateCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
var customerInfo *model.CloudCustomerInfo
if err = json.Unmarshal(bodyBytes, &customerInfo); err != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
customer, appErr := c.App.Cloud().UpdateCloudCustomer(c.AppContext.Session().UserId, customerInfo)
if appErr != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
return
}
json, err := json.Marshal(customer)
if err != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func updateCloudCustomerAddress(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
var address *model.Address
if err = json.Unmarshal(bodyBytes, &address); err != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
customer, appErr := c.App.Cloud().UpdateCloudCustomerAddress(c.AppContext.Session().UserId, address)
if appErr != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
return
}
json, err := json.Marshal(customer)
if err != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func createCustomerPayment(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.createCustomerPayment", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
auditRec := c.MakeAuditRecord("createCustomerPayment", audit.Fail)
defer c.LogAuditRec(auditRec)
intent, err := c.App.Cloud().CreateCustomerPayment(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.createCustomerPayment", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(intent)
if err != nil {
c.Err = model.NewAppError("Api4.createCustomerPayment", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(json)
}
func confirmCustomerPayment(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.confirmCustomerPayment", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
auditRec := c.MakeAuditRecord("confirmCustomerPayment", audit.Fail)
defer c.LogAuditRec(auditRec)
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.confirmCustomerPayment", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
var confirmRequest *model.ConfirmPaymentMethodRequest
if err = json.Unmarshal(bodyBytes, &confirmRequest); err != nil {
c.Err = model.NewAppError("Api4.confirmCustomerPayment", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
err = c.App.Cloud().ConfirmCustomerPayment(c.AppContext.Session().UserId, confirmRequest)
if err != nil {
c.Err = model.NewAppError("Api4.createCustomerPayment", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getInvoicesForSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getInvoicesForSubscription", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
c.SetPermissionError(model.PermissionSysconsoleReadBilling)
return
}
invoices, appErr := c.App.Cloud().GetInvoicesForSubscription(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = model.NewAppError("Api4.getInvoicesForSubscription", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
return
}
json, err := json.Marshal(invoices)
if err != nil {
c.Err = model.NewAppError("Api4.getInvoicesForSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func getSubscriptionInvoicePDF(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getSubscriptionInvoicePDF", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
c.RequireInvoiceId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
c.SetPermissionError(model.PermissionSysconsoleReadBilling)
return
}
pdfData, filename, appErr := c.App.Cloud().GetInvoicePDF(c.AppContext.Session().UserId, c.Params.InvoiceId)
if appErr != nil {
c.Err = model.NewAppError("Api4.getSubscriptionInvoicePDF", "api.cloud.request_error", nil, appErr.Error(), http.StatusInternalServerError)
return
}
web.WriteFileResponse(
filename,
"application/pdf",
int64(binary.Size(pdfData)),
time.Now(),
*c.App.Config().ServiceSettings.WebserverMode,
bytes.NewReader(pdfData),
false,
w,
r,
)
}
func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
defer r.Body.Close()
var event *model.CWSWebhookPayload
if err = json.Unmarshal(bodyBytes, &event); err != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
switch event.Event {
case model.EventTypeFailedPayment:
if nErr := c.App.SendPaymentFailedEmail(event.FailedPayment); nErr != nil {
c.Err = nErr
return
}
case model.EventTypeFailedPaymentNoCard:
if nErr := c.App.SendNoCardPaymentFailedEmail(); nErr != nil {
c.Err = nErr
return
}
case model.EventTypeSendUpgradeConfirmationEmail:
// isYearly determines whether to send the yearly or monthly Upgrade email
isYearly := false
if event.Subscription != nil && event.CloudWorkspaceOwner != nil {
user, appErr := c.App.GetUserByUsername(event.CloudWorkspaceOwner.UserName)
if appErr != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", appErr.Id, nil, appErr.Error(), appErr.StatusCode)
return
}
// Get the current cloud product to determine whether it's a monthly or yearly product
product, err := c.App.Cloud().GetCloudProduct(user.Id, event.Subscription.ProductID)
if err != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError)
return
}
isYearly = product.IsYearly()
}
if nErr := c.App.SendUpgradeConfirmationEmail(isYearly); nErr != nil {
c.Err = nErr
return
}
case model.EventTypeSendAdminWelcomeEmail:
user, appErr := c.App.GetUserByUsername(event.CloudWorkspaceOwner.UserName)
if appErr != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", appErr.Id, nil, appErr.Error(), appErr.StatusCode)
return
}
teams, appErr := c.App.GetAllTeams()
if appErr != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", appErr.Id, nil, appErr.Error(), appErr.StatusCode)
return
}
team := teams[0]
subscription, err := c.App.Cloud().GetSubscription(user.Id)
if err != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError)
return
}
if err := c.App.Srv().EmailService.SendCloudWelcomeEmail(user.Email, user.Locale, team.InviteId, subscription.GetWorkSpaceNameFromDNS(), subscription.DNS, *c.App.Config().ServiceSettings.SiteURL); err != nil {
c.Err = model.NewAppError("SendCloudWelcomeEmail", "api.user.send_cloud_welcome_email.error", nil, err.Error(), http.StatusInternalServerError)
return
}
case model.EventTypeTriggerDelinquencyEmail:
var emailToTrigger model.DelinquencyEmail
if event.DelinquencyEmail != nil {
emailToTrigger = model.DelinquencyEmail(event.DelinquencyEmail.EmailToTrigger)
} else {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.delinquency_email.missing_email_to_trigger", nil, "", http.StatusInternalServerError)
return
}
if nErr := c.App.SendDelinquencyEmail(emailToTrigger); nErr != nil {
c.Err = nErr
return
}
default:
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.cws_webhook_event_missing_error", nil, "", http.StatusNotFound)
return
}
ReturnStatusOK(w)
}
func handleCheckCWSConnection(c *Context, w http.ResponseWriter, r *http.Request) {
cloud := c.App.Cloud()
if cloud == nil {
c.Err = model.NewAppError("Api4.handleCWSHealthCheck", "api.server.cws.needs_enterprise_edition", nil, "", http.StatusBadRequest)
return
}
if err := cloud.CheckCWSConnection(c.AppContext.Session().UserId); err != nil {
c.Err = model.NewAppError("Api4.handleCWSHealthCheck", "api.server.cws.health_check.app_error", nil, "CWS Server is not available.", http.StatusInternalServerError)
return
}
ReturnStatusOK(w)
}
func selfServeDeleteWorkspace(c *Context, w http.ResponseWriter, r *http.Request) {
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.selfServeDeleteWorkspace", "api.cloud.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
defer r.Body.Close()
var deleteRequest *model.WorkspaceDeletionRequest
if err = json.Unmarshal(bodyBytes, &deleteRequest); err != nil {
c.Err = model.NewAppError("Api4.selfServeDeleteWorkspace", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
if err := c.App.Cloud().SelfServeDeleteWorkspace(c.AppContext.Session().UserId, deleteRequest); err != nil {
c.Err = model.NewAppError("Api4.selfServeDeleteWorkspace", "api.server.cws.delete_workspace.app_error", nil, "CWS Server failed to delete workspace.", http.StatusInternalServerError)
return
}
c.App.Srv().GetTelemetryService().SendTelemetry("delete_workspace_feedback", deleteRequest.Feedback.ToMap())
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
)
func (api *API) InitCluster() {
api.BaseRoutes.Cluster.Handle("/status", api.APISessionRequired(getClusterStatus)).Methods("GET")
}
func getClusterStatus(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadEnvironmentHighAvailability) {
c.SetPermissionError(model.PermissionSysconsoleReadEnvironmentHighAvailability)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("getClusterStatus", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
infos := c.App.GetClusterStatus()
js, err := json.Marshal(infos)
if err != nil {
c.Err = model.NewAppError("getClusterStatus", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitCommand() {
api.BaseRoutes.Commands.Handle("", api.APISessionRequired(createCommand)).Methods("POST")
api.BaseRoutes.Commands.Handle("", api.APISessionRequired(listCommands)).Methods("GET")
api.BaseRoutes.Commands.Handle("/execute", api.APISessionRequired(executeCommand)).Methods("POST")
api.BaseRoutes.Command.Handle("", api.APISessionRequired(getCommand)).Methods("GET")
api.BaseRoutes.Command.Handle("", api.APISessionRequired(updateCommand)).Methods("PUT")
api.BaseRoutes.Command.Handle("/move", api.APISessionRequired(moveCommand)).Methods("PUT")
api.BaseRoutes.Command.Handle("", api.APISessionRequired(deleteCommand)).Methods("DELETE")
api.BaseRoutes.Team.Handle("/commands/autocomplete", api.APISessionRequired(listAutocompleteCommands)).Methods("GET")
api.BaseRoutes.Team.Handle("/commands/autocomplete_suggestions", api.APISessionRequired(listCommandAutocompleteSuggestions)).Methods("GET")
api.BaseRoutes.Command.Handle("/regen_token", api.APISessionRequired(regenCommandToken)).Methods("PUT")
}
func createCommand(c *Context, w http.ResponseWriter, r *http.Request) {
var cmd model.Command
if jsonErr := json.NewDecoder(r.Body).Decode(&cmd); jsonErr != nil {
c.SetInvalidParamWithErr("command", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createCommand", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "command", &cmd)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) {
c.SetPermissionError(model.PermissionManageSlashCommands)
return
}
cmd.CreatorId = c.AppContext.Session().UserId
rcmd, err := c.App.CreateCommand(&cmd)
if err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
auditRec.AddEventResultState(rcmd)
auditRec.AddEventObjectType("command")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rcmd); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateCommand(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireCommandId()
if c.Err != nil {
return
}
var cmd model.Command
if jsonErr := json.NewDecoder(r.Body).Decode(&cmd); jsonErr != nil || cmd.Id != c.Params.CommandId {
c.SetInvalidParamWithErr("command", jsonErr)
return
}
auditRec := c.MakeAuditRecord("updateCommand", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "command", &cmd)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
oldCmd, err := c.App.GetCommand(c.Params.CommandId)
if err != nil {
audit.AddEventParameter(auditRec, "command_id", c.Params.CommandId)
c.SetCommandNotFoundError()
return
}
auditRec.AddEventPriorState(oldCmd)
if cmd.TeamId != oldCmd.TeamId {
c.Err = model.NewAppError("updateCommand", "api.command.team_mismatch.app_error", nil, "user_id="+c.AppContext.Session().UserId, http.StatusBadRequest)
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), oldCmd.TeamId, model.PermissionManageSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
// here we return Not_found instead of a permissions error so we don't leak the existence of
// a command to someone without permissions for the team it belongs to.
c.SetCommandNotFoundError()
return
}
if c.AppContext.Session().UserId != oldCmd.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), oldCmd.TeamId, model.PermissionManageOthersSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersSlashCommands)
return
}
rcmd, err := c.App.UpdateCommand(oldCmd, &cmd)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(rcmd)
auditRec.AddEventObjectType("command")
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(rcmd); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func moveCommand(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireCommandId()
if c.Err != nil {
return
}
var cmr model.CommandMoveRequest
if jsonErr := json.NewDecoder(r.Body).Decode(&cmr); jsonErr != nil {
c.SetInvalidParamWithErr("team_id", jsonErr)
return
}
auditRec := c.MakeAuditRecord("moveCommand", audit.Fail)
audit.AddEventParameter(auditRec, "command_move_request", cmr.TeamId)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
newTeam, appErr := c.App.GetTeam(cmr.TeamId)
if appErr != nil {
c.Err = appErr
return
}
audit.AddEventParameterAuditable(auditRec, "team", newTeam)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), newTeam.Id, model.PermissionManageSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageSlashCommands)
return
}
cmd, appErr := c.App.GetCommand(c.Params.CommandId)
if appErr != nil {
c.SetCommandNotFoundError()
return
}
auditRec.AddEventPriorState(cmd)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
// here we return Not_found instead of a permissions error so we don't leak the existence of
// a command to someone without permissions for the team it belongs to.
c.SetCommandNotFoundError()
return
}
if appErr = c.App.MoveCommand(newTeam, cmd); appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(cmd)
auditRec.AddEventObjectType("command")
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}
func deleteCommand(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireCommandId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("deleteCommand", audit.Fail)
audit.AddEventParameter(auditRec, "command_id", c.Params.CommandId)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
cmd, err := c.App.GetCommand(c.Params.CommandId)
if err != nil {
c.SetCommandNotFoundError()
return
}
auditRec.AddEventPriorState(cmd)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
// here we return Not_found instead of a permissions error so we don't leak the existence of
// a command to someone without permissions for the team it belongs to.
c.SetCommandNotFoundError()
return
}
if c.AppContext.Session().UserId != cmd.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageOthersSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersSlashCommands)
return
}
err = c.App.DeleteCommand(cmd.Id)
if err != nil {
c.Err = err
return
}
auditRec.AddEventObjectType("command")
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}
func listCommands(c *Context, w http.ResponseWriter, r *http.Request) {
customOnly, _ := strconv.ParseBool(r.URL.Query().Get("custom_only"))
teamId := r.URL.Query().Get("team_id")
if teamId == "" {
c.SetInvalidParam("team_id")
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
var commands []*model.Command
var err *model.AppError
if customOnly {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageSlashCommands) {
c.SetPermissionError(model.PermissionManageSlashCommands)
return
}
commands, err = c.App.ListTeamCommands(teamId)
if err != nil {
c.Err = err
return
}
} else {
//User with no permission should see only system commands
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageSlashCommands) {
commands, err = c.App.ListAutocompleteCommands(teamId, c.AppContext.T)
if err != nil {
c.Err = err
return
}
} else {
commands, err = c.App.ListAllCommands(teamId, c.AppContext.T)
if err != nil {
c.Err = err
return
}
}
}
if err := json.NewEncoder(w).Encode(commands); err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func getCommand(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireCommandId()
if c.Err != nil {
return
}
cmd, err := c.App.GetCommand(c.Params.CommandId)
if err != nil {
c.SetCommandNotFoundError()
return
}
// check for permissions to view this command; must have perms to view team and
// PERMISSION_MANAGE_SLASH_COMMANDS for the team the command belongs to.
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionViewTeam) {
// here we return Not_found instead of a permissions error so we don't leak the existence of
// a command to someone without permissions for the team it belongs to.
c.SetCommandNotFoundError()
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) {
// again, return not_found to ensure id existence does not leak.
c.SetCommandNotFoundError()
return
}
if err := json.NewEncoder(w).Encode(cmd); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) {
var commandArgs model.CommandArgs
if jsonErr := json.NewDecoder(r.Body).Decode(&commandArgs); jsonErr != nil {
c.SetInvalidParamWithErr("command_args", jsonErr)
return
}
if len(commandArgs.Command) <= 1 || strings.Index(commandArgs.Command, "/") != 0 || !model.IsValidId(commandArgs.ChannelId) {
c.Err = model.NewAppError("executeCommand", "api.command.execute_command.start.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("executeCommand", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "command_args", &commandArgs)
// checks that user is a member of the specified channel, and that they have permission to use slash commands in it
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), commandArgs.ChannelId, model.PermissionUseSlashCommands) {
c.SetPermissionError(model.PermissionUseSlashCommands)
return
}
channel, err := c.App.GetChannel(c.AppContext, commandArgs.ChannelId)
if err != nil {
c.Err = err
return
}
if channel.Type != model.ChannelTypeDirect && channel.Type != model.ChannelTypeGroup {
// if this isn't a DM or GM, the team id is implicitly taken from the channel so that slash commands created on
// some other team can't be run against this one
commandArgs.TeamId = channel.TeamId
} else {
// if the slash command was used in a DM or GM, ensure that the user is a member of the specified team, so that
// they can't just execute slash commands against arbitrary teams
if c.AppContext.Session().GetTeamByTeamId(commandArgs.TeamId) == nil {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionUseSlashCommands) {
c.SetPermissionError(model.PermissionUseSlashCommands)
return
}
}
}
commandArgs.UserId = c.AppContext.Session().UserId
commandArgs.T = c.AppContext.T
commandArgs.SiteURL = c.GetSiteURLHeader()
commandArgs.Session = *c.AppContext.Session()
response, err := c.App.ExecuteCommand(c.AppContext, &commandArgs)
if err != nil {
c.Err = err
return
}
auditRec.Success()
if err := json.NewEncoder(w).Encode(response); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func listAutocompleteCommands(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
commands, err := c.App.ListAutocompleteCommands(c.Params.TeamId, c.AppContext.T)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(commands); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func listCommandAutocompleteSuggestions(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
roleId := model.SystemUserRoleId
if c.IsSystemAdmin() {
roleId = model.SystemAdminRoleId
}
query := r.URL.Query()
userInput := query.Get("user_input")
if userInput == "" {
c.SetInvalidParam("userInput")
return
}
userInput = strings.TrimPrefix(userInput, "/")
commands, appErr := c.App.ListAutocompleteCommands(c.Params.TeamId, c.AppContext.T)
if appErr != nil {
c.Err = appErr
return
}
commandArgs := &model.CommandArgs{
ChannelId: query.Get("channel_id"),
TeamId: c.Params.TeamId,
RootId: query.Get("root_id"),
UserId: c.AppContext.Session().UserId,
T: c.AppContext.T,
Session: *c.AppContext.Session(),
SiteURL: c.GetSiteURLHeader(),
Command: userInput,
}
suggestions := c.App.GetSuggestions(c.AppContext, commandArgs, commands, roleId)
js, err := json.Marshal(suggestions)
if err != nil {
c.Err = model.NewAppError("listCommandAutocompleteSuggestions", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func regenCommandToken(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireCommandId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("regenCommandToken", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
cmd, err := c.App.GetCommand(c.Params.CommandId)
if err != nil {
audit.AddEventParameter(auditRec, "command_id", c.Params.CommandId)
c.SetCommandNotFoundError()
return
}
auditRec.AddEventPriorState(cmd)
audit.AddEventParameter(auditRec, "command_id", c.Params.CommandId)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
// here we return Not_found instead of a permissions error so we don't leak the existence of
// a command to someone without permissions for the team it belongs to.
c.SetCommandNotFoundError()
return
}
if c.AppContext.Session().UserId != cmd.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageOthersSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersSlashCommands)
return
}
rcmd, err := c.App.RegenCommandToken(cmd)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(rcmd)
auditRec.Success()
c.LogAudit("success")
resp := make(map[string]string)
resp["token"] = rcmd.Token
w.Write([]byte(model.MapToJSON(resp)))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitCommandLocal() {
api.BaseRoutes.Commands.Handle("", api.APILocal(localCreateCommand)).Methods("POST")
api.BaseRoutes.Commands.Handle("", api.APILocal(listCommands)).Methods("GET")
api.BaseRoutes.Command.Handle("", api.APILocal(getCommand)).Methods("GET")
api.BaseRoutes.Command.Handle("", api.APILocal(updateCommand)).Methods("PUT")
api.BaseRoutes.Command.Handle("/move", api.APILocal(moveCommand)).Methods("PUT")
api.BaseRoutes.Command.Handle("", api.APILocal(deleteCommand)).Methods("DELETE")
}
func localCreateCommand(c *Context, w http.ResponseWriter, r *http.Request) {
var cmd model.Command
if jsonErr := json.NewDecoder(r.Body).Decode(&cmd); jsonErr != nil {
c.SetInvalidParamWithErr("command", jsonErr)
return
}
auditRec := c.MakeAuditRecord("localCreateCommand", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "command", &cmd)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
rcmd, err := c.App.CreateCommand(&cmd)
if err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
auditRec.AddEventResultState(rcmd)
auditRec.AddEventObjectType("command")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rcmd); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"strconv"
"github.com/avct/uasurfer"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitCompliance() {
api.BaseRoutes.Compliance.Handle("/reports", api.APISessionRequired(createComplianceReport)).Methods("POST")
api.BaseRoutes.Compliance.Handle("/reports", api.APISessionRequired(getComplianceReports)).Methods("GET")
api.BaseRoutes.Compliance.Handle("/reports/{report_id:[A-Za-z0-9]+}", api.APISessionRequired(getComplianceReport)).Methods("GET")
api.BaseRoutes.Compliance.Handle("/reports/{report_id:[A-Za-z0-9]+}/download", api.APISessionRequiredTrustRequester(downloadComplianceReport)).Methods("GET")
}
func createComplianceReport(c *Context, w http.ResponseWriter, r *http.Request) {
var job model.Compliance
if jsonErr := json.NewDecoder(r.Body).Decode(&job); jsonErr != nil {
c.SetInvalidParamWithErr("compliance", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createComplianceReport", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "compliance", &job)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateComplianceExportJob) {
c.SetPermissionError(model.PermissionCreateComplianceExportJob)
return
}
job.UserId = c.AppContext.Session().UserId
rjob, err := c.App.SaveComplianceReport(&job)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(rjob)
auditRec.AddEventObjectType("compliance")
auditRec.AddMeta("compliance_id", rjob.Id)
auditRec.AddMeta("compliance_desc", rjob.Desc)
c.LogAudit("")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rjob); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getComplianceReports(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadComplianceExportJob) {
c.SetPermissionError(model.PermissionReadComplianceExportJob)
return
}
auditRec := c.MakeAuditRecord("getComplianceReports", audit.Fail)
defer c.LogAuditRec(auditRec)
crs, err := c.App.GetComplianceReports(c.Params.Page, c.Params.PerPage)
if err != nil {
c.Err = err
return
}
auditRec.Success()
if err := json.NewEncoder(w).Encode(crs); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getComplianceReport(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireReportId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("getComplianceReport", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadComplianceExportJob) {
c.SetPermissionError(model.PermissionReadComplianceExportJob)
return
}
audit.AddEventParameter(auditRec, "report_id", c.Params.ReportId)
job, err := c.App.GetComplianceReport(c.Params.ReportId)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddMeta("compliance_id", job.Id)
auditRec.AddMeta("compliance_desc", job.Desc)
if err := json.NewEncoder(w).Encode(job); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func downloadComplianceReport(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireReportId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("downloadComplianceReport", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "compliance_id", c.Params.ReportId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionDownloadComplianceExportResult) {
c.SetPermissionError(model.PermissionDownloadComplianceExportResult)
return
}
job, err := c.App.GetComplianceReport(c.Params.ReportId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(job)
auditRec.AddEventObjectType("compliance")
reportBytes, err := c.App.GetComplianceFile(job)
if err != nil {
c.Err = err
return
}
auditRec.AddMeta("length", len(reportBytes))
c.LogAudit("downloaded " + job.Desc)
w.Header().Set("Cache-Control", "max-age=2592000, private")
w.Header().Set("Content-Length", strconv.Itoa(len(reportBytes)))
w.Header().Del("Content-Type") // Content-Type will be set automatically by the http writer
// attach extra headers to trigger a download on IE, Edge, and Safari
ua := uasurfer.Parse(r.UserAgent())
w.Header().Set("Content-Disposition", "attachment;filename=\""+job.JobName()+".zip\"")
if ua.Browser.Name == uasurfer.BrowserIE || ua.Browser.Name == uasurfer.BrowserSafari {
// trim off anything before the final / so we just get the file's name
w.Header().Set("Content-Type", "application/octet-stream")
}
auditRec.Success()
w.Write(reportBytes)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var writeFilter func(c *Context, structField reflect.StructField) bool
var readFilter func(c *Context, structField reflect.StructField) bool
var permissionMap map[string]*model.Permission
type filterType string
const (
FilterTypeWrite filterType = "write"
FilterTypeRead filterType = "read"
)
func (api *API) InitConfig() {
api.BaseRoutes.APIRoot.Handle("/config", api.APISessionRequired(getConfig)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/config", api.APISessionRequired(updateConfig)).Methods("PUT")
api.BaseRoutes.APIRoot.Handle("/config/patch", api.APISessionRequired(patchConfig)).Methods("PUT")
api.BaseRoutes.APIRoot.Handle("/config/reload", api.APISessionRequired(configReload)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/config/client", api.APIHandler(getClientConfig)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/config/environment", api.APISessionRequired(getEnvironmentConfig)).Methods("GET")
}
func init() {
writeFilter = makeFilterConfigByPermission(FilterTypeWrite)
readFilter = makeFilterConfigByPermission(FilterTypeRead)
permissionMap = map[string]*model.Permission{}
for _, p := range model.AllPermissions {
permissionMap[p.Id] = p
}
}
func getConfig(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleReadPermissions) {
c.SetPermissionError(model.SysconsoleReadPermissions...)
return
}
auditRec := c.MakeAuditRecord("getConfig", audit.Fail)
defer c.LogAuditRec(auditRec)
cfg, err := config.Merge(&model.Config{}, c.App.GetSanitizedConfig(), &utils.MergeConfig{
StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
return readFilter(c, structField)
},
})
if err != nil {
c.Err = model.NewAppError("getConfig", "api.config.get_config.restricted_merge.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
auditRec.Success()
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if c.App.Channels().License().IsCloud() {
js, jsonErr := cfg.ToJSONFiltered(model.ConfigAccessTagType, model.ConfigAccessTagCloudRestrictable)
if jsonErr != nil {
c.Err = model.NewAppError("getConfig", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
return
}
w.Write(js)
return
}
if err := json.NewEncoder(w).Encode(cfg); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func configReload(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("configReload", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReloadConfig) {
c.SetPermissionError(model.PermissionReloadConfig)
return
}
if !c.AppContext.Session().IsUnrestricted() && *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("configReload", "api.restricted_system_admin", nil, "", http.StatusBadRequest)
return
}
if err := c.App.ReloadConfig(); err != nil {
c.Err = model.NewAppError("configReload", "api.config.reload_config.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
auditRec.Success()
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
ReturnStatusOK(w)
}
func updateConfig(c *Context, w http.ResponseWriter, r *http.Request) {
var cfg *model.Config
err := json.NewDecoder(r.Body).Decode(&cfg)
if err != nil || cfg == nil {
c.SetInvalidParamWithErr("config", err)
return
}
auditRec := c.MakeAuditRecord("updateConfig", audit.Fail)
// audit.AddEventParameter(auditRec, "config", cfg) // TODO We can do this but do we want to?
defer c.LogAuditRec(auditRec)
cfg.SetDefaults()
if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleWritePermissions) {
c.SetPermissionError(model.SysconsoleWritePermissions...)
return
}
appCfg := c.App.Config()
if *appCfg.ServiceSettings.SiteURL != "" && *cfg.ServiceSettings.SiteURL == "" {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.clear_siteurl.app_error", nil, "", http.StatusBadRequest)
return
}
cfg, err = config.Merge(appCfg, cfg, &utils.MergeConfig{
StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
return writeFilter(c, structField)
},
})
if err != nil {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.restricted_merge.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
// Do not allow plugin uploads to be toggled through the API
*cfg.PluginSettings.EnableUploads = *appCfg.PluginSettings.EnableUploads
// Do not allow certificates to be changed through the API
// This shallow-copies the slice header. So be careful if there are concurrent
// modifications to the slice.
cfg.PluginSettings.SignaturePublicKeyFiles = appCfg.PluginSettings.SignaturePublicKeyFiles
// Do not allow marketplace URL to be toggled through the API if EnableUploads are disabled.
if cfg.PluginSettings.EnableUploads != nil && !*appCfg.PluginSettings.EnableUploads {
*cfg.PluginSettings.MarketplaceURL = *appCfg.PluginSettings.MarketplaceURL
}
// There are some settings that cannot be changed in a cloud env
if c.App.Channels().License().IsCloud() {
// Both of them cannot be nil since cfg.SetDefaults is called earlier for cfg,
// and appCfg is the existing earlier config and if it's nil, server sets a default value.
if *appCfg.ComplianceSettings.Directory != *cfg.ComplianceSettings.Directory {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.not_allowed_security.app_error", map[string]any{"Name": "ComplianceSettings.Directory"}, "", http.StatusForbidden)
return
}
}
c.App.HandleMessageExportConfig(cfg, appCfg)
if appErr := cfg.IsValid(); appErr != nil {
c.Err = appErr
return
}
oldCfg, newCfg, appErr := c.App.SaveConfig(cfg, true)
if appErr != nil {
c.Err = appErr
return
}
// If the config for default server locale has changed, reinitialize the server's translations.
if oldCfg.LocalizationSettings.DefaultServerLocale != newCfg.LocalizationSettings.DefaultServerLocale {
s := newCfg.LocalizationSettings
if err = i18n.InitTranslations(*s.DefaultServerLocale, *s.DefaultClientLocale); err != nil {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.translations.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
}
diffs, err := config.Diff(oldCfg, newCfg)
if err != nil {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.diff.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.AddEventPriorState(&diffs)
newCfg.Sanitize()
cfg, err = config.Merge(&model.Config{}, newCfg, &utils.MergeConfig{
StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
return readFilter(c, structField)
},
})
if err != nil {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.restricted_merge.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
//auditRec.AddEventResultState(cfg) // TODO we can do this too but do we want to? the config object is huge
auditRec.AddEventObjectType("config")
auditRec.Success()
c.LogAudit("updateConfig")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if c.App.Channels().License().IsCloud() {
js, err := cfg.ToJSONFiltered(model.ConfigAccessTagType, model.ConfigAccessTagCloudRestrictable)
if err != nil {
c.Err = model.NewAppError("updateConfig", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
return
}
if err := json.NewEncoder(w).Encode(cfg); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getClientConfig(c *Context, w http.ResponseWriter, r *http.Request) {
format := r.URL.Query().Get("format")
if format == "" {
c.Err = model.NewAppError("getClientConfig", "api.config.client.old_format.app_error", nil, "", http.StatusNotImplemented)
return
}
if format != "old" {
c.SetInvalidParam("format")
return
}
var config map[string]string
if c.AppContext.Session().UserId == "" {
config = c.App.Srv().Platform().LimitedClientConfigWithComputed()
} else {
config = c.App.Srv().Platform().ClientConfigWithComputed()
}
w.Write([]byte(model.MapToJSON(config)))
}
func getEnvironmentConfig(c *Context, w http.ResponseWriter, r *http.Request) {
// Only return the environment variables for the subsections which the client is
// allowed to see
envConfig := c.App.GetEnvironmentConfig(func(structField reflect.StructField) bool {
return readFilter(c, structField)
})
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Write([]byte(model.StringInterfaceToJSON(envConfig)))
}
func patchConfig(c *Context, w http.ResponseWriter, r *http.Request) {
var cfg *model.Config
err := json.NewDecoder(r.Body).Decode(&cfg)
if err != nil || cfg == nil {
c.SetInvalidParamWithErr("config", err)
return
}
auditRec := c.MakeAuditRecord("patchConfig", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleWritePermissions) {
c.SetPermissionError(model.SysconsoleWritePermissions...)
return
}
appCfg := c.App.Config()
if *appCfg.ServiceSettings.SiteURL != "" && cfg.ServiceSettings.SiteURL != nil && *cfg.ServiceSettings.SiteURL == "" {
c.Err = model.NewAppError("patchConfig", "api.config.update_config.clear_siteurl.app_error", nil, "", http.StatusBadRequest)
return
}
filterFn := func(structField reflect.StructField, base, patch reflect.Value) bool {
return writeFilter(c, structField)
}
// Do not allow plugin uploads to be toggled through the API
if cfg.PluginSettings.EnableUploads != nil && *cfg.PluginSettings.EnableUploads != *appCfg.PluginSettings.EnableUploads {
c.Err = model.NewAppError("patchConfig", "api.config.update_config.not_allowed_security.app_error", map[string]any{"Name": "PluginSettings.EnableUploads"}, "", http.StatusForbidden)
return
}
// Do not allow marketplace URL to be toggled if plugin uploads are disabled.
if cfg.PluginSettings.MarketplaceURL != nil && cfg.PluginSettings.EnableUploads != nil {
// Breaking it down to 2 conditions to make it simple.
if *cfg.PluginSettings.MarketplaceURL != *appCfg.PluginSettings.MarketplaceURL && !*cfg.PluginSettings.EnableUploads {
c.Err = model.NewAppError("patchConfig", "api.config.update_config.not_allowed_security.app_error", map[string]any{"Name": "PluginSettings.MarketplaceURL"}, "", http.StatusForbidden)
return
}
}
// There are some settings that cannot be changed in a cloud env
if c.App.Channels().License().IsCloud() {
if cfg.ComplianceSettings.Directory != nil && *appCfg.ComplianceSettings.Directory != *cfg.ComplianceSettings.Directory {
c.Err = model.NewAppError("patchConfig", "api.config.update_config.not_allowed_security.app_error", map[string]any{"Name": "ComplianceSettings.Directory"}, "", http.StatusForbidden)
return
}
}
if cfg.MessageExportSettings.EnableExport != nil {
c.App.HandleMessageExportConfig(cfg, appCfg)
}
updatedCfg, err := config.Merge(appCfg, cfg, &utils.MergeConfig{
StructFieldFilter: filterFn,
})
if err != nil {
c.Err = model.NewAppError("patchConfig", "api.config.update_config.restricted_merge.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
appErr := updatedCfg.IsValid()
if appErr != nil {
c.Err = appErr
return
}
oldCfg, newCfg, appErr := c.App.SaveConfig(updatedCfg, true)
if appErr != nil {
c.Err = appErr
return
}
diffs, err := config.Diff(oldCfg, newCfg)
if err != nil {
c.Err = model.NewAppError("patchConfig", "api.config.patch_config.diff.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.AddEventPriorState(&diffs)
newCfg.Sanitize()
auditRec.Success()
cfg, err = config.Merge(&model.Config{}, newCfg, &utils.MergeConfig{
StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
return readFilter(c, structField)
},
})
if err != nil {
c.Err = model.NewAppError("patchConfig", "api.config.patch_config.restricted_merge.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if c.App.Channels().License().IsCloud() {
js, err := cfg.ToJSONFiltered(model.ConfigAccessTagType, model.ConfigAccessTagCloudRestrictable)
if err != nil {
c.Err = model.NewAppError("patchConfig", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
return
}
if err := json.NewEncoder(w).Encode(cfg); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func makeFilterConfigByPermission(accessType filterType) func(c *Context, structField reflect.StructField) bool {
return func(c *Context, structField reflect.StructField) bool {
if structField.Type.Kind() == reflect.Struct {
return true
}
tagPermissions := strings.Split(structField.Tag.Get("access"), ",")
// If there are no access tag values and the role has manage_system, no need to continue
// checking permissions.
if len(tagPermissions) == 0 {
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
return true
}
}
// one iteration for write_restrictable value, it could be anywhere in the order of values
for _, val := range tagPermissions {
tagValue := strings.TrimSpace(val)
if tagValue == "" {
continue
}
// ConfigAccessTagWriteRestrictable trumps all other permissions
if tagValue == model.ConfigAccessTagWriteRestrictable || tagValue == model.ConfigAccessTagCloudRestrictable {
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin && accessType == FilterTypeWrite {
return false
}
continue
}
}
// another iteration for permissions checks of other tag values
for _, val := range tagPermissions {
tagValue := strings.TrimSpace(val)
if tagValue == "" {
continue
}
if tagValue == model.ConfigAccessTagWriteRestrictable {
continue
}
if tagValue == model.ConfigAccessTagCloudRestrictable {
continue
}
if tagValue == model.ConfigAccessTagAnySysConsoleRead && accessType == FilterTypeRead &&
c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleReadPermissions) {
return true
}
permissionID := fmt.Sprintf("sysconsole_%s_%s", accessType, tagValue)
if permission, ok := permissionMap[permissionID]; ok {
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), permission) {
return true
}
} else {
mlog.Warn("Unrecognized config permissions tag value.", mlog.String("tag_value", permissionID))
}
}
// with manage_system, default to allow, otherwise default not-allow
return c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"reflect"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitConfigLocal() {
api.BaseRoutes.APIRoot.Handle("/config", api.APILocal(localGetConfig)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/config", api.APILocal(localUpdateConfig)).Methods("PUT")
api.BaseRoutes.APIRoot.Handle("/config/patch", api.APILocal(localPatchConfig)).Methods("PUT")
api.BaseRoutes.APIRoot.Handle("/config/reload", api.APILocal(configReload)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/config/migrate", api.APILocal(localMigrateConfig)).Methods("POST")
}
func localGetConfig(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("localGetConfig", audit.Fail)
defer c.LogAuditRec(auditRec)
cfg := c.App.GetSanitizedConfig()
auditRec.Success()
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if err := json.NewEncoder(w).Encode(cfg); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localUpdateConfig(c *Context, w http.ResponseWriter, r *http.Request) {
var cfg *model.Config
err := json.NewDecoder(r.Body).Decode(&cfg)
if err != nil || cfg == nil {
c.SetInvalidParamWithErr("config", err)
return
}
auditRec := c.MakeAuditRecord("localUpdateConfig", audit.Fail)
defer c.LogAuditRec(auditRec)
cfg.SetDefaults()
appCfg := c.App.Config()
// Do not allow plugin uploads to be toggled through the API
cfg.PluginSettings.EnableUploads = appCfg.PluginSettings.EnableUploads
// Do not allow certificates to be changed through the API
cfg.PluginSettings.SignaturePublicKeyFiles = appCfg.PluginSettings.SignaturePublicKeyFiles
c.App.HandleMessageExportConfig(cfg, appCfg)
appErr := cfg.IsValid()
if appErr != nil {
c.Err = appErr
return
}
oldCfg, newCfg, appErr := c.App.SaveConfig(cfg, true)
if appErr != nil {
c.Err = appErr
return
}
diffs, diffErr := config.Diff(oldCfg, newCfg)
if diffErr != nil {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.diff.app_error", nil, diffErr.Error(), http.StatusInternalServerError)
return
}
auditRec.AddEventPriorState(&diffs)
newCfg.Sanitize()
auditRec.Success()
c.LogAudit("updateConfig")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if err := json.NewEncoder(w).Encode(newCfg); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localPatchConfig(c *Context, w http.ResponseWriter, r *http.Request) {
var cfg *model.Config
err := json.NewDecoder(r.Body).Decode(&cfg)
if err != nil || cfg == nil {
c.SetInvalidParamWithErr("config", err)
return
}
auditRec := c.MakeAuditRecord("localPatchConfig", audit.Fail)
defer c.LogAuditRec(auditRec)
appCfg := c.App.Config()
filterFn := func(structField reflect.StructField, base, patch reflect.Value) bool {
return true
}
if cfg.MessageExportSettings.EnableExport != nil {
c.App.HandleMessageExportConfig(cfg, appCfg)
}
updatedCfg, mergeErr := config.Merge(appCfg, cfg, &utils.MergeConfig{
StructFieldFilter: filterFn,
})
if mergeErr != nil {
c.Err = model.NewAppError("patchConfig", "api.config.update_config.restricted_merge.app_error", nil, mergeErr.Error(), http.StatusInternalServerError)
return
}
appErr := updatedCfg.IsValid()
if appErr != nil {
c.Err = appErr
return
}
oldCfg, newCfg, appErr := c.App.SaveConfig(updatedCfg, true)
if appErr != nil {
c.Err = appErr
return
}
diffs, err := config.Diff(oldCfg, newCfg)
if err != nil {
c.Err = model.NewAppError("patchConfig", "api.config.patch_config.diff.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.AddEventPriorState(&diffs)
auditRec.Success()
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if err := json.NewEncoder(w).Encode(c.App.GetSanitizedConfig()); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localMigrateConfig(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.StringInterfaceFromJSON(r.Body)
from, ok := props["from"].(string)
if !ok {
c.SetInvalidParam("from")
return
}
to, ok := props["to"].(string)
if !ok {
c.SetInvalidParam("to")
return
}
auditRec := c.MakeAuditRecord("migrateConfig", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
err := config.Migrate(from, to)
if err != nil {
c.Err = model.NewAppError("migrateConfig", "api.config.migrate_config.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
auditRec.Success()
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitDataRetention() {
api.BaseRoutes.DataRetention.Handle("/policy", api.APISessionRequired(getGlobalPolicy)).Methods("GET")
api.BaseRoutes.DataRetention.Handle("/policies", api.APISessionRequired(getPolicies)).Methods("GET")
api.BaseRoutes.DataRetention.Handle("/policies_count", api.APISessionRequired(getPoliciesCount)).Methods("GET")
api.BaseRoutes.DataRetention.Handle("/policies", api.APISessionRequired(createPolicy)).Methods("POST")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}", api.APISessionRequired(getPolicy)).Methods("GET")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}", api.APISessionRequired(patchPolicy)).Methods("PATCH")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}", api.APISessionRequired(deletePolicy)).Methods("DELETE")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/teams", api.APISessionRequired(getTeamsForPolicy)).Methods("GET")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/teams", api.APISessionRequired(addTeamsToPolicy)).Methods("POST")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/teams", api.APISessionRequired(removeTeamsFromPolicy)).Methods("DELETE")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/teams/search", api.APISessionRequired(searchTeamsInPolicy)).Methods("POST")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/channels", api.APISessionRequired(getChannelsForPolicy)).Methods("GET")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/channels", api.APISessionRequired(addChannelsToPolicy)).Methods("POST")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/channels", api.APISessionRequired(removeChannelsFromPolicy)).Methods("DELETE")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/channels/search", api.APISessionRequired(searchChannelsInPolicy)).Methods("POST")
api.BaseRoutes.User.Handle("/data_retention/team_policies", api.APISessionRequired(getTeamPoliciesForUser)).Methods("GET")
api.BaseRoutes.User.Handle("/data_retention/channel_policies", api.APISessionRequired(getChannelPoliciesForUser)).Methods("GET")
}
func getGlobalPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
// No permission check required.
policy, appErr := c.App.GetGlobalRetentionPolicy()
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(policy)
if err != nil {
c.Err = model.NewAppError("getGlobalPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getPolicies(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
limit := c.Params.PerPage
offset := c.Params.Page * limit
policies, appErr := c.App.GetRetentionPolicies(offset, limit)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(policies)
if err != nil {
c.Err = model.NewAppError("getPolicies", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getPoliciesCount(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
count, appErr := c.App.GetRetentionPoliciesCount()
if appErr != nil {
c.Err = appErr
return
}
body := struct {
TotalCount int64 `json:"total_count"`
}{count}
err := json.NewEncoder(w).Encode(body)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func getPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
c.RequirePolicyId()
policy, appErr := c.App.GetRetentionPolicy(c.Params.PolicyId)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(policy)
if err != nil {
c.Err = model.NewAppError("getPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func createPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
var policy model.RetentionPolicyWithTeamAndChannelIDs
if jsonErr := json.NewDecoder(r.Body).Decode(&policy); jsonErr != nil {
c.SetInvalidParamWithErr("policy", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "policy", &policy)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
return
}
newPolicy, appErr := c.App.CreateRetentionPolicy(&policy)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(newPolicy)
auditRec.AddEventObjectType("policy")
js, err := json.Marshal(newPolicy)
if err != nil {
c.Err = model.NewAppError("createPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.WriteHeader(http.StatusCreated)
w.Write(js)
}
func patchPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
var patch model.RetentionPolicyWithTeamAndChannelIDs
if jsonErr := json.NewDecoder(r.Body).Decode(&patch); jsonErr != nil {
c.SetInvalidParamWithErr("policy", jsonErr)
return
}
c.RequirePolicyId()
patch.ID = c.Params.PolicyId
auditRec := c.MakeAuditRecord("patchPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "patch", &patch)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
return
}
policy, appErr := c.App.PatchRetentionPolicy(&patch)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(policy)
auditRec.AddEventObjectType("retention_policy")
js, err := json.Marshal(policy)
if err != nil {
c.Err = model.NewAppError("patchPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(js)
}
func deletePolicy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
policyId := c.Params.PolicyId
auditRec := c.MakeAuditRecord("deletePolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "policy_id", policyId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
return
}
err := c.App.DeleteRetentionPolicy(policyId)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getTeamsForPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
c.RequirePolicyId()
policyId := c.Params.PolicyId
limit := c.Params.PerPage
offset := c.Params.Page * limit
teams, appErr := c.App.GetTeamsForRetentionPolicy(policyId, offset, limit)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(teams)
if err != nil {
c.Err = model.NewAppError("Api4.getTeamsForPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func searchTeamsInPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
var props model.TeamSearch
if err := json.NewDecoder(r.Body).Decode(&props); err != nil {
c.SetInvalidParamWithErr("team_search", err)
return
}
props.PolicyID = model.NewString(c.Params.PolicyId)
props.IncludePolicyID = model.NewBool(true)
teams, _, appErr := c.App.SearchAllTeams(&props)
if appErr != nil {
c.Err = appErr
return
}
c.App.SanitizeTeams(*c.AppContext.Session(), teams)
js, err := json.Marshal(teams)
if err != nil {
c.Err = model.NewAppError("searchTeamsInPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func addTeamsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
policyId := c.Params.PolicyId
var teamIDs []string
jsonErr := json.NewDecoder(r.Body).Decode(&teamIDs)
if jsonErr != nil {
c.SetInvalidParamWithErr("team_ids", jsonErr)
return
}
auditRec := c.MakeAuditRecord("addTeamsToPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "policy_id", policyId)
audit.AddEventParameter(auditRec, "team_ids", teamIDs)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
return
}
err := c.App.AddTeamsToRetentionPolicy(policyId, teamIDs)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func removeTeamsFromPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
policyId := c.Params.PolicyId
var teamIDs []string
jsonErr := json.NewDecoder(r.Body).Decode(&teamIDs)
if jsonErr != nil {
c.SetInvalidParamWithErr("team_ids", jsonErr)
return
}
auditRec := c.MakeAuditRecord("removeTeamsFromPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "policy_id", policyId)
audit.AddEventParameter(auditRec, "team_ids", teamIDs)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
return
}
err := c.App.RemoveTeamsFromRetentionPolicy(policyId, teamIDs)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getChannelsForPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
c.RequirePolicyId()
policyId := c.Params.PolicyId
limit := c.Params.PerPage
offset := c.Params.Page * limit
channels, appErr := c.App.GetChannelsForRetentionPolicy(policyId, offset, limit)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(channels)
if err != nil {
c.Err = model.NewAppError("Api4.getChannelsForPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func searchChannelsInPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
var props *model.ChannelSearch
err := json.NewDecoder(r.Body).Decode(&props)
if err != nil {
c.SetInvalidParamWithErr("channel_search", err)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
opts := model.ChannelSearchOpts{
PolicyID: c.Params.PolicyId,
IncludePolicyID: true,
Deleted: props.Deleted,
IncludeDeleted: props.IncludeDeleted,
Public: props.Public,
Private: props.Private,
TeamIds: props.TeamIds,
}
channels, _, appErr := c.App.SearchAllChannels(c.AppContext, props.Term, opts)
if appErr != nil {
c.Err = appErr
return
}
channelsJSON, jsonErr := json.Marshal(channels)
if jsonErr != nil {
c.Err = model.NewAppError("searchChannelsInPolicy", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
return
}
w.Write(channelsJSON)
}
func addChannelsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
policyId := c.Params.PolicyId
var channelIDs []string
jsonErr := json.NewDecoder(r.Body).Decode(&channelIDs)
if jsonErr != nil {
c.SetInvalidParamWithErr("channel_ids", jsonErr)
return
}
auditRec := c.MakeAuditRecord("addChannelsToPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "policy_id", policyId)
audit.AddEventParameter(auditRec, "channel_ids", channelIDs)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
return
}
err := c.App.AddChannelsToRetentionPolicy(policyId, channelIDs)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func removeChannelsFromPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
policyId := c.Params.PolicyId
var channelIDs []string
jsonErr := json.NewDecoder(r.Body).Decode(&channelIDs)
if jsonErr != nil {
c.SetInvalidParamWithErr("channel_ids", jsonErr)
return
}
auditRec := c.MakeAuditRecord("removeChannelsFromPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "policy_id", policyId)
audit.AddEventParameter(auditRec, "channel_ids", channelIDs)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
return
}
err := c.App.RemoveChannelsFromRetentionPolicy(policyId, channelIDs)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getTeamPoliciesForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
userID := c.Params.UserId
limit := c.Params.PerPage
offset := c.Params.Page * limit
if userID != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
policies, err := c.App.GetTeamPoliciesForUser(userID, offset, limit)
if err != nil {
c.Err = err
return
}
js, jsonErr := json.Marshal(policies)
if jsonErr != nil {
c.Err = model.NewAppError("getTeamPoliciesForUser", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
return
}
w.Write(js)
}
func getChannelPoliciesForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
userID := c.Params.UserId
limit := c.Params.PerPage
offset := c.Params.Page * limit
if userID != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
policies, err := c.App.GetChannelPoliciesForUser(userID, offset, limit)
if err != nil {
c.Err = err
return
}
js, jsonErr := json.Marshal(policies)
if jsonErr != nil {
c.Err = model.NewAppError("getChannelPoliciesForUser", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
return
}
w.Write(js)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitDrafts() {
api.BaseRoutes.Drafts.Handle("", api.APISessionRequired(upsertDraft)).Methods("POST")
api.BaseRoutes.TeamForUser.Handle("/drafts", api.APISessionRequired(getDrafts)).Methods("GET")
api.BaseRoutes.ChannelForUser.Handle("/drafts/{thread_id:[A-Za-z0-9]+}", api.APISessionRequired(deleteDraft)).Methods("DELETE")
api.BaseRoutes.ChannelForUser.Handle("/drafts", api.APISessionRequired(deleteDraft)).Methods("DELETE")
}
func upsertDraft(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().ServiceSettings.AllowSyncedDrafts {
c.Err = model.NewAppError("upsertDraft", "api.drafts.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
var draft model.Draft
if jsonErr := json.NewDecoder(r.Body).Decode(&draft); jsonErr != nil {
c.SetInvalidParam("draft")
return
}
draft.DeleteAt = 0
draft.UserId = c.AppContext.Session().UserId
connectionID := r.Header.Get(model.ConnectionId)
hasPermission := false
if c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), draft.ChannelId, model.PermissionCreatePost) {
hasPermission = true
} else if channel, err := c.App.GetChannel(c.AppContext, draft.ChannelId); err == nil {
// Temporary permission check method until advanced permissions, please do not copy
if channel.Type == model.ChannelTypeOpen && c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionCreatePostPublic) {
hasPermission = true
}
}
if !hasPermission {
c.SetPermissionError(model.PermissionCreatePost)
return
}
dt, err := c.App.UpsertDraft(c.AppContext, &draft, connectionID)
if err != nil {
c.Err = err
return
}
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(dt); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func getDrafts(c *Context, w http.ResponseWriter, r *http.Request) {
if c.Err != nil {
return
}
if !*c.App.Config().ServiceSettings.AllowSyncedDrafts {
c.Err = model.NewAppError("getDrafts", "api.drafts.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
hasPermission := false
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
hasPermission = true
}
if !hasPermission {
c.SetPermissionError(model.PermissionCreatePost)
return
}
drafts, err := c.App.GetDraftsForUser(c.AppContext.Session().UserId, c.Params.TeamId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(drafts); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteDraft(c *Context, w http.ResponseWriter, r *http.Request) {
if c.Err != nil {
return
}
if !*c.App.Config().ServiceSettings.AllowSyncedDrafts {
c.Err = model.NewAppError("deleteDraft", "api.drafts.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
rootID := ""
connectionID := r.Header.Get(model.ConnectionId)
if c.Params.ThreadId != "" {
rootID = c.Params.ThreadId
}
userID := c.AppContext.Session().UserId
channelID := c.Params.ChannelId
draft, err := c.App.GetDraft(userID, channelID, rootID)
if err != nil {
switch {
case err.StatusCode == http.StatusNotFound:
// If the draft doesn't exist in the server, we don't need to delete.
ReturnStatusOK(w)
default:
c.Err = err
}
return
}
if c.AppContext.Session().UserId != draft.UserId {
c.SetPermissionError(model.PermissionDeletePost)
return
}
if _, err := c.App.DeleteDraft(userID, channelID, rootID, connectionID); err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitElasticsearch() {
api.BaseRoutes.Elasticsearch.Handle("/test", api.APISessionRequired(testElasticsearch)).Methods("POST")
api.BaseRoutes.Elasticsearch.Handle("/purge_indexes", api.APISessionRequired(purgeElasticsearchIndexes)).Methods("POST")
}
func testElasticsearch(c *Context, w http.ResponseWriter, r *http.Request) {
var cfg *model.Config
err := json.NewDecoder(r.Body).Decode(&cfg)
if err != nil {
c.Logger.Warn("Error decoding config.", mlog.Err(err))
}
if cfg == nil {
cfg = c.App.Config()
}
// we set BulkIndexingTimeWindowSeconds to a random value to avoid failing on the nil check
// TODO: remove this hack once we remove BulkIndexingTimeWindowSeconds from the config.
if cfg.ElasticsearchSettings.BulkIndexingTimeWindowSeconds == nil {
cfg.ElasticsearchSettings.BulkIndexingTimeWindowSeconds = model.NewInt(0)
}
if checkHasNilFields(&cfg.ElasticsearchSettings) {
c.Err = model.NewAppError("testElasticsearch", "api.elasticsearch.test_elasticsearch_settings_nil.app_error", nil, "", http.StatusBadRequest)
return
}
// PERMISSION_TEST_ELASTICSEARCH is an ancillary permission of PERMISSION_SYSCONSOLE_WRITE_ENVIRONMENT_ELASTICSEARCH,
// which should prevent read-only managers from password sniffing
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionTestElasticsearch) {
c.SetPermissionError(model.PermissionTestElasticsearch)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("testElasticsearch", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
if err := c.App.TestElasticsearch(cfg); err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
func purgeElasticsearchIndexes(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("purgeElasticsearchIndexes", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionPurgeElasticsearchIndexes) {
c.SetPermissionError(model.PermissionPurgeElasticsearchIndexes)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("purgeElasticsearchIndexes", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
if err := c.App.PurgeElasticsearchIndexes(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"io"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
EmojiMaxAutocompleteItems = 100
)
func (api *API) InitEmoji() {
api.BaseRoutes.Emojis.Handle("", api.APISessionRequired(createEmoji)).Methods("POST")
api.BaseRoutes.Emojis.Handle("", api.APISessionRequired(getEmojiList)).Methods("GET")
api.BaseRoutes.Emojis.Handle("/search", api.APISessionRequired(searchEmojis)).Methods("POST")
api.BaseRoutes.Emojis.Handle("/autocomplete", api.APISessionRequired(autocompleteEmojis)).Methods("GET")
api.BaseRoutes.Emoji.Handle("", api.APISessionRequired(deleteEmoji)).Methods("DELETE")
api.BaseRoutes.Emoji.Handle("", api.APISessionRequired(getEmoji)).Methods("GET")
api.BaseRoutes.EmojiByName.Handle("", api.APISessionRequired(getEmojiByName)).Methods("GET")
api.BaseRoutes.Emoji.Handle("/image", api.APISessionRequiredTrustRequester(getEmojiImage)).Methods("GET")
}
func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
defer io.Copy(io.Discard, r.Body)
if !*c.App.Config().ServiceSettings.EnableCustomEmoji {
c.Err = model.NewAppError("createEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
if r.ContentLength > app.MaxEmojiFileSize {
c.Err = model.NewAppError("createEmoji", "api.emoji.create.too_large.app_error", nil, "", http.StatusRequestEntityTooLarge)
return
}
if err := r.ParseMultipartForm(app.MaxEmojiFileSize); err != nil {
c.Err = model.NewAppError("createEmoji", "api.emoji.create.parse.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("createEmoji", audit.Fail)
defer c.LogAuditRec(auditRec)
// Allow any user with CREATE_EMOJIS permission at Team level to create emojis at system level
memberships, err := c.App.GetTeamMembersForUser(c.AppContext.Session().UserId, "", true)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateEmojis) {
hasPermission := false
for _, membership := range memberships {
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), membership.TeamId, model.PermissionCreateEmojis) {
hasPermission = true
break
}
}
if !hasPermission {
c.SetPermissionError(model.PermissionCreateEmojis)
return
}
}
m := r.MultipartForm
props := m.Value
if len(props["emoji"]) == 0 {
c.SetInvalidParam("emoji")
return
}
var emoji model.Emoji
if jsonErr := json.Unmarshal([]byte(props["emoji"][0]), &emoji); jsonErr != nil {
c.SetInvalidParam("emoji")
return
}
auditRec.AddEventResultState(&emoji)
auditRec.AddEventObjectType("emoji")
newEmoji, err := c.App.CreateEmoji(c.AppContext, c.AppContext.Session().UserId, &emoji, m)
if err != nil {
c.Err = err
return
}
auditRec.Success()
if err := json.NewEncoder(w).Encode(newEmoji); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getEmojiList(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().ServiceSettings.EnableCustomEmoji {
c.Err = model.NewAppError("getEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
sort := r.URL.Query().Get("sort")
if sort != "" && sort != model.EmojiSortByName {
c.SetInvalidURLParam("sort")
return
}
listEmoji, err := c.App.GetEmojiList(c.AppContext, c.Params.Page, c.Params.PerPage, sort)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(listEmoji); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireEmojiId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("deleteEmoji", audit.Fail)
defer c.LogAuditRec(auditRec)
emoji, err := c.App.GetEmoji(c.AppContext, c.Params.EmojiId)
if err != nil {
audit.AddEventParameter(auditRec, "emoji_id", c.Params.EmojiId)
c.Err = err
return
}
auditRec.AddEventPriorState(emoji)
auditRec.AddEventObjectType("emoji")
// Allow any user with DELETE_EMOJIS permission at Team level to delete emojis at system level
memberships, err := c.App.GetTeamMembersForUser(c.AppContext.Session().UserId, "", true)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionDeleteEmojis) {
hasPermission := false
for _, membership := range memberships {
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), membership.TeamId, model.PermissionDeleteEmojis) {
hasPermission = true
break
}
}
if !hasPermission {
c.SetPermissionError(model.PermissionDeleteEmojis)
return
}
}
if c.AppContext.Session().UserId != emoji.CreatorId {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionDeleteOthersEmojis) {
hasPermission := false
for _, membership := range memberships {
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), membership.TeamId, model.PermissionDeleteOthersEmojis) {
hasPermission = true
break
}
}
if !hasPermission {
c.SetPermissionError(model.PermissionDeleteOthersEmojis)
return
}
}
}
err = c.App.DeleteEmoji(c.AppContext, emoji)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireEmojiId()
if c.Err != nil {
return
}
if !*c.App.Config().ServiceSettings.EnableCustomEmoji {
c.Err = model.NewAppError("getEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
emoji, err := c.App.GetEmoji(c.AppContext, c.Params.EmojiId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(emoji); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getEmojiByName(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireEmojiName()
if c.Err != nil {
return
}
emoji, err := c.App.GetEmojiByName(c.AppContext, c.Params.EmojiName)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(emoji); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getEmojiImage(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireEmojiId()
if c.Err != nil {
return
}
if !*c.App.Config().ServiceSettings.EnableCustomEmoji {
c.Err = model.NewAppError("getEmojiImage", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
image, imageType, err := c.App.GetEmojiImage(c.AppContext, c.Params.EmojiId)
if err != nil {
c.Err = err
return
}
w.Header().Set("Content-Type", "image/"+imageType)
w.Header().Set("Cache-Control", "max-age=2592000, private")
w.Write(image)
}
func searchEmojis(c *Context, w http.ResponseWriter, r *http.Request) {
var emojiSearch model.EmojiSearch
if jsonErr := json.NewDecoder(r.Body).Decode(&emojiSearch); jsonErr != nil {
c.SetInvalidParamWithErr("term", jsonErr)
return
}
if emojiSearch.Term == "" {
c.SetInvalidParam("term")
return
}
emojis, err := c.App.SearchEmoji(c.AppContext, emojiSearch.Term, emojiSearch.PrefixOnly, web.PerPageMaximum)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(emojis); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func autocompleteEmojis(c *Context, w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
c.SetInvalidURLParam("name")
return
}
emojis, err := c.App.SearchEmoji(c.AppContext, name, true, EmojiMaxAutocompleteItems)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(emojis); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"path/filepath"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
)
func (api *API) InitExport() {
api.BaseRoutes.Exports.Handle("", api.APISessionRequired(listExports)).Methods("GET")
api.BaseRoutes.Export.Handle("", api.APISessionRequired(deleteExport)).Methods("DELETE")
api.BaseRoutes.Export.Handle("", api.APISessionRequired(downloadExport)).Methods("GET")
}
func listExports(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionManageSystem)
return
}
exports, appErr := c.App.ListExports()
if appErr != nil {
c.Err = appErr
return
}
data, err := json.Marshal(exports)
if err != nil {
c.Err = model.NewAppError("listImports", "app.export.marshal.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(data)
}
func deleteExport(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("deleteExport", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "export_name", c.Params.ExportName)
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if err := c.App.DeleteExport(c.Params.ExportName); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func downloadExport(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionManageSystem)
return
}
filePath := filepath.Join(*c.App.Config().ExportSettings.Directory, c.Params.ExportName)
if ok, err := c.App.FileExists(filePath); err != nil {
c.Err = err
return
} else if !ok {
c.Err = model.NewAppError("downloadExport", "api.export.export_not_found.app_error", nil, "", http.StatusNotFound)
return
}
file, err := c.App.FileReader(filePath)
if err != nil {
c.Err = err
return
}
defer file.Close()
w.Header().Set("Content-Type", "application/zip")
http.ServeContent(w, r, c.Params.ExportName, time.Time{}, file)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitExportLocal() {
api.BaseRoutes.Exports.Handle("", api.APILocal(listExports)).Methods("GET")
api.BaseRoutes.Export.Handle("", api.APILocal(deleteExport)).Methods("DELETE")
api.BaseRoutes.Export.Handle("", api.APILocal(downloadExport)).Methods("GET")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"crypto/subtle"
"encoding/json"
"io"
"mime"
"mime/multipart"
"net/http"
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/web"
)
const (
FileTeamId = "noteam"
PreviewImageType = "image/jpeg"
ThumbnailImageType = "image/jpeg"
)
const maxMultipartFormDataBytes = 10 * 1024 // 10Kb
func (api *API) InitFile() {
api.BaseRoutes.Files.Handle("", api.APISessionRequired(uploadFileStream)).Methods("POST")
api.BaseRoutes.Files.Handle("/search", api.APISessionRequired(searchFilesForUser)).Methods("POST")
api.BaseRoutes.File.Handle("", api.APISessionRequiredTrustRequester(getFile)).Methods("GET")
api.BaseRoutes.File.Handle("/thumbnail", api.APISessionRequiredTrustRequester(getFileThumbnail)).Methods("GET")
api.BaseRoutes.File.Handle("/link", api.APISessionRequired(getFileLink)).Methods("GET")
api.BaseRoutes.File.Handle("/preview", api.APISessionRequiredTrustRequester(getFilePreview)).Methods("GET")
api.BaseRoutes.File.Handle("/info", api.APISessionRequired(getFileInfo)).Methods("GET")
api.BaseRoutes.Team.Handle("/files/search", api.APISessionRequiredDisableWhenBusy(searchFilesInTeam)).Methods("POST")
api.BaseRoutes.PublicFile.Handle("", api.APIHandler(getPublicFile)).Methods("GET")
}
func parseMultipartRequestHeader(req *http.Request) (boundary string, err error) {
v := req.Header.Get("Content-Type")
if v == "" {
return "", http.ErrNotMultipart
}
d, params, err := mime.ParseMediaType(v)
if err != nil || d != "multipart/form-data" {
return "", http.ErrNotMultipart
}
boundary, ok := params["boundary"]
if !ok {
return "", http.ErrMissingBoundary
}
return boundary, nil
}
func multipartReader(req *http.Request, stream io.Reader) (*multipart.Reader, error) {
boundary, err := parseMultipartRequestHeader(req)
if err != nil {
return nil, err
}
if stream != nil {
return multipart.NewReader(stream, boundary), nil
}
return multipart.NewReader(req.Body, boundary), nil
}
func uploadFileStream(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().FileSettings.EnableFileAttachments {
c.Err = model.NewAppError("uploadFileStream",
"api.file.attachments.disabled.app_error",
nil, "", http.StatusForbidden)
return
}
// Parse the post as a regular form (in practice, use the URL values
// since we never expect a real application/x-www-form-urlencoded
// form).
if r.Form == nil {
err := r.ParseForm()
if err != nil {
c.Err = model.NewAppError("uploadFileStream",
"api.file.upload_file.read_request.app_error",
nil, err.Error(), http.StatusBadRequest)
return
}
}
if r.ContentLength == 0 {
c.Err = model.NewAppError("uploadFileStream",
"api.file.upload_file.read_request.app_error",
nil, "Content-Length should not be 0", http.StatusBadRequest)
return
}
timestamp := time.Now()
var fileUploadResponse *model.FileUploadResponse
_, err := parseMultipartRequestHeader(r)
switch err {
case nil:
fileUploadResponse = uploadFileMultipart(c, r, nil, timestamp)
case http.ErrNotMultipart:
fileUploadResponse = uploadFileSimple(c, r, timestamp)
default:
c.Err = model.NewAppError("uploadFileStream",
"api.file.upload_file.read_request.app_error",
nil, err.Error(), http.StatusBadRequest)
}
if c.Err != nil {
return
}
// Write the response values to the output upon return
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(fileUploadResponse); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// uploadFileSimple uploads a file from a simple POST with the file in the request body
func uploadFileSimple(c *Context, r *http.Request, timestamp time.Time) *model.FileUploadResponse {
// Simple POST with the file in the body and all metadata in the args.
c.RequireChannelId()
c.RequireFilename()
if c.Err != nil {
return nil
}
auditRec := c.MakeAuditRecord("uploadFileSimple", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionUploadFile) {
c.SetPermissionError(model.PermissionUploadFile)
return nil
}
clientId := r.Form.Get("client_id")
audit.AddEventParameter(auditRec, "client_id", clientId)
info, appErr := c.App.UploadFileX(c.AppContext, c.Params.ChannelId, c.Params.Filename, r.Body,
app.UploadFileSetTeamId(FileTeamId),
app.UploadFileSetUserId(c.AppContext.Session().UserId),
app.UploadFileSetTimestamp(timestamp),
app.UploadFileSetContentLength(r.ContentLength),
app.UploadFileSetClientId(clientId))
if appErr != nil {
c.Err = appErr
return nil
}
audit.AddEventParameterAuditable(auditRec, "file", info)
fileUploadResponse := &model.FileUploadResponse{
FileInfos: []*model.FileInfo{info},
}
if clientId != "" {
fileUploadResponse.ClientIds = []string{clientId}
}
auditRec.Success()
return fileUploadResponse
}
// uploadFileMultipart parses and uploads file(s) from a mime/multipart
// request. It pre-buffers up to the first part which is either the (a)
// `channel_id` value, or (b) a file. Then in case of (a) it re-processes the
// entire message recursively calling itself in stream mode. In case of (b) it
// calls to uploadFileMultipartLegacy for legacy support
func uploadFileMultipart(c *Context, r *http.Request, asStream io.Reader, timestamp time.Time) *model.FileUploadResponse {
expectClientIds := true
var clientIds []string
resp := model.FileUploadResponse{
FileInfos: []*model.FileInfo{},
ClientIds: []string{},
}
var buf *bytes.Buffer
var mr *multipart.Reader
var err error
if asStream == nil {
// We need to buffer until we get the channel_id, or the first file.
buf = &bytes.Buffer{}
mr, err = multipartReader(r, io.TeeReader(r.Body, buf))
} else {
mr, err = multipartReader(r, asStream)
}
if err != nil {
c.Err = model.NewAppError("uploadFileMultipart",
"api.file.upload_file.read_request.app_error",
nil, err.Error(), http.StatusBadRequest)
return nil
}
nFiles := 0
NextPart:
for {
part, err := mr.NextPart()
if err == io.EOF {
break
}
if err != nil {
c.Err = model.NewAppError("uploadFileMultipart",
"api.file.upload_file.read_request.app_error",
nil, err.Error(), http.StatusBadRequest)
return nil
}
// Parse any form fields in the multipart.
formname := part.FormName()
if formname == "" {
continue
}
filename := part.FileName()
if filename == "" {
var b bytes.Buffer
_, err = io.CopyN(&b, part, maxMultipartFormDataBytes)
if err != nil && err != io.EOF {
c.Err = model.NewAppError("uploadFileMultipart",
"api.file.upload_file.read_form_value.app_error",
map[string]any{"Formname": formname},
err.Error(), http.StatusBadRequest)
return nil
}
v := b.String()
switch formname {
case "channel_id":
if c.Params.ChannelId != "" && c.Params.ChannelId != v {
c.Err = model.NewAppError("uploadFileMultipart",
"api.file.upload_file.multiple_channel_ids.app_error",
nil, "", http.StatusBadRequest)
return nil
}
if v != "" {
c.Params.ChannelId = v
}
// Got channel_id, re-process the entire post
// in the streaming mode.
if asStream == nil {
return uploadFileMultipart(c, r, io.MultiReader(buf, r.Body), timestamp)
}
case "client_ids":
if !expectClientIds {
c.SetInvalidParam("client_ids")
return nil
}
clientIds = append(clientIds, v)
default:
c.SetInvalidParam(formname)
return nil
}
continue NextPart
}
// A file part.
if c.Params.ChannelId == "" && asStream == nil {
// Got file before channel_id, fall back to legacy buffered mode
mr, err = multipartReader(r, io.MultiReader(buf, r.Body))
if err != nil {
c.Err = model.NewAppError("uploadFileMultipart",
"api.file.upload_file.read_request.app_error",
nil, err.Error(), http.StatusBadRequest)
return nil
}
return uploadFileMultipartLegacy(c, mr, timestamp)
}
c.RequireChannelId()
if c.Err != nil {
return nil
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionUploadFile) {
c.SetPermissionError(model.PermissionUploadFile)
return nil
}
// If there's no clientIds when the first file comes, expect
// none later.
if nFiles == 0 && len(clientIds) == 0 {
expectClientIds = false
}
// Must have a exactly one client ID for each file.
clientId := ""
if expectClientIds {
if nFiles >= len(clientIds) {
c.SetInvalidParam("client_ids")
return nil
}
clientId = clientIds[nFiles]
}
auditRec := c.MakeAuditRecord("uploadFileMultipart", audit.Fail)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
audit.AddEventParameter(auditRec, "client_id", clientId)
info, appErr := c.App.UploadFileX(c.AppContext, c.Params.ChannelId, filename, part,
app.UploadFileSetTeamId(FileTeamId),
app.UploadFileSetUserId(c.AppContext.Session().UserId),
app.UploadFileSetTimestamp(timestamp),
app.UploadFileSetContentLength(-1),
app.UploadFileSetClientId(clientId))
if appErr != nil {
c.Err = appErr
c.LogAuditRec(auditRec)
return nil
}
audit.AddEventParameterAuditable(auditRec, "file", info)
auditRec.Success()
c.LogAuditRec(auditRec)
// add to the response
resp.FileInfos = append(resp.FileInfos, info)
if expectClientIds {
resp.ClientIds = append(resp.ClientIds, clientId)
}
nFiles++
}
// Verify that the number of ClientIds matched the number of files.
if expectClientIds && len(clientIds) != nFiles {
c.Err = model.NewAppError("uploadFileMultipart",
"api.file.upload_file.incorrect_number_of_client_ids.app_error",
map[string]any{"NumClientIds": len(clientIds), "NumFiles": nFiles},
"", http.StatusBadRequest)
return nil
}
return &resp
}
// uploadFileMultipartLegacy reads, buffers, and then uploads the message,
// borrowing from http.ParseMultipartForm. If successful it returns a
// *model.FileUploadResponse filled in with the individual model.FileInfo's.
func uploadFileMultipartLegacy(c *Context, mr *multipart.Reader,
timestamp time.Time) *model.FileUploadResponse {
// Parse the entire form.
form, err := mr.ReadForm(*c.App.Config().FileSettings.MaxFileSize)
if err != nil {
c.Err = model.NewAppError("uploadFileMultipartLegacy",
"api.file.upload_file.read_request.app_error",
nil, err.Error(), http.StatusInternalServerError)
return nil
}
// get and validate the channel Id, permission to upload there.
if len(form.Value["channel_id"]) == 0 {
c.SetInvalidParam("channel_id")
return nil
}
channelId := form.Value["channel_id"][0]
c.Params.ChannelId = channelId
c.RequireChannelId()
if c.Err != nil {
return nil
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelId, model.PermissionUploadFile) {
c.SetPermissionError(model.PermissionUploadFile)
return nil
}
// Check that we have either no client IDs, or one per file.
clientIds := form.Value["client_ids"]
fileHeaders := form.File["files"]
if len(clientIds) != 0 && len(clientIds) != len(fileHeaders) {
c.Err = model.NewAppError("uploadFilesMultipartBuffered",
"api.file.upload_file.incorrect_number_of_client_ids.app_error",
map[string]any{"NumClientIds": len(clientIds), "NumFiles": len(fileHeaders)},
"", http.StatusBadRequest)
return nil
}
resp := model.FileUploadResponse{
FileInfos: []*model.FileInfo{},
ClientIds: []string{},
}
for i, fileHeader := range fileHeaders {
f, err := fileHeader.Open()
if err != nil {
c.Err = model.NewAppError("uploadFileMultipartLegacy",
"api.file.upload_file.read_request.app_error",
nil, err.Error(), http.StatusBadRequest)
return nil
}
clientId := ""
if len(clientIds) > 0 {
clientId = clientIds[i]
}
auditRec := c.MakeAuditRecord("uploadFileMultipartLegacy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "channel_id", channelId)
audit.AddEventParameter(auditRec, "client_id", clientId)
info, appErr := c.App.UploadFileX(c.AppContext, c.Params.ChannelId, fileHeader.Filename, f,
app.UploadFileSetTeamId(FileTeamId),
app.UploadFileSetUserId(c.AppContext.Session().UserId),
app.UploadFileSetTimestamp(timestamp),
app.UploadFileSetContentLength(-1),
app.UploadFileSetClientId(clientId))
f.Close()
if appErr != nil {
c.Err = appErr
c.LogAuditRec(auditRec)
return nil
}
audit.AddEventParameterAuditable(auditRec, "file", info)
auditRec.Success()
c.LogAuditRec(auditRec)
resp.FileInfos = append(resp.FileInfos, info)
if clientId != "" {
resp.ClientIds = append(resp.ClientIds, clientId)
}
}
return &resp
}
func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireFileId()
if c.Err != nil {
return
}
forceDownload, _ := strconv.ParseBool(r.URL.Query().Get("download"))
auditRec := c.MakeAuditRecord("getFile", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "force_download", forceDownload)
info, err := c.App.GetFileInfo(c.Params.FileId)
if err != nil {
c.Err = err
setInaccessibleFileHeader(w, err)
return
}
audit.AddEventParameterAuditable(auditRec, "file", info)
if info.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), info.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
fileReader, err := c.App.FileReader(info.Path)
if err != nil {
c.Err = err
c.Err.StatusCode = http.StatusNotFound
return
}
defer fileReader.Close()
auditRec.Success()
web.WriteFileResponse(info.Name, info.MimeType, info.Size, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, forceDownload, w, r)
}
func getFileThumbnail(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireFileId()
if c.Err != nil {
return
}
forceDownload, _ := strconv.ParseBool(r.URL.Query().Get("download"))
info, err := c.App.GetFileInfo(c.Params.FileId)
if err != nil {
c.Err = err
setInaccessibleFileHeader(w, err)
return
}
if info.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), info.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if info.ThumbnailPath == "" {
c.Err = model.NewAppError("getFileThumbnail", "api.file.get_file_thumbnail.no_thumbnail.app_error", nil, "file_id="+info.Id, http.StatusBadRequest)
return
}
fileReader, err := c.App.FileReader(info.ThumbnailPath)
if err != nil {
c.Err = err
c.Err.StatusCode = http.StatusNotFound
return
}
defer fileReader.Close()
web.WriteFileResponse(info.Name, ThumbnailImageType, 0, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, forceDownload, w, r)
}
func getFileLink(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireFileId()
if c.Err != nil {
return
}
if !*c.App.Config().FileSettings.EnablePublicLink {
c.Err = model.NewAppError("getPublicLink", "api.file.get_public_link.disabled.app_error", nil, "", http.StatusForbidden)
return
}
auditRec := c.MakeAuditRecord("getFileLink", audit.Fail)
defer c.LogAuditRec(auditRec)
info, err := c.App.GetFileInfo(c.Params.FileId)
if err != nil {
c.Err = err
setInaccessibleFileHeader(w, err)
return
}
audit.AddEventParameterAuditable(auditRec, "file", info)
if info.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), info.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if info.PostId == "" {
c.Err = model.NewAppError("getPublicLink", "api.file.get_public_link.no_post.app_error", nil, "file_id="+info.Id, http.StatusBadRequest)
return
}
resp := make(map[string]string)
link := c.App.GeneratePublicLink(c.GetSiteURLHeader(), info)
resp["link"] = link
auditRec.Success()
w.Write([]byte(model.MapToJSON(resp)))
}
func getFilePreview(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireFileId()
if c.Err != nil {
return
}
forceDownload, _ := strconv.ParseBool(r.URL.Query().Get("download"))
info, err := c.App.GetFileInfo(c.Params.FileId)
if err != nil {
c.Err = err
setInaccessibleFileHeader(w, err)
return
}
if info.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), info.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if info.PreviewPath == "" {
c.Err = model.NewAppError("getFilePreview", "api.file.get_file_preview.no_preview.app_error", nil, "file_id="+info.Id, http.StatusBadRequest)
return
}
fileReader, err := c.App.FileReader(info.PreviewPath)
if err != nil {
c.Err = err
c.Err.StatusCode = http.StatusNotFound
return
}
defer fileReader.Close()
web.WriteFileResponse(info.Name, PreviewImageType, 0, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, forceDownload, w, r)
}
func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireFileId()
if c.Err != nil {
return
}
info, err := c.App.GetFileInfo(c.Params.FileId)
if err != nil {
c.Err = err
setInaccessibleFileHeader(w, err)
return
}
if info.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), info.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
w.Header().Set("Cache-Control", "max-age=2592000, private")
if err := json.NewEncoder(w).Encode(info); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireFileId()
if c.Err != nil {
return
}
if !*c.App.Config().FileSettings.EnablePublicLink {
c.Err = model.NewAppError("getPublicFile", "api.file.get_public_link.disabled.app_error", nil, "", http.StatusForbidden)
return
}
info, err := c.App.GetFileInfo(c.Params.FileId)
if err != nil {
c.Err = err
setInaccessibleFileHeader(w, err)
return
}
hash := r.URL.Query().Get("h")
if hash == "" {
c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest)
utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey())
return
}
if subtle.ConstantTimeCompare([]byte(hash), []byte(app.GeneratePublicLinkHash(info.Id, *c.App.Config().FileSettings.PublicLinkSalt))) != 1 {
c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest)
utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey())
return
}
fileReader, err := c.App.FileReader(info.Path)
if err != nil {
c.Err = err
c.Err.StatusCode = http.StatusNotFound
return
}
defer fileReader.Close()
web.WriteFileResponse(info.Name, info.MimeType, info.Size, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, false, w, r)
}
func searchFilesInTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
searchFiles(c, w, r, c.Params.TeamId)
}
func searchFilesForUser(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Config().FeatureFlags.CommandPalette {
searchFiles(c, w, r, "")
}
}
func searchFiles(c *Context, w http.ResponseWriter, r *http.Request, teamID string) {
var params model.SearchParameter
jsonErr := json.NewDecoder(r.Body).Decode(¶ms)
if jsonErr != nil {
c.Err = model.NewAppError("searchFiles", "api.post.search_files.invalid_body.app_error", nil, "", http.StatusBadRequest).Wrap(jsonErr)
return
}
if params.Terms == nil || *params.Terms == "" {
c.SetInvalidParam("terms")
return
}
terms := *params.Terms
timeZoneOffset := 0
if params.TimeZoneOffset != nil {
timeZoneOffset = *params.TimeZoneOffset
}
isOrSearch := false
if params.IsOrSearch != nil {
isOrSearch = *params.IsOrSearch
}
page := 0
if params.Page != nil {
page = *params.Page
}
perPage := 60
if params.PerPage != nil {
perPage = *params.PerPage
}
includeDeletedChannels := false
if params.IncludeDeletedChannels != nil {
includeDeletedChannels = *params.IncludeDeletedChannels
}
modifier := ""
if params.Modifier != nil {
modifier = *params.Modifier
}
if modifier != "" && modifier != model.ModifierFiles && modifier != model.ModifierMessages {
c.SetInvalidParam("modifier")
return
}
startTime := time.Now()
results, err := c.App.SearchFilesInTeamForUser(c.AppContext, terms, c.AppContext.Session().UserId, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, modifier)
elapsedTime := float64(time.Since(startTime)) / float64(time.Second)
metrics := c.App.Metrics()
if metrics != nil {
metrics.IncrementFilesSearchCounter()
metrics.ObserveFilesSearchDuration(elapsedTime)
}
if err != nil {
c.Err = err
return
}
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if err := json.NewEncoder(w).Encode(results); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func setInaccessibleFileHeader(w http.ResponseWriter, appErr *model.AppError) {
// File is inaccessible due to cloud plan's limit.
if appErr.Id == "app.file.cloud.get.app_error" {
w.Header().Set(model.HeaderFirstInaccessibleFileTime, "1")
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
_ "embed"
"encoding/json"
"net/http"
"github.com/graph-gophers/dataloader/v6"
graphql "github.com/graph-gophers/graphql-go"
gqlerrors "github.com/graph-gophers/graphql-go/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type graphQLInput struct {
Query string `json:"query"`
OperationName string `json:"operationName"`
Variables map[string]any `json:"variables"`
}
// Unique type to hold our context.
type ctxKey int
const (
webCtx ctxKey = 0
rolesLoaderCtx ctxKey = 1
channelsLoaderCtx ctxKey = 2
teamsLoaderCtx ctxKey = 3
usersLoaderCtx ctxKey = 4
)
const loaderBatchCapacity = web.PerPageMaximum
//go:embed schema.graphqls
var schemaRaw string
func (api *API) InitGraphQL() error {
// Guard with a feature flag.
if !api.srv.Config().FeatureFlags.GraphQL {
return nil
}
var err error
opts := []graphql.SchemaOpt{
graphql.UseFieldResolvers(),
graphql.Logger(mlog.NewGraphQLLogger(api.srv.Log())),
graphql.MaxParallelism(loaderBatchCapacity), // This is dangerous if the query
// uses any non-dataloader backed object. So we need to be a bit careful here.
}
if isProd() {
opts = append(opts,
// MaxDepth cannot be moved as a general param
// because otherwise introspection also doesn't work
// with just a depth of 4.
graphql.MaxDepth(4),
graphql.DisableIntrospection(),
)
}
api.schema, err = graphql.ParseSchema(schemaRaw, &resolver{}, opts...)
if err != nil {
return err
}
api.BaseRoutes.APIRoot5.Handle("/graphql", api.APIHandlerTrustRequester(graphiQL)).Methods("GET")
api.BaseRoutes.APIRoot5.Handle("/graphql", api.APISessionRequired(api.graphQL)).Methods("POST")
return nil
}
func (api *API) graphQL(c *Context, w http.ResponseWriter, r *http.Request) {
var response *graphql.Response
defer func() {
if response != nil {
if err := json.NewEncoder(w).Encode(response); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
}()
// Limit bodies to 100KiB.
// We need to enforce a lower limit than the file upload size,
// to prevent the library doing unnecessary parsing.
r.Body = http.MaxBytesReader(w, r.Body, 102400)
var params graphQLInput
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
err2 := gqlerrors.Errorf("invalid request body: %v", err)
response = &graphql.Response{Errors: []*gqlerrors.QueryError{err2}}
return
}
if isProd() && params.OperationName == "" {
err2 := gqlerrors.Errorf("operation name not passed")
response = &graphql.Response{Errors: []*gqlerrors.QueryError{err2}}
return
}
c.GraphQLOperationName = params.OperationName
// Populate the context with required info.
reqCtx := r.Context()
reqCtx = context.WithValue(reqCtx, webCtx, c)
rolesLoader := dataloader.NewBatchedLoader(graphQLRolesLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
reqCtx = context.WithValue(reqCtx, rolesLoaderCtx, rolesLoader)
channelsLoader := dataloader.NewBatchedLoader(graphQLChannelsLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
reqCtx = context.WithValue(reqCtx, channelsLoaderCtx, channelsLoader)
teamsLoader := dataloader.NewBatchedLoader(graphQLTeamsLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
reqCtx = context.WithValue(reqCtx, teamsLoaderCtx, teamsLoader)
usersLoader := dataloader.NewBatchedLoader(graphQLUsersLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
reqCtx = context.WithValue(reqCtx, usersLoaderCtx, usersLoader)
response = api.schema.Exec(reqCtx,
params.Query,
params.OperationName,
params.Variables)
if len(response.Errors) > 0 {
logFunc := mlog.Error
for _, gqlErr := range response.Errors {
if gqlErr.Err != nil {
if appErr, ok := gqlErr.Err.(*model.AppError); ok && appErr.StatusCode < http.StatusInternalServerError {
logFunc = mlog.Debug
break
}
}
}
logFunc("Error executing request", mlog.String("operation", params.OperationName),
mlog.Array("errors", response.Errors))
}
}
func graphiQL(c *Context, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write(graphiqlPage)
}
var graphiqlPage = []byte(`
<!DOCTYPE html>
<html>
<head>
<title>GraphiQL editor | Mattermost</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.css" integrity="sha256-gSgd+on4bTXigueyd/NSRNAy4cBY42RAVNaXnQDjOW8=" crossorigin="anonymous"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js" integrity="sha256-OI3N9zCKabDov2rZFzl8lJUXCcP7EmsGcGoP6DMXQCo=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/2.0.3/fetch.min.js" integrity="sha256-aB35laj7IZhLTx58xw/Gm1EKOoJJKZt6RY+bH1ReHxs=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.production.min.js" integrity="sha256-wouRkivKKXA3y6AuyFwcDcF50alCNV8LbghfYCH6Z98=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.production.min.js" integrity="sha256-9hrJxD4IQsWHdNpzLkJKYGiY/SEZFJJSUqyeZPNKd8g=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.js" integrity="sha256-oeWyQyKKUurcnbFRsfeSgrdOpXXiRYopnPjTVZ+6UmI=" crossorigin="anonymous"></script>
</head>
<body style="width: 100%; height: 100%; margin: 0; overflow: hidden;">
<div id="graphiql" style="height: 100vh;">Loading...</div>
<script>
function graphQLFetcher(graphQLParams) {
return fetch("/api/v5/graphql", {
method: "post",
body: JSON.stringify(graphQLParams),
credentials: "include",
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
}).then(function (response) {
return response.text();
}).then(function (responseBody) {
try {
return JSON.parse(responseBody);
} catch (error) {
return responseBody;
}
});
}
ReactDOM.render(
React.createElement(GraphiQL, {fetcher: graphQLFetcher}),
document.getElementById("graphiql")
);
</script>
</body>
</html>
`)
// isProd is a helper function to apply prod-specific graphQL validations.
func isProd() bool {
return model.BuildNumber != "dev"
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"io"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
)
// graphQLClient is an internal test client to run the tests.
// When the API matures, we will expose it to the model package.
type graphQLClient struct {
URL string // The location of the server, for example "http://localhost:8065"
APIURL string // The api location of the server, for example "http://localhost:8065/api/v4"
httpClient *http.Client // The http client
authToken string
authType string
httpHeader map[string]string // Headers to be copied over for each request
}
func newGraphQLClient(url string) *graphQLClient {
url = strings.TrimRight(url, "/")
return &graphQLClient{url, url + model.APIURLSuffix, &http.Client{}, "", "", map[string]string{}}
}
func (c *graphQLClient) login(loginId string, password string) (*model.User, *model.Response, error) {
m := make(map[string]string)
m["login_id"] = loginId
m["password"] = password
r, err := c.doAPIRequest(http.MethodPost, c.APIURL+"/users/login", strings.NewReader(model.MapToJSON(m)), map[string]string{model.HeaderEtagClient: ""})
if err != nil {
return nil, model.BuildResponse(r), err
}
defer closeBody(r)
c.authToken = r.Header.Get(model.HeaderToken)
c.authType = model.HeaderBearer
var user model.User
if jsonErr := json.NewDecoder(r.Body).Decode(&user); jsonErr != nil {
return nil, nil, model.NewAppError("login", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
return &user, model.BuildResponse(r), nil
}
func (c *graphQLClient) doAPIRequest(method, url string, data io.Reader, headers map[string]string) (*http.Response, error) {
rq, err := c.prepareRequest(method, url, data, headers)
if err != nil {
return nil, err
}
rp, err := c.httpClient.Do(rq)
if err != nil {
return rp, err
}
return rp, nil
}
func (c *graphQLClient) prepareRequest(method, url string, data io.Reader, headers map[string]string) (*http.Request, error) {
rq, err := http.NewRequest(method, url, data)
if err != nil {
return nil, err
}
for k, v := range headers {
rq.Header.Set(k, v)
}
if c.authToken != "" {
rq.Header.Set(model.HeaderAuth, c.authType+" "+c.authToken)
}
if c.httpHeader != nil && len(c.httpHeader) > 0 {
for k, v := range c.httpHeader {
rq.Header.Set(k, v)
}
}
return rq, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
)
func (api *API) InitGroup() {
// GET /api/v4/groups
api.BaseRoutes.Groups.Handle("", api.APISessionRequired(getGroups)).Methods("GET")
// POST /api/v4/groups
api.BaseRoutes.Groups.Handle("", api.APISessionRequired(createGroup)).Methods("POST")
// GET /api/v4/groups/:group_id
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}",
api.APISessionRequired(getGroup)).Methods("GET")
// PUT /api/v4/groups/:group_id/patch
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/patch",
api.APISessionRequired(patchGroup)).Methods("PUT")
// POST /api/v4/groups/:group_id/teams/:team_id/link
// POST /api/v4/groups/:group_id/channels/:channel_id/link
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}/link",
api.APISessionRequired(linkGroupSyncable)).Methods("POST")
// DELETE /api/v4/groups/:group_id/teams/:team_id/link
// DELETE /api/v4/groups/:group_id/channels/:channel_id/link
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}/link",
api.APISessionRequired(unlinkGroupSyncable)).Methods("DELETE")
// GET /api/v4/groups/:group_id/teams/:team_id
// GET /api/v4/groups/:group_id/channels/:channel_id
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}",
api.APISessionRequired(getGroupSyncable)).Methods("GET")
// GET /api/v4/groups/:group_id/teams
// GET /api/v4/groups/:group_id/channels
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}",
api.APISessionRequired(getGroupSyncables)).Methods("GET")
// PUT /api/v4/groups/:group_id/teams/:team_id/patch
// PUT /api/v4/groups/:group_id/channels/:channel_id/patch
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}/patch",
api.APISessionRequired(patchGroupSyncable)).Methods("PUT")
// GET /api/v4/groups/:group_id/stats
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/stats",
api.APISessionRequired(getGroupStats)).Methods("GET")
// GET /api/v4/groups/:group_id/members
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members",
api.APISessionRequired(getGroupMembers)).Methods("GET")
// GET /api/v4/users/:user_id/groups
api.BaseRoutes.Users.Handle("/{user_id:[A-Za-z0-9]+}/groups",
api.APISessionRequired(getGroupsByUserId)).Methods("GET")
// GET /api/v4/channels/:channel_id/groups
api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/groups",
api.APISessionRequired(getGroupsByChannel)).Methods("GET")
// GET /api/v4/teams/:team_id/groups
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups",
api.APISessionRequired(getGroupsByTeam)).Methods("GET")
// GET /api/v4/teams/:team_id/groups_by_channels
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups_by_channels",
api.APISessionRequired(getGroupsAssociatedToChannelsByTeam)).Methods("GET")
// DELETE /api/v4/groups/:group_id
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}",
api.APISessionRequired(deleteGroup)).Methods("DELETE")
// GET /api/v4/groups/:group_id
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/restore",
api.APISessionRequired(restoreGroup)).Methods("POST")
// POST /api/v4/groups/:group_id/members
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members",
api.APISessionRequired(addGroupMembers)).Methods("POST")
// DELETE /api/v4/groups/:group_id/members
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members",
api.APISessionRequired(deleteGroupMembers)).Methods("DELETE")
}
func getGroup(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
group, appErr := c.App.GetGroup(c.Params.GroupId, &model.GetGroupOpts{
IncludeMemberCount: c.Params.IncludeMemberCount,
}, restrictions)
if appErr != nil {
c.Err = appErr
return
}
if group.Source == model.GroupSourceLdap {
if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionSysconsoleReadUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups)
return
}
}
if appErr := licensedAndConfiguredForGroupBySource(c.App, group.Source); appErr != nil {
appErr.Where = "Api4.getGroup"
c.Err = appErr
return
}
b, err := json.Marshal(group)
if err != nil {
c.Err = model.NewAppError("Api4.getGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func createGroup(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
var group *model.GroupWithUserIds
if err := json.NewDecoder(r.Body).Decode(&group); err != nil {
c.SetInvalidParamWithErr("group", err)
return
}
if group.Source != model.GroupSourceCustom {
c.Err = model.NewAppError("createGroup", "app.group.crud_permission", nil, "", http.StatusBadRequest)
return
}
if appErr := licensedAndConfiguredForGroupBySource(c.App, group.Source); appErr != nil {
appErr.Where = "Api4.createGroup"
c.Err = appErr
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateCustomGroup) {
c.SetPermissionError(model.PermissionCreateCustomGroup)
return
}
if !group.AllowReference {
c.Err = model.NewAppError("createGroup", "api.custom_groups.must_be_referenceable", nil, "", http.StatusBadRequest)
return
}
if group.GetRemoteId() != "" {
c.Err = model.NewAppError("createGroup", "api.custom_groups.no_remote_id", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("createGroup", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "group", group)
newGroup, appErr := c.App.CreateGroupWithUserIds(group)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(newGroup)
auditRec.AddEventObjectType("group")
js, err := json.Marshal(newGroup)
if err != nil {
c.Err = model.NewAppError("createGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.WriteHeader(http.StatusCreated)
w.Write(js)
}
func patchGroup(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
group, appErr := c.App.GetGroup(c.Params.GroupId, nil, nil)
if appErr != nil {
c.Err = appErr
return
}
appErr = licensedAndConfiguredForGroupBySource(c.App, group.Source)
if appErr != nil {
appErr.Where = "Api4.patchGroup"
c.Err = appErr
return
}
var requiredPermission *model.Permission
if group.Source == model.GroupSourceCustom {
requiredPermission = model.PermissionEditCustomGroup
} else {
requiredPermission = model.PermissionSysconsoleWriteUserManagementGroups
}
if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, requiredPermission) {
c.SetPermissionError(requiredPermission)
return
}
var groupPatch model.GroupPatch
if err := json.NewDecoder(r.Body).Decode(&groupPatch); err != nil {
c.SetInvalidParamWithErr("group", err)
return
}
if group.Source == model.GroupSourceCustom && groupPatch.AllowReference != nil && !*groupPatch.AllowReference {
c.Err = model.NewAppError("Api4.patchGroup", "api.custom_groups.must_be_referenceable", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("patchGroup", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "group", group)
if groupPatch.AllowReference != nil && *groupPatch.AllowReference {
if groupPatch.Name == nil {
tmp := strings.ReplaceAll(strings.ToLower(group.DisplayName), " ", "-")
groupPatch.Name = &tmp
} else {
if *groupPatch.Name == model.UserNotifyAll || *groupPatch.Name == model.ChannelMentionsNotifyProp || *groupPatch.Name == model.UserNotifyHere {
c.Err = model.NewAppError("Api4.patchGroup", "api.ldap_groups.existing_reserved_name_error", nil, "", http.StatusBadRequest)
return
}
//check if a user already has this group name
user, _ := c.App.GetUserByUsername(*groupPatch.Name)
if user != nil {
c.Err = model.NewAppError("Api4.patchGroup", "api.ldap_groups.existing_user_name_error", nil, "", http.StatusBadRequest)
return
}
//check if a mentionable group already has this name
searchOpts := model.GroupSearchOpts{
FilterAllowReference: true,
}
existingGroup, _ := c.App.GetGroupByName(*groupPatch.Name, searchOpts)
if existingGroup != nil {
c.Err = model.NewAppError("Api4.patchGroup", "api.ldap_groups.existing_group_name_error", nil, "", http.StatusBadRequest)
return
}
}
}
group.Patch(&groupPatch)
group, appErr = c.App.UpdateGroup(group)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(group)
auditRec.AddEventObjectType("group")
b, err := json.Marshal(group)
if err != nil {
c.Err = model.NewAppError("Api4.patchGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(b)
}
func linkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
c.RequireSyncableId()
if c.Err != nil {
return
}
syncableID := c.Params.SyncableId
c.RequireSyncableType()
if c.Err != nil {
return
}
syncableType := c.Params.SyncableType
body, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.createGroupSyncable", "api.io_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
group, appErr := c.App.GetGroup(c.Params.GroupId, nil, nil)
if appErr != nil {
c.Err = appErr
return
}
if group.Source != model.GroupSourceLdap {
c.Err = model.NewAppError("Api4.linkGroupSyncable", "app.group.crud_permission", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("linkGroupSyncable", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "group_id", c.Params.GroupId)
audit.AddEventParameter(auditRec, "syncable_id", syncableID)
audit.AddEventParameter(auditRec, "syncable_type", string(syncableType))
var patch *model.GroupSyncablePatch
err = json.Unmarshal(body, &patch)
if err != nil || patch == nil {
c.SetInvalidParamWithErr(fmt.Sprintf("Group%s", syncableType), err)
return
}
audit.AddEventParameterAuditable(auditRec, "patch", patch)
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.createGroupSyncable", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
appErr = verifyLinkUnlinkPermission(c, syncableType, syncableID)
if appErr != nil {
c.Err = appErr
return
}
groupSyncable := &model.GroupSyncable{
GroupId: c.Params.GroupId,
SyncableId: syncableID,
Type: syncableType,
}
groupSyncable.Patch(patch)
groupSyncable, appErr = c.App.UpsertGroupSyncable(groupSyncable)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(groupSyncable)
auditRec.AddEventObjectType("group_syncable")
c.App.Srv().Go(func() {
c.App.SyncRolesAndMembership(c.AppContext, syncableID, syncableType, false)
})
w.WriteHeader(http.StatusCreated)
b, err := json.Marshal(groupSyncable)
if err != nil {
c.Err = model.NewAppError("Api4.createGroupSyncable", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(b)
}
func getGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
c.RequireSyncableId()
if c.Err != nil {
return
}
syncableID := c.Params.SyncableId
c.RequireSyncableType()
if c.Err != nil {
return
}
syncableType := c.Params.SyncableType
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.getGroupSyncable", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
groupSyncable, appErr := c.App.GetGroupSyncable(c.Params.GroupId, syncableID, syncableType)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(groupSyncable)
if err != nil {
c.Err = model.NewAppError("Api4.getGroupSyncable", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func getGroupSyncables(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
c.RequireSyncableType()
if c.Err != nil {
return
}
syncableType := c.Params.SyncableType
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.getGroupSyncables", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups)
return
}
groupSyncables, appErr := c.App.GetGroupSyncables(c.Params.GroupId, syncableType)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(groupSyncables)
if err != nil {
c.Err = model.NewAppError("Api4.getGroupSyncables", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func patchGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
c.RequireSyncableId()
if c.Err != nil {
return
}
syncableID := c.Params.SyncableId
c.RequireSyncableType()
if c.Err != nil {
return
}
syncableType := c.Params.SyncableType
body, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.patchGroupSyncable", "api.io_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
auditRec := c.MakeAuditRecord("patchGroupSyncable", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "group_id", c.Params.GroupId)
audit.AddEventParameter(auditRec, "old_syncable_id", syncableID)
audit.AddEventParameter(auditRec, "old_syncable_type", string(syncableType))
var patch *model.GroupSyncablePatch
err = json.Unmarshal(body, &patch)
if err != nil || patch == nil {
c.SetInvalidParamWithErr(fmt.Sprintf("Group[%s]Patch", syncableType), err)
return
}
audit.AddEventParameterAuditable(auditRec, "patch", patch)
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.patchGroupSyncable", "api.ldap_groups.license_error", nil, "",
http.StatusForbidden)
return
}
appErr := verifyLinkUnlinkPermission(c, syncableType, syncableID)
if appErr != nil {
c.Err = appErr
return
}
groupSyncable, appErr := c.App.GetGroupSyncable(c.Params.GroupId, syncableID, syncableType)
if appErr != nil {
c.Err = appErr
return
}
groupSyncable.Patch(patch)
groupSyncable, appErr = c.App.UpdateGroupSyncable(groupSyncable)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(groupSyncable)
auditRec.AddEventObjectType("group_syncable")
c.App.Srv().Go(func() {
c.App.SyncRolesAndMembership(c.AppContext, syncableID, syncableType, false)
})
b, err := json.Marshal(groupSyncable)
if err != nil {
c.Err = model.NewAppError("Api4.patchGroupSyncable", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(b)
}
func unlinkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
c.RequireSyncableId()
if c.Err != nil {
return
}
syncableID := c.Params.SyncableId
c.RequireSyncableType()
if c.Err != nil {
return
}
syncableType := c.Params.SyncableType
auditRec := c.MakeAuditRecord("unlinkGroupSyncable", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "group_id", c.Params.GroupId)
audit.AddEventParameter(auditRec, "syncable_id", syncableID)
audit.AddEventParameter(auditRec, "syncable_type", string(syncableType))
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.unlinkGroupSyncable", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
appErr := verifyLinkUnlinkPermission(c, syncableType, syncableID)
if appErr != nil {
c.Err = appErr
return
}
_, appErr = c.App.DeleteGroupSyncable(c.Params.GroupId, syncableID, syncableType)
if appErr != nil {
c.Err = appErr
return
}
c.App.Srv().Go(func() {
c.App.SyncRolesAndMembership(c.AppContext, syncableID, syncableType, false)
})
auditRec.Success()
ReturnStatusOK(w)
}
func verifyLinkUnlinkPermission(c *Context, syncableType model.GroupSyncableType, syncableID string) *model.AppError {
switch syncableType {
case model.GroupSyncableTypeTeam:
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), syncableID, model.PermissionManageTeam) {
return c.App.MakePermissionError(c.AppContext.Session(), []*model.Permission{model.PermissionManageTeam})
}
case model.GroupSyncableTypeChannel:
channel, err := c.App.GetChannel(c.AppContext, syncableID)
if err != nil {
return err
}
var permission *model.Permission
if channel.Type == model.ChannelTypePrivate {
permission = model.PermissionManagePrivateChannelMembers
} else {
permission = model.PermissionManagePublicChannelMembers
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), syncableID, permission) {
return c.App.MakePermissionError(c.AppContext.Session(), []*model.Permission{permission})
}
}
return nil
}
func getGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
group, appErr := c.App.GetGroup(c.Params.GroupId, nil, nil)
if appErr != nil {
c.Err = appErr
return
}
appErr = licensedAndConfiguredForGroupBySource(c.App, group.Source)
if appErr != nil {
appErr.Where = "Api4.getGroupMembers"
c.Err = appErr
return
}
if group.Source == model.GroupSourceLdap && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups)
return
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
members, count, appErr := c.App.GetGroupMemberUsersPage(c.Params.GroupId, c.Params.Page, c.Params.PerPage, restrictions)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(struct {
Members []*model.User `json:"members"`
Count int `json:"total_member_count"`
}{
Members: members,
Count: count,
})
if err != nil {
c.Err = model.NewAppError("Api4.getGroupMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func getGroupStats(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.getGroupStats", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups)
return
}
groupID := c.Params.GroupId
count, appErr := c.App.GetGroupMemberCount(groupID, nil)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(model.GroupStats{
GroupID: groupID,
TotalMemberCount: count,
})
if err != nil {
c.Err = model.NewAppError("Api4.getGroupStats", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func getGroupsByUserId(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireUserId()
if c.Err != nil {
return
}
if c.AppContext.Session().UserId != c.Params.UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.getGroupsByUserId", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
groups, appErr := c.App.GetGroupsByUserId(c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(groups)
if err != nil {
c.Err = model.NewAppError("Api4.getGroupsByUserId", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func getGroupsByChannel(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireChannelId()
if c.Err != nil {
return
}
b, appErr := getGroupsByChannelCommon(c, r)
if appErr != nil {
c.Err = appErr
return
}
w.Write(b)
}
func getGroupsByTeam(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
b, appError := getGroupsByTeamCommon(c, r)
if appError != nil {
c.Err = appError
return
}
w.Write(b)
}
func getGroupsByTeamCommon(c *Context, r *http.Request) ([]byte, *model.AppError) {
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
return nil, model.NewAppError("Api4.getGroupsByTeam", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
}
opts := model.GroupSearchOpts{
Q: c.Params.Q,
IncludeMemberCount: c.Params.IncludeMemberCount,
FilterAllowReference: c.Params.FilterAllowReference,
}
if c.Params.Paginate == nil || *c.Params.Paginate {
opts.PageOpts = &model.PageOpts{Page: c.Params.Page, PerPage: c.Params.PerPage}
}
groups, totalCount, appErr := c.App.GetGroupsByTeam(c.Params.TeamId, opts)
if appErr != nil {
return nil, appErr
}
b, err := json.Marshal(struct {
Groups []*model.GroupWithSchemeAdmin `json:"groups"`
Count int `json:"total_group_count"`
}{
Groups: groups,
Count: totalCount,
})
if err != nil {
return nil, model.NewAppError("Api4.getGroupsByTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return b, nil
}
func getGroupsByChannelCommon(c *Context, r *http.Request) ([]byte, *model.AppError) {
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
return nil, model.NewAppError("Api4.getGroupsByChannel", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
}
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if appErr != nil {
return nil, appErr
}
var permission *model.Permission
if channel.Type == model.ChannelTypePrivate {
permission = model.PermissionReadPrivateChannelGroups
} else {
permission = model.PermissionReadPublicChannelGroups
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, permission) {
return nil, c.App.MakePermissionError(c.AppContext.Session(), []*model.Permission{permission})
}
opts := model.GroupSearchOpts{
Q: c.Params.Q,
IncludeMemberCount: c.Params.IncludeMemberCount,
FilterAllowReference: c.Params.FilterAllowReference,
}
if c.Params.Paginate == nil || *c.Params.Paginate {
opts.PageOpts = &model.PageOpts{Page: c.Params.Page, PerPage: c.Params.PerPage}
}
groups, totalCount, appErr := c.App.GetGroupsByChannel(c.Params.ChannelId, opts)
if appErr != nil {
return nil, appErr
}
b, err := json.Marshal(struct {
Groups []*model.GroupWithSchemeAdmin `json:"groups"`
Count int `json:"total_group_count"`
}{
Groups: groups,
Count: totalCount,
})
if err != nil {
return nil, model.NewAppError("Api4.getGroupsByChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return b, nil
}
func getGroupsAssociatedToChannelsByTeam(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
if !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.getGroupsAssociatedToChannelsByTeam", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
opts := model.GroupSearchOpts{
Q: c.Params.Q,
IncludeMemberCount: c.Params.IncludeMemberCount,
FilterAllowReference: c.Params.FilterAllowReference,
}
if c.Params.Paginate == nil || *c.Params.Paginate {
opts.PageOpts = &model.PageOpts{Page: c.Params.Page, PerPage: c.Params.PerPage}
}
groupsAssociatedByChannelID, appErr := c.App.GetGroupsAssociatedToChannelsByTeam(c.Params.TeamId, opts)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(struct {
GroupsAssociatedToChannels map[string][]*model.GroupWithSchemeAdmin `json:"groups"`
}{
GroupsAssociatedToChannels: groupsAssociatedByChannelID,
})
if err != nil {
c.Err = model.NewAppError("Api4.getGroupsAssociatedToChannelsByTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
var teamID, NotAssociatedToChannelID, ChannelIDForMemberCount string
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
source := c.Params.GroupSource
if id := c.Params.NotAssociatedToTeam; model.IsValidId(id) {
teamID = id
}
if id := c.Params.NotAssociatedToChannel; model.IsValidId(id) {
NotAssociatedToChannelID = id
}
if id := c.Params.IncludeChannelMemberCount; model.IsValidId(id) {
ChannelIDForMemberCount = id
}
// If they specify the group_source as custom when the feature is disabled, throw an error
if appErr := licensedAndConfiguredForGroupBySource(c.App, source); appErr != nil {
appErr.Where = "Api4.getGroups"
c.Err = appErr
return
}
// If they don't specify a source and custom groups are disabled, ensure they only get ldap groups in the response
if !*c.App.Config().ServiceSettings.EnableCustomGroups {
source = model.GroupSourceLdap
}
includeTimezones := r.URL.Query().Get("include_timezones") == "true"
opts := model.GroupSearchOpts{
Q: c.Params.Q,
IncludeMemberCount: c.Params.IncludeMemberCount,
FilterAllowReference: c.Params.FilterAllowReference,
FilterParentTeamPermitted: c.Params.FilterParentTeamPermitted,
Source: source,
FilterHasMember: c.Params.FilterHasMember,
IncludeTimezones: includeTimezones,
}
if teamID != "" {
_, appErr := c.App.GetTeam(teamID)
if appErr != nil {
c.Err = appErr
return
}
opts.NotAssociatedToTeam = teamID
}
if NotAssociatedToChannelID != "" {
channel, appErr := c.App.GetChannel(c.AppContext, NotAssociatedToChannelID)
if appErr != nil {
c.Err = appErr
return
}
var permission *model.Permission
if channel.Type == model.ChannelTypePrivate {
permission = model.PermissionManagePrivateChannelMembers
} else {
permission = model.PermissionManagePublicChannelMembers
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), NotAssociatedToChannelID, permission) {
c.SetPermissionError(permission)
return
}
opts.NotAssociatedToChannel = NotAssociatedToChannelID
}
if ChannelIDForMemberCount != "" {
channel, appErr := c.App.GetChannel(c.AppContext, ChannelIDForMemberCount)
if appErr != nil {
c.Err = appErr
return
}
var permission *model.Permission
if channel.Type == model.ChannelTypePrivate {
permission = model.PermissionManagePrivateChannelMembers
} else {
permission = model.PermissionManagePublicChannelMembers
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), ChannelIDForMemberCount, permission) {
c.SetPermissionError(permission)
return
}
opts.IncludeChannelMemberCount = ChannelIDForMemberCount
}
sinceString := r.URL.Query().Get("since")
if sinceString != "" {
since, err := strconv.ParseInt(sinceString, 10, 64)
if err != nil {
c.SetInvalidParamWithErr("since", err)
return
}
opts.Since = since
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
var (
groups = []*model.Group{}
canSee bool = true
)
if opts.FilterHasMember != "" {
canSee, appErr = c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, opts.FilterHasMember)
if appErr != nil {
c.Err = appErr
return
}
}
if canSee {
groups, appErr = c.App.GetGroups(c.Params.Page, c.Params.PerPage, opts, restrictions)
if appErr != nil {
c.Err = appErr
return
}
}
var (
b []byte
err error
)
if c.Params.IncludeTotalCount {
totalCount, cerr := c.App.Srv().Store().Group().GroupCount()
if cerr != nil {
c.Err = model.NewAppError("Api4.getGroups", "api.custom_groups.count_err", nil, "", http.StatusInternalServerError).Wrap(cerr)
return
}
gwc := &model.GroupsWithCount{
Groups: groups,
TotalCount: totalCount,
}
b, err = json.Marshal(gwc)
} else {
b, err = json.Marshal(groups)
}
if err != nil {
c.Err = model.NewAppError("Api4.getGroups", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func deleteGroup(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
group, err := c.App.GetGroup(c.Params.GroupId, nil, nil)
if err != nil {
c.Err = err
return
}
if group.Source != model.GroupSourceCustom {
c.Err = model.NewAppError("Api4.deleteGroup", "app.group.crud_permission", nil, "", http.StatusBadRequest)
return
}
if lcErr := licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom); lcErr != nil {
lcErr.Where = "Api4.deleteGroup"
c.Err = lcErr
return
}
if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionDeleteCustomGroup) {
c.SetPermissionError(model.PermissionDeleteCustomGroup)
return
}
auditRec := c.MakeAuditRecord("deleteGroup", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "group_id", c.Params.GroupId)
_, err = c.App.DeleteGroup(c.Params.GroupId)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func restoreGroup(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
group, err := c.App.GetGroup(c.Params.GroupId, nil, nil)
if err != nil {
c.Err = err
return
}
if group.Source != model.GroupSourceCustom {
c.Err = model.NewAppError("Api4.restoreGroup", "app.group.crud_permission", nil, "", http.StatusNotImplemented)
return
}
if lcErr := licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom); lcErr != nil {
lcErr.Where = "Api4.restoreGroup"
c.Err = lcErr
return
}
if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionRestoreCustomGroup) {
c.SetPermissionError(model.PermissionRestoreCustomGroup)
return
}
auditRec := c.MakeAuditRecord("restoreGroup", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "group_id", c.Params.GroupId)
_, err = c.App.RestoreGroup(c.Params.GroupId)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func addGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
group, appErr := c.App.GetGroup(c.Params.GroupId, nil, nil)
if appErr != nil {
c.Err = appErr
return
}
if group.Source != model.GroupSourceCustom {
c.Err = model.NewAppError("Api4.deleteGroup", "app.group.crud_permission", nil, "", http.StatusBadRequest)
return
}
appErr = licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom)
if appErr != nil {
appErr.Where = "Api4.deleteGroup"
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionManageCustomGroupMembers) {
c.SetPermissionError(model.PermissionManageCustomGroupMembers)
return
}
var newMembers *model.GroupModifyMembers
if err := json.NewDecoder(r.Body).Decode(&newMembers); err != nil {
c.SetInvalidParamWithErr("addGroupMembers", err)
return
}
auditRec := c.MakeAuditRecord("addGroupMembers", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "addGroupMembers_userids", newMembers.UserIds)
members, appErr := c.App.UpsertGroupMembers(c.Params.GroupId, newMembers.UserIds)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(members)
if err != nil {
c.Err = model.NewAppError("Api4.addGroupMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(b)
}
func deleteGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
}
group, appErr := c.App.GetGroup(c.Params.GroupId, nil, nil)
if appErr != nil {
c.Err = appErr
return
}
if group.Source != model.GroupSourceCustom {
c.Err = model.NewAppError("Api4.deleteGroup", "app.group.crud_permission", nil, "", http.StatusBadRequest)
return
}
appErr = licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom)
if appErr != nil {
appErr.Where = "Api4.deleteGroup"
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionManageCustomGroupMembers) {
c.SetPermissionError(model.PermissionManageCustomGroupMembers)
return
}
var deleteBody *model.GroupModifyMembers
if err := json.NewDecoder(r.Body).Decode(&deleteBody); err != nil {
c.SetInvalidParamWithErr("deleteGroupMembers", err)
return
}
auditRec := c.MakeAuditRecord("deleteGroupMembers", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "deleteGroupMembers_userids", deleteBody.UserIds)
members, appErr := c.App.DeleteGroupMembers(c.Params.GroupId, deleteBody.UserIds)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(members)
if err != nil {
c.Err = model.NewAppError("Api4.addGroupMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(b)
}
// licensedAndConfiguredForGroupBySource returns an app error if not properly license or configured for the given group type. The returned app error
// will have a blank 'Where' field, which should be subsequently set by the caller, for example:
//
// err := licensedAndConfiguredForGroupBySource(c.App, group.Source)
// err.Where = "Api4.getGroup"
func licensedAndConfiguredForGroupBySource(app app.AppIface, source model.GroupSource) *model.AppError {
lic := app.Srv().License()
if lic == nil {
return model.NewAppError("", "api.license_error", nil, "", http.StatusForbidden)
}
if source == model.GroupSourceLdap && !*lic.Features.LDAPGroups {
return model.NewAppError("", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
}
if source == model.GroupSourceCustom && lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise {
return model.NewAppError("", "api.custom_groups.license_error", nil, "", http.StatusBadRequest)
}
if source == model.GroupSourceCustom && !*app.Config().ServiceSettings.EnableCustomGroups {
return model.NewAppError("", "api.custom_groups.feature_disabled", nil, "", http.StatusBadRequest)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
)
func (api *API) InitGroupLocal() {
api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/groups", api.APILocal(getGroupsByChannelLocal)).Methods("GET")
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups", api.APILocal(getGroupsByTeamLocal)).Methods("GET")
}
func getGroupsByChannelLocal(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
b, appErr := getGroupsByChannelCommon(c, r)
if appErr != nil {
c.Err = appErr
return
}
w.Write(b)
}
func getGroupsByTeamLocal(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
b, appError := getGroupsByTeamCommon(c, r)
if appError != nil {
c.Err = appError
return
}
w.Write(b)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"github.com/mattermost/gziphandler"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
)
type Context = web.Context
type handlerFunc func(*Context, http.ResponseWriter, *http.Request)
// APIHandler provides a handler for API endpoints which do not require the user to be logged in order for access to be
// granted.
func (api *API) APIHandler(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: false,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// APISessionRequired provides a handler for API endpoints which require the user to be logged in in order for access to
// be granted.
func (api *API) APISessionRequired(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: true,
TrustRequester: false,
RequireMfa: true,
IsStatic: false,
IsLocal: false,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// CloudAPIKeyRequired provides a handler for webhook endpoints to access Cloud installations from CWS
func (api *API) CloudAPIKeyRequired(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: false,
RequireCloudKey: true,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// RemoteClusterTokenRequired provides a handler for remote cluster requests to /remotecluster endpoints.
func (api *API) RemoteClusterTokenRequired(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: false,
RequireCloudKey: false,
RequireRemoteClusterToken: true,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// APISessionRequiredMfa provides a handler for API endpoints which require a logged-in user session but when accessed,
// if MFA is enabled, the MFA process is not yet complete, and therefore the requirement to have completed the MFA
// authentication must be waived.
func (api *API) APISessionRequiredMfa(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: true,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// APIHandlerTrustRequester provides a handler for API endpoints which do not require the user to be logged in and are
// allowed to be requested directly rather than via javascript/XMLHttpRequest, such as site branding images or the
// websocket.
func (api *API) APIHandlerTrustRequester(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: false,
TrustRequester: true,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// APISessionRequiredTrustRequester provides a handler for API endpoints which do require the user to be logged in and
// are allowed to be requested directly rather than via javascript/XMLHttpRequest, such as emoji or file uploads.
func (api *API) APISessionRequiredTrustRequester(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: true,
TrustRequester: true,
RequireMfa: true,
IsStatic: false,
IsLocal: false,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// DisableWhenBusy provides a handler for API endpoints which should be disabled when the server is under load,
// responding with HTTP 503 (Service Unavailable).
func (api *API) APISessionRequiredDisableWhenBusy(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: true,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
DisableWhenBusy: true,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// APILocal provides a handler for API endpoints to be used in local
// mode, this is, through a UNIX socket and without an authenticated
// session, but with one that has no user set and no permission
// restrictions
func (api *API) APILocal(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: false,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: true,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
func requireLicense(c *Context) *model.AppError {
if c.App.Channels().License() == nil {
err := model.NewAppError("", "api.license_error", nil, "", http.StatusNotImplemented)
return err
}
return nil
}
func minimumProfessionalLicense(c *Context) *model.AppError {
lic := c.App.Srv().License()
if lic == nil || (lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise) {
err := model.NewAppError("", model.NoTranslation, nil, "license is neither professional nor enterprise", http.StatusNotImplemented)
return err
}
return nil
}
func rejectGuests(c *Context) *model.AppError {
if c.AppContext.Session().Props[model.SessionPropIsGuest] == "true" {
err := model.NewAppError("", model.NoTranslation, nil, "insufficient permissions as a guest user", http.StatusNotImplemented)
return err
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/url"
"strconv"
"github.com/pkg/errors"
)
func parseInt(u *url.URL, name string, defaultValue int) (int, error) {
valueStr := u.Query().Get(name)
if valueStr == "" {
return defaultValue, nil
}
value, err := strconv.Atoi(valueStr)
if err != nil {
return 0, errors.Wrapf(err, "failed to parse %s as integer", name)
}
return value, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/web"
)
// APIs for self-hosted workspaces to communicate with the backing customer & payments system.
// Endpoints for cloud installations should not go in this file.
func (api *API) InitHostedCustomer() {
// POST /api/v4/hosted_customer/available
api.BaseRoutes.HostedCustomer.Handle("/signup_available", api.APISessionRequired(handleSignupAvailable)).Methods("GET")
// POST /api/v4/hosted_customer/bootstrap
api.BaseRoutes.HostedCustomer.Handle("/bootstrap", api.APISessionRequired(selfHostedBootstrap)).Methods("POST")
// POST /api/v4/hosted_customer/customer
api.BaseRoutes.HostedCustomer.Handle("/customer", api.APISessionRequired(selfHostedCustomer)).Methods("POST")
// POST /api/v4/hosted_customer/confirm
api.BaseRoutes.HostedCustomer.Handle("/confirm", api.APISessionRequired(selfHostedConfirm)).Methods("POST")
// GET /api/v4/hosted_customer/invoices
api.BaseRoutes.HostedCustomer.Handle("/invoices", api.APISessionRequired(selfHostedInvoices)).Methods("GET")
// GET /api/v4/hosted_customer/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf
api.BaseRoutes.HostedCustomer.Handle("/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf", api.APISessionRequired(selfHostedInvoicePDF)).Methods("GET")
}
func ensureSelfHostedAdmin(c *Context, where string) {
cloud := c.App.Cloud()
if cloud == nil {
c.Err = model.NewAppError(where, "api.server.cws.needs_enterprise_edition", nil, "", http.StatusBadRequest)
return
}
license := c.App.Channels().License()
if license.IsCloud() {
c.Err = model.NewAppError(where, "api.cloud.license_error", nil, "Cloud installations do not use this endpoint", http.StatusBadRequest)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
}
func checkSelfHostedPurchaseEnabled(c *Context) bool {
config := c.App.Config()
if config == nil {
return false
}
enabled := config.ServiceSettings.SelfHostedPurchase
return enabled != nil && *enabled
}
func selfHostedBootstrap(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.selfHostedBootstrap"
if !checkSelfHostedPurchaseEnabled(c) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
return
}
reset := r.URL.Query().Get("reset") == "true"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
user, userErr := c.App.GetUser(c.AppContext.Session().UserId)
if userErr != nil {
c.Err = userErr
return
}
signupProgress, err := c.App.Cloud().BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: user.Email, Reset: reset})
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError)
return
}
json, err := json.Marshal(signupProgress)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError)
return
}
w.Write(json)
}
func selfHostedCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.selfHostedCustomer"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
if !checkSelfHostedPurchaseEnabled(c) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
var form *model.SelfHostedCustomerForm
if err = json.Unmarshal(bodyBytes, &form); err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
user, userErr := c.App.GetUser(c.AppContext.Session().UserId)
if userErr != nil {
c.Err = userErr
return
}
customerResponse, err := c.App.Cloud().CreateCustomerSelfHostedSignup(*form, user.Email)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(customerResponse)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func selfHostedConfirm(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.selfHostedConfirm"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
if !checkSelfHostedPurchaseEnabled(c) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
var confirm model.SelfHostedConfirmPaymentMethodRequest
err = json.Unmarshal(bodyBytes, &confirm)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.request_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
user, userErr := c.App.GetUser(c.AppContext.Session().UserId)
if userErr != nil {
c.Err = userErr
return
}
confirmResponse, err := c.App.Cloud().ConfirmSelfHostedSignup(confirm, user.Email)
if err != nil {
if confirmResponse != nil {
c.App.NotifySelfHostedSignupProgress(confirmResponse.Progress, user.Id)
}
if err.Error() == fmt.Sprintf("%d", http.StatusUnprocessableEntity) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusUnprocessableEntity).Wrap(err)
return
}
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
license, err := c.App.Srv().Platform().SaveLicense([]byte(confirmResponse.License))
// dealing with an AppError
if !(reflect.ValueOf(err).Kind() == reflect.Ptr && reflect.ValueOf(err).IsNil()) {
if confirmResponse != nil {
c.App.NotifySelfHostedSignupProgress(confirmResponse.Progress, user.Id)
}
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
clientResponse, err := json.Marshal(model.SelfHostedSignupConfirmClientResponse{
License: utils.GetClientLicense(license),
Progress: confirmResponse.Progress,
})
if err != nil {
if confirmResponse != nil {
c.App.NotifySelfHostedSignupProgress(confirmResponse.Progress, user.Id)
}
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
go func() {
err := c.App.Cloud().ConfirmSelfHostedSignupLicenseApplication()
if err != nil {
c.Logger.Warn("Unable to confirm license application", mlog.Err(err))
}
}()
_, _ = w.Write(clientResponse)
}
func handleSignupAvailable(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.handleSignupAvailable"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
if !checkSelfHostedPurchaseEnabled(c) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
return
}
if err := c.App.Cloud().SelfHostedSignupAvailable(); err != nil {
if err.Error() == "upstream_off" {
c.Err = model.NewAppError(where, "api.server.hosted_signup_unavailable.error", nil, "", http.StatusServiceUnavailable)
} else {
c.Err = model.NewAppError(where, "api.server.hosted_signup_unavailable.error", nil, "", http.StatusNotImplemented)
}
return
}
systemValue, err := c.App.Srv().Store().System().GetByName(model.SystemHostedPurchaseNeedsScreening)
if err == nil && systemValue != nil {
c.Err = model.NewAppError(where, "api.server.hosted_signup_unavailable.error", nil, "", http.StatusTooEarly)
return
}
ReturnStatusOK(w)
}
func selfHostedInvoices(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.selfHostedInvoices"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
invoices, err := c.App.Cloud().GetSelfHostedInvoices()
if err != nil {
if err.Error() == "404" {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotFound).Wrap(errors.New("invoices for license not found"))
return
}
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(invoices)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func selfHostedInvoicePDF(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.selfHostedInvoicePDF"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
pdfData, filename, appErr := c.App.Cloud().GetSelfHostedInvoicePDF(c.Params.InvoiceId)
if appErr != nil {
c.Err = model.NewAppError("Api4.getSubscriptionInvoicePDF", "api.cloud.request_error", nil, appErr.Error(), http.StatusInternalServerError)
return
}
web.WriteFileResponse(
filename,
"application/pdf",
int64(binary.Size(pdfData)),
time.Now(),
*c.App.Config().ServiceSettings.WebserverMode,
bytes.NewReader(pdfData),
false,
w,
r,
)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"net/url"
"github.com/mattermost/mattermost-server/v6/model"
)
func (api *API) InitImage() {
api.BaseRoutes.Image.Handle("", api.APISessionRequiredTrustRequester(getImage)).Methods("GET")
}
func getImage(c *Context, w http.ResponseWriter, r *http.Request) {
actualURL := r.URL.Query().Get("url")
parsedURL, err := url.Parse(actualURL)
if err != nil {
c.Err = model.NewAppError("getImage", "api.image.get.app_error", nil, err.Error(), http.StatusBadRequest)
return
} else if parsedURL.Opaque != "" {
c.Err = model.NewAppError("getImage", "api.image.get.app_error", nil, "", http.StatusBadRequest)
return
}
siteURL, err := url.Parse(*c.App.Config().ServiceSettings.SiteURL)
if err != nil {
c.Err = model.NewAppError("getImage", "model.config.is_valid.site_url.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
if parsedURL.Scheme == "" {
parsedURL.Scheme = siteURL.Scheme
}
if parsedURL.Host == "" {
parsedURL.Host = siteURL.Host
}
// in case image proxy is enabled and we are fetching a remote image (NOT static or served by plugins), pass request to proxy
if *c.App.Config().ImageProxySettings.Enable && parsedURL.Host != siteURL.Host {
c.App.ImageProxy().GetImage(w, r, parsedURL.String())
} else {
http.Redirect(w, r, parsedURL.String(), http.StatusFound)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitImport() {
api.BaseRoutes.Imports.Handle("", api.APISessionRequired(listImports)).Methods("GET")
}
func listImports(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionManageSystem)
return
}
imports, appErr := c.App.ListImports()
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(imports); err != nil {
c.Logger.Warn("Error writing imports", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitImportLocal() {
api.BaseRoutes.Imports.Handle("", api.APILocal(listImports)).Methods("GET")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"time"
"github.com/mattermost/mattermost-server/v6/model"
)
func (api *API) InitInsights() {
// Reactions
api.BaseRoutes.InsightsForTeam.Handle("/reactions", api.APISessionRequired(getTopReactionsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/reactions", api.APISessionRequired(getTopReactionsForUserSince)).Methods("GET")
// Channels
api.BaseRoutes.InsightsForTeam.Handle("/channels", api.APISessionRequired(getTopChannelsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/channels", api.APISessionRequired(getTopChannelsForUserSince)).Methods("GET")
// Threads
api.BaseRoutes.InsightsForTeam.Handle("/threads", api.APISessionRequired(getTopThreadsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/threads", api.APISessionRequired(getTopThreadsForUserSince)).Methods("GET")
// user DMs
api.BaseRoutes.InsightsForUser.Handle("/dms", api.APISessionRequired(getTopDMsForUserSince)).Methods("GET")
// Inactive channels
api.BaseRoutes.InsightsForTeam.Handle("/inactive_channels", api.APISessionRequired(getTopInactiveChannelsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/inactive_channels", api.APISessionRequired(getTopInactiveChannelsForUserSince)).Methods("GET")
// New teammembers
api.BaseRoutes.InsightsForTeam.Handle("/team_members", api.APISessionRequired(getNewTeamMembersSince)).Methods("GET")
}
// Top Reactions
func getTopReactionsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topReactionList, appErr := c.App.GetTopReactionsForTeamSince(c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topReactionList); err != nil {
c.Err = model.NewAppError("getTopReactionsForTeamSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
func getTopReactionsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// guest user check
permissionErr := rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.Params.TeamId = r.URL.Query().Get("team_id")
// TeamId is an optional parameter
if c.Params.TeamId != "" {
if !model.IsValidId(c.Params.TeamId) {
c.SetInvalidURLParam("team_id")
return
}
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
}
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topReactionList, appErr := c.App.GetTopReactionsForUserSince(c.AppContext.Session().UserId, c.Params.TeamId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topReactionList); err != nil {
c.Err = model.NewAppError("getTopReactionsForUserSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// Top Channels
func getTopChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
loc := user.GetTimezoneLocation()
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
topChannels, appErr := c.App.GetTopChannelsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
topChannels.PostCountByDuration, appErr = postCountByDurationViewModel(c, topChannels, startTime, c.Params.TimeRange, nil, loc)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topChannels); err != nil {
c.Err = model.NewAppError("getTopChannelsForTeamSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
func getTopChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// guest user check
permissionErr := rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.Params.TeamId = r.URL.Query().Get("team_id")
// TeamId is an optional parameter
if c.Params.TeamId != "" {
if !model.IsValidId(c.Params.TeamId) {
c.SetInvalidURLParam("team_id")
return
}
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
}
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
loc := user.GetTimezoneLocation()
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
topChannels, appErr := c.App.GetTopChannelsForUserSince(c.AppContext, c.AppContext.Session().UserId, c.Params.TeamId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
topChannels.PostCountByDuration, appErr = postCountByDurationViewModel(c, topChannels, startTime, c.Params.TimeRange, &c.AppContext.Session().UserId, loc)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topChannels); err != nil {
c.Err = model.NewAppError("getTopChannelsForUserSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// Top Threads
func getTopThreadsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
// restrict users with no access to team
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topThreads, appErr := c.App.GetTopThreadsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topThreads); err != nil {
c.Err = model.NewAppError("getTopThreadsForTeamSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
func getTopThreadsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// guest user check
permissionErr := rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.Params.TeamId = r.URL.Query().Get("team_id")
// restrict users with no access to team
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
// TeamId is an optional parameter
if c.Params.TeamId != "" {
if !model.IsValidId(c.Params.TeamId) {
c.SetInvalidURLParam("team_id")
return
}
team, teamErr := c.App.GetTeam(c.Params.TeamId)
if teamErr != nil {
c.Err = teamErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
}
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topThreads, appErr := c.App.GetTopThreadsForUserSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topThreads); err != nil {
c.Err = model.NewAppError("getTopThreadsForUserSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// Top DMs
func getTopDMsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// guest user check
permissionErr := rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topDMs, err := c.App.GetTopDMsForUserSince(user.Id, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(topDMs); err != nil {
c.Err = model.NewAppError("getTopDMsForUserSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// Top Channels
func getTopInactiveChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
team, err := c.App.GetTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
loc := user.GetTimezoneLocation()
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
topChannels, err := c.App.GetTopInactiveChannelsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(topChannels); err != nil {
c.Err = model.NewAppError("getTopInactiveChannelsForTeamSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// top inactive channels
func getTopInactiveChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// guest user check
permissionErr := rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.Params.TeamId = r.URL.Query().Get("team_id")
// TeamId is an optional parameter
if c.Params.TeamId != "" {
if !model.IsValidId(c.Params.TeamId) {
c.SetInvalidURLParam("team_id")
return
}
team, teamErr := c.App.GetTeam(c.Params.TeamId)
if teamErr != nil {
c.Err = teamErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
loc := user.GetTimezoneLocation()
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
topChannels, err := c.App.GetTopInactiveChannelsForUserSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(topChannels); err != nil {
c.Err = model.NewAppError("getTopInactiveChannelsForUserSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// postCountByDurationViewModel expects a list of channels that are pre-authorized for the given user to view.
func postCountByDurationViewModel(c *Context, topChannelList *model.TopChannelList, startTime *time.Time, timeRange string, userID *string, location *time.Location) (model.ChannelPostCountByDuration, *model.AppError) {
if len(topChannelList.Items) == 0 {
return nil, nil
}
var postCountsByDay []*model.DurationPostCount
channelIDs := topChannelList.ChannelIDs()
var grouping model.PostCountGrouping
if timeRange == model.TimeRangeToday {
grouping = model.PostsByHour
} else {
grouping = model.PostsByDay
}
postCountsByDay, err := c.App.PostCountsByDuration(c.AppContext, channelIDs, startTime.UnixMilli(), userID, grouping, location)
if err != nil {
return nil, err
}
return model.ToDailyPostCountViewModel(postCountsByDay, startTime, model.TimeRangeToNumberDays(timeRange), channelIDs), nil
}
func getNewTeamMembersSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
team, err := c.App.GetTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
loc := user.GetTimezoneLocation()
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
ntms, count, err := c.App.GetNewTeamMembersSince(c.AppContext, c.Params.TeamId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if err != nil {
c.Err = err
return
}
ntms.TotalCount = count
if err := json.NewEncoder(w).Encode(ntms); err != nil {
c.Err = model.NewAppError("getNewTeamembersForTeamSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitAction() {
api.BaseRoutes.Post.Handle("/actions/{action_id:[A-Za-z0-9]+}", api.APISessionRequired(doPostAction)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/actions/dialogs/open", api.APIHandler(openDialog)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/actions/dialogs/submit", api.APISessionRequired(submitDialog)).Methods("POST")
}
func doPostAction(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
var actionRequest model.DoPostActionRequest
err := json.NewDecoder(r.Body).Decode(&actionRequest)
if err != nil {
c.Logger.Warn("Error decoding the action request", mlog.Err(err))
}
var cookie *model.PostActionCookie
if actionRequest.Cookie != "" {
cookie = &model.PostActionCookie{}
cookieStr := ""
cookieStr, err = model.DecryptPostActionCookie(actionRequest.Cookie, c.App.PostActionCookieSecret())
if err != nil {
c.Err = model.NewAppError("DoPostAction", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
err = json.Unmarshal([]byte(cookieStr), &cookie)
if err != nil {
c.Err = model.NewAppError("DoPostAction", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), cookie.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
} else {
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
}
var appErr *model.AppError
resp := &model.PostActionAPIResponse{Status: "OK"}
resp.TriggerId, appErr = c.App.DoPostActionWithCookie(c.AppContext, c.Params.PostId, c.Params.ActionId, c.AppContext.Session().UserId,
actionRequest.SelectedOption, cookie)
if appErr != nil {
c.Err = appErr
return
}
err = json.NewEncoder(w).Encode(resp)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func openDialog(c *Context, w http.ResponseWriter, r *http.Request) {
var dialog model.OpenDialogRequest
err := json.NewDecoder(r.Body).Decode(&dialog)
if err != nil {
c.SetInvalidParamWithErr("dialog", err)
return
}
if dialog.URL == "" {
c.SetInvalidParam("url")
return
}
if appErr := c.App.OpenInteractiveDialog(dialog); appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
func submitDialog(c *Context, w http.ResponseWriter, r *http.Request) {
var submit model.SubmitDialogRequest
jsonErr := json.NewDecoder(r.Body).Decode(&submit)
if jsonErr != nil {
c.SetInvalidParamWithErr("dialog", jsonErr)
return
}
if submit.URL == "" {
c.SetInvalidParam("url")
return
}
submit.UserId = c.AppContext.Session().UserId
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), submit.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), submit.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
resp, err := c.App.SubmitInteractiveDialog(c.AppContext, submit)
if err != nil {
c.Err = err
return
}
b, _ := json.Marshal(resp)
w.Write(b)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"path/filepath"
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/web"
)
func (api *API) InitJob() {
api.BaseRoutes.Jobs.Handle("", api.APISessionRequired(getJobs)).Methods("GET")
api.BaseRoutes.Jobs.Handle("", api.APISessionRequired(createJob)).Methods("POST")
api.BaseRoutes.Jobs.Handle("/{job_id:[A-Za-z0-9]+}", api.APISessionRequired(getJob)).Methods("GET")
api.BaseRoutes.Jobs.Handle("/{job_id:[A-Za-z0-9]+}/download", api.APISessionRequiredTrustRequester(downloadJob)).Methods("GET")
api.BaseRoutes.Jobs.Handle("/{job_id:[A-Za-z0-9]+}/cancel", api.APISessionRequired(cancelJob)).Methods("POST")
api.BaseRoutes.Jobs.Handle("/type/{job_type:[A-Za-z0-9_-]+}", api.APISessionRequired(getJobsByType)).Methods("GET")
}
func getJob(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireJobId()
if c.Err != nil {
return
}
job, err := c.App.GetJob(c.Params.JobId)
if err != nil {
c.Err = err
return
}
hasPermission, permissionRequired := c.App.SessionHasPermissionToReadJob(*c.AppContext.Session(), job.Type)
if permissionRequired == nil {
c.Err = model.NewAppError("getJob", "api.job.retrieve.nopermissions", nil, "", http.StatusBadRequest)
return
}
if !hasPermission {
c.SetPermissionError(permissionRequired)
return
}
if err := json.NewEncoder(w).Encode(job); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func downloadJob(c *Context, w http.ResponseWriter, r *http.Request) {
config := c.App.Config()
const FilePath = "export"
const FileMime = "application/zip"
c.RequireJobId()
if c.Err != nil {
return
}
if !*config.MessageExportSettings.DownloadExportResults {
c.Err = model.NewAppError("downloadExportResultsNotEnabled", "app.job.download_export_results_not_enabled", nil, "", http.StatusNotImplemented)
return
}
job, err := c.App.GetJob(c.Params.JobId)
if err != nil {
c.Err = err
return
}
// Currently, this endpoint only supports downloading the compliance report.
// If you need to download another job type, you will need to alter this section of the code to accommodate it.
if job.Type == model.JobTypeMessageExport && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionDownloadComplianceExportResult) {
c.SetPermissionError(model.PermissionDownloadComplianceExportResult)
return
} else if job.Type != model.JobTypeMessageExport {
c.Err = model.NewAppError("unableToDownloadJob", "api.job.unable_to_download_job.incorrect_job_type", nil, "", http.StatusBadRequest)
return
}
isDownloadable, _ := strconv.ParseBool(job.Data["is_downloadable"])
if !isDownloadable {
c.Err = model.NewAppError("unableToDownloadJob", "api.job.unable_to_download_job", nil, "", http.StatusBadRequest)
return
}
fileName := job.Id + ".zip"
filePath := filepath.Join(FilePath, fileName)
fileReader, err := c.App.FileReader(filePath)
if err != nil {
c.Err = err
c.Err.StatusCode = http.StatusNotFound
return
}
defer fileReader.Close()
// We are able to pass 0 for content size due to the fact that Golang's serveContent (https://golang.org/src/net/http/fs.go)
// already sets that for us
web.WriteFileResponse(fileName, FileMime, 0, time.Unix(0, job.LastActivityAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, true, w, r)
}
func createJob(c *Context, w http.ResponseWriter, r *http.Request) {
var job model.Job
if jsonErr := json.NewDecoder(r.Body).Decode(&job); jsonErr != nil {
c.SetInvalidParamWithErr("job", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createJob", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "job", &job)
hasPermission, permissionRequired := c.App.SessionHasPermissionToCreateJob(*c.AppContext.Session(), &job)
if permissionRequired == nil {
c.Err = model.NewAppError("unableToCreateJob", "api.job.unable_to_create_job.incorrect_job_type", nil, "", http.StatusBadRequest)
return
}
if !hasPermission {
c.SetPermissionError(permissionRequired)
return
}
rjob, err := c.App.CreateJob(&job)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(rjob)
auditRec.AddEventObjectType("job")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rjob); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getJobs(c *Context, w http.ResponseWriter, r *http.Request) {
if c.Err != nil {
return
}
var validJobTypes []string
for _, jobType := range model.AllJobTypes {
hasPermission, permissionRequired := c.App.SessionHasPermissionToReadJob(*c.AppContext.Session(), jobType)
if permissionRequired == nil {
mlog.Warn("The job types of a job you are trying to retrieve does not contain permissions", mlog.String("jobType", jobType))
continue
}
if hasPermission {
validJobTypes = append(validJobTypes, jobType)
}
}
if len(validJobTypes) == 0 {
c.SetPermissionError()
return
}
jobs, appErr := c.App.GetJobsByTypesPage(validJobTypes, c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(jobs)
if err != nil {
c.Err = model.NewAppError("getJobs", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getJobsByType(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireJobType()
if c.Err != nil {
return
}
hasPermission, permissionRequired := c.App.SessionHasPermissionToReadJob(*c.AppContext.Session(), c.Params.JobType)
if permissionRequired == nil {
c.Err = model.NewAppError("getJobsByType", "api.job.retrieve.nopermissions", nil, "", http.StatusBadRequest)
return
}
if !hasPermission {
c.SetPermissionError(permissionRequired)
return
}
jobs, appErr := c.App.GetJobsByTypePage(c.Params.JobType, c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(jobs)
if err != nil {
c.Err = model.NewAppError("getJobsByType", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func cancelJob(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireJobId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("cancelJob", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "job_id", c.Params.JobId)
job, err := c.App.GetJob(c.Params.JobId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(job)
auditRec.AddEventObjectType("job")
// if permission to create, permission to cancel, same permission
hasPermission, permissionRequired := c.App.SessionHasPermissionToCreateJob(*c.AppContext.Session(), job)
if permissionRequired == nil {
c.Err = model.NewAppError("unableToCancelJob", "api.job.unable_to_create_job.incorrect_job_type", nil, "", http.StatusBadRequest)
return
}
if !hasPermission {
c.SetPermissionError(permissionRequired)
return
}
if err := c.App.CancelJob(c.Params.JobId); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitJobLocal() {
api.BaseRoutes.Jobs.Handle("", api.APILocal(getJobs)).Methods("GET")
api.BaseRoutes.Jobs.Handle("", api.APILocal(createJob)).Methods("POST")
api.BaseRoutes.Jobs.Handle("/{job_id:[A-Za-z0-9]+}", api.APILocal(getJob)).Methods("GET")
api.BaseRoutes.Jobs.Handle("/{job_id:[A-Za-z0-9]+}/cancel", api.APILocal(cancelJob)).Methods("POST")
api.BaseRoutes.Jobs.Handle("/type/{job_type:[A-Za-z0-9_-]+}", api.APILocal(getJobsByType)).Methods("GET")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"mime/multipart"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type mixedUnlinkedGroup struct {
Id *string `json:"mattermost_group_id"`
DisplayName string `json:"name"`
RemoteId string `json:"primary_key"`
HasSyncables *bool `json:"has_syncables"`
}
func (api *API) InitLdap() {
api.BaseRoutes.LDAP.Handle("/sync", api.APISessionRequired(syncLdap)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/test", api.APISessionRequired(testLdap)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/migrateid", api.APISessionRequired(migrateIdLdap)).Methods("POST")
// GET /api/v4/ldap/groups?page=0&per_page=1000
api.BaseRoutes.LDAP.Handle("/groups", api.APISessionRequired(getLdapGroups)).Methods("GET")
// POST /api/v4/ldap/groups/:remote_id/link
api.BaseRoutes.LDAP.Handle(`/groups/{remote_id}/link`, api.APISessionRequired(linkLdapGroup)).Methods("POST")
// DELETE /api/v4/ldap/groups/:remote_id/link
api.BaseRoutes.LDAP.Handle(`/groups/{remote_id}/link`, api.APISessionRequired(unlinkLdapGroup)).Methods("DELETE")
api.BaseRoutes.LDAP.Handle("/certificate/public", api.APISessionRequired(addLdapPublicCertificate)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/certificate/private", api.APISessionRequired(addLdapPrivateCertificate)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/certificate/public", api.APISessionRequired(removeLdapPublicCertificate)).Methods("DELETE")
api.BaseRoutes.LDAP.Handle("/certificate/private", api.APISessionRequired(removeLdapPrivateCertificate)).Methods("DELETE")
api.BaseRoutes.LDAP.Handle("/users/{user_id}/group_sync_memberships", api.APISessionRequired(addUserToGroupSyncables)).Methods("POST")
}
func syncLdap(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAP {
c.Err = model.NewAppError("Api4.syncLdap", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
return
}
type LdapSyncOptions struct {
IncludeRemovedMembers bool `json:"include_removed_members"`
}
var opts LdapSyncOptions
err := json.NewDecoder(r.Body).Decode(&opts)
if err != nil {
c.Logger.Warn("Error decoding LDAP sync options", mlog.Err(err))
}
auditRec := c.MakeAuditRecord("syncLdap", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateLdapSyncJob) {
c.SetPermissionError(model.PermissionCreateLdapSyncJob)
return
}
c.App.SyncLdap(opts.IncludeRemovedMembers)
auditRec.Success()
ReturnStatusOK(w)
}
func testLdap(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAP {
c.Err = model.NewAppError("Api4.testLdap", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionTestLdap) {
c.SetPermissionError(model.PermissionTestLdap)
return
}
if err := c.App.TestLdap(); err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
func getLdapGroups(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups)
return
}
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.getLdapGroups", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
return
}
opts := model.LdapGroupSearchOpts{
Q: c.Params.Q,
}
if c.Params.IsLinked != nil {
opts.IsLinked = c.Params.IsLinked
}
if c.Params.IsConfigured != nil {
opts.IsConfigured = c.Params.IsConfigured
}
groups, total, appErr := c.App.GetAllLdapGroupsPage(c.Params.Page, c.Params.PerPage, opts)
if appErr != nil {
c.Err = appErr
return
}
mugs := []*mixedUnlinkedGroup{}
for _, group := range groups {
mug := &mixedUnlinkedGroup{
DisplayName: group.DisplayName,
RemoteId: group.GetRemoteId(),
}
if len(group.Id) == 26 {
mug.Id = &group.Id
mug.HasSyncables = &group.HasSyncables
}
mugs = append(mugs, mug)
}
b, err := json.Marshal(struct {
Count int `json:"count"`
Groups []*mixedUnlinkedGroup `json:"groups"`
}{Count: total, Groups: mugs})
if err != nil {
c.Err = model.NewAppError("Api4.getLdapGroups", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func linkLdapGroup(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireRemoteId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementGroups)
return
}
auditRec := c.MakeAuditRecord("linkLdapGroup", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "remote_id", c.Params.RemoteId)
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.linkLdapGroup", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
return
}
ldapGroup, appErr := c.App.GetLdapGroup(c.Params.RemoteId)
if appErr != nil {
c.Err = appErr
return
}
if ldapGroup == nil {
c.Err = model.NewAppError("Api4.linkLdapGroup", "api.ldap_group.not_found", nil, "", http.StatusNotFound)
return
}
group, appErr := c.App.GetGroupByRemoteID(ldapGroup.GetRemoteId(), model.GroupSourceLdap)
if appErr != nil && appErr.Id != "app.group.no_rows" {
c.Err = appErr
return
}
if group != nil {
audit.AddEventParameterAuditable(auditRec, "group", group)
}
var status int
var newOrUpdatedGroup *model.Group
// Truncate display name if necessary
var displayName string
if len(ldapGroup.DisplayName) > model.GroupDisplayNameMaxLength {
displayName = ldapGroup.DisplayName[:model.GroupDisplayNameMaxLength]
} else {
displayName = ldapGroup.DisplayName
}
// Group has been previously linked
if group != nil {
if group.DeleteAt == 0 {
newOrUpdatedGroup = group
} else {
group.DeleteAt = 0
group.DisplayName = displayName
group.RemoteId = ldapGroup.RemoteId
newOrUpdatedGroup, appErr = c.App.UpdateGroup(group)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(newOrUpdatedGroup)
auditRec.AddEventObjectType("group")
}
status = http.StatusOK
} else {
// Group has never been linked
//
// For group mentions implementation, the Name column will no longer be set by default.
// Instead it will be set and saved in the web app when Group Mentions is enabled.
newGroup := &model.Group{
DisplayName: displayName,
RemoteId: ldapGroup.RemoteId,
Source: model.GroupSourceLdap,
}
newOrUpdatedGroup, appErr = c.App.CreateGroup(newGroup)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(newOrUpdatedGroup)
auditRec.AddEventObjectType("group")
status = http.StatusCreated
}
b, err := json.Marshal(newOrUpdatedGroup)
if err != nil {
c.Err = model.NewAppError("Api4.linkLdapGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.WriteHeader(status)
w.Write(b)
}
func unlinkLdapGroup(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireRemoteId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("unlinkLdapGroup", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "remote_id", c.Params.RemoteId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementGroups)
return
}
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.unlinkLdapGroup", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
return
}
group, err := c.App.GetGroupByRemoteID(c.Params.RemoteId, model.GroupSourceLdap)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(group)
auditRec.AddEventObjectType("group")
if group.DeleteAt == 0 {
deletedGroup, err := c.App.DeleteGroup(group.Id)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(deletedGroup)
}
auditRec.Success()
ReturnStatusOK(w)
}
func migrateIdLdap(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.StringInterfaceFromJSON(r.Body)
toAttribute, ok := props["toAttribute"].(string)
if !ok || toAttribute == "" {
c.SetInvalidParam("toAttribute")
return
}
auditRec := c.MakeAuditRecord("idMigrateLdap", audit.Fail)
audit.AddEventParameter(auditRec, "to_attribute", toAttribute)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAP {
c.Err = model.NewAppError("Api4.idMigrateLdap", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
return
}
if err := c.App.MigrateIdLDAP(toAttribute); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func parseLdapCertificateRequest(r *http.Request, maxFileSize int64) (*multipart.FileHeader, *model.AppError) {
err := r.ParseMultipartForm(maxFileSize)
if err != nil {
return nil, model.NewAppError("addLdapCertificate", "api.admin.add_certificate.parseform.app_error", nil, err.Error(), http.StatusBadRequest)
}
m := r.MultipartForm
fileArray, ok := m.File["certificate"]
if !ok {
return nil, model.NewAppError("addLdapCertificate", "api.admin.add_certificate.no_file.app_error", nil, "", http.StatusBadRequest)
}
if len(fileArray) <= 0 {
return nil, model.NewAppError("addLdapCertificate", "api.admin.add_certificate.array.app_error", nil, "", http.StatusBadRequest)
}
return fileArray[0], nil
}
func addLdapPublicCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionAddLdapPublicCert) {
c.SetPermissionError(model.PermissionAddLdapPublicCert)
return
}
fileData, err := parseLdapCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("addLdapPublicCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
if err := c.App.AddLdapPublicCertificate(fileData); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func addLdapPrivateCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionAddLdapPrivateCert) {
c.SetPermissionError(model.PermissionAddLdapPrivateCert)
return
}
fileData, err := parseLdapCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("addLdapPrivateCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
if err := c.App.AddLdapPrivateCertificate(fileData); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func removeLdapPublicCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRemoveLdapPublicCert) {
c.SetPermissionError(model.PermissionRemoveLdapPublicCert)
return
}
auditRec := c.MakeAuditRecord("removeLdapPublicCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
if err := c.App.RemoveLdapPublicCertificate(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func removeLdapPrivateCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRemoveLdapPrivateCert) {
c.SetPermissionError(model.PermissionRemoveLdapPrivateCert)
return
}
auditRec := c.MakeAuditRecord("removeLdapPrivateCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
if err := c.App.RemoveLdapPrivateCertificate(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
// addUserToGroupSyncables creates memberships—for the given user—to all of their group syncables (i.e. channels or teams).
// For each group the user is a member of, for each channel and/or team that group is associated with, the user will be added.
func addUserToGroupSyncables(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementGroups)
return
}
user, appErr := c.App.GetUser(c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
if user.AuthService != model.UserAuthServiceLdap {
c.Err = model.NewAppError("addUserToGroupSyncables", "api.user.add_user_to_group_syncables.not_ldap_user.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("addUserToGroupSyncables", audit.Fail)
defer c.LogAuditRec(auditRec)
params := model.CreateDefaultMembershipParams{Since: 0, ReAddRemovedMembers: true, ScopedUserID: &user.Id}
err := c.App.CreateDefaultMemberships(c.AppContext, params)
if err != nil {
c.Err = model.NewAppError("addUserToGroupSyncables", "api.admin.syncables_error", nil, err.Error(), http.StatusBadRequest)
return
}
auditRec.Success()
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitLdapLocal() {
api.BaseRoutes.LDAP.Handle("/migrateid", api.APILocal(migrateIdLdap)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/sync", api.APILocal(syncLdap)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/test", api.APILocal(testLdap)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/groups", api.APILocal(getLdapGroups)).Methods("GET")
api.BaseRoutes.LDAP.Handle("/certificate/public", api.APILocal(addLdapPublicCertificate)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/certificate/private", api.APILocal(addLdapPrivateCertificate)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/certificate/public", api.APILocal(removeLdapPublicCertificate)).Methods("DELETE")
api.BaseRoutes.LDAP.Handle("/certificate/private", api.APILocal(removeLdapPrivateCertificate)).Methods("DELETE")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
b64 "encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
)
func (api *API) InitLicense() {
api.BaseRoutes.APIRoot.Handle("/trial-license", api.APISessionRequired(requestTrialLicense)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/trial-license/prev", api.APISessionRequired(getPrevTrialLicense)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/license", api.APISessionRequired(addLicense)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/license", api.APISessionRequired(removeLicense)).Methods("DELETE")
api.BaseRoutes.APIRoot.Handle("/license/renewal", api.APISessionRequired(requestRenewalLink)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/license/client", api.APIHandler(getClientLicense)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/license/review", api.APISessionRequired(requestTrueUpReview)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/license/review/status", api.APISessionRequired(trueUpReviewStatus)).Methods("GET")
}
func getClientLicense(c *Context, w http.ResponseWriter, r *http.Request) {
format := r.URL.Query().Get("format")
if format == "" {
c.Err = model.NewAppError("getClientLicense", "api.license.client.old_format.app_error", nil, "", http.StatusBadRequest)
return
}
if format != "old" {
c.SetInvalidParam("format")
return
}
var clientLicense map[string]string
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadLicenseInformation) {
clientLicense = c.App.Srv().ClientLicense()
} else {
clientLicense = c.App.Srv().GetSanitizedClientLicense()
}
w.Write([]byte(model.MapToJSON(clientLicense)))
}
func addLicense(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("addLicense", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("addLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
m := r.MultipartForm
fileArray, ok := m.File["license"]
if !ok {
c.Err = model.NewAppError("addLicense", "api.license.add_license.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(fileArray) <= 0 {
c.Err = model.NewAppError("addLicense", "api.license.add_license.array.app_error", nil, "", http.StatusBadRequest)
return
}
fileData := fileArray[0]
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
file, err := fileData.Open()
if err != nil {
c.Err = model.NewAppError("addLicense", "api.license.add_license.open.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
defer file.Close()
buf := bytes.NewBuffer(nil)
io.Copy(buf, file)
licenseBytes := buf.Bytes()
license, appErr := utils.LicenseValidator.LicenseFromBytes(licenseBytes)
if appErr != nil {
c.Err = appErr
return
}
// skip the restrictions if license is a sanctioned trial
if !license.IsSanctionedTrial() && license.IsTrialLicense() {
lm := c.App.Srv().Platform().LicenseManager()
if lm == nil {
c.Err = model.NewAppError("addLicense", "api.license.upgrade_needed.app_error", nil, "", http.StatusInternalServerError)
return
}
canStartTrialLicense, err := lm.CanStartTrial()
if err != nil {
c.Err = model.NewAppError("addLicense", "api.license.add_license.open.app_error", nil, "", http.StatusInternalServerError)
return
}
if !canStartTrialLicense {
c.Err = model.NewAppError("addLicense", "api.license.request-trial.can-start-trial.not-allowed", nil, "", http.StatusBadRequest)
return
}
}
license, appErr = c.App.Srv().SaveLicense(licenseBytes)
if appErr != nil {
if appErr.Id == model.ExpiredLicenseError {
c.LogAudit("failed - expired or non-started license")
} else if appErr.Id == model.InvalidLicenseError {
c.LogAudit("failed - invalid license")
} else {
c.LogAudit("failed - unable to save license")
}
c.Err = appErr
return
}
if c.App.Channels().License().IsCloud() {
// If cloud, invalidate the caches when a new license is loaded
defer c.App.Srv().Cloud.HandleLicenseChange()
}
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(license); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func removeLicense(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("removeLicense", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("removeLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
if err := c.App.Srv().RemoveLicense(); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}
func requestTrialLicense(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("requestTrialLicense", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("requestTrialLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
if c.App.Srv().Platform().LicenseManager() == nil {
c.Err = model.NewAppError("requestTrialLicense", "api.license.upgrade_needed.app_error", nil, "", http.StatusForbidden)
return
}
canStartTrialLicense, err := c.App.Srv().Platform().LicenseManager().CanStartTrial()
if err != nil {
c.Err = model.NewAppError("requestTrialLicense", "api.license.request-trial.can-start-trial.error", nil, err.Error(), http.StatusInternalServerError)
return
}
if !canStartTrialLicense {
c.Err = model.NewAppError("requestTrialLicense", "api.license.request-trial.can-start-trial.not-allowed", nil, "", http.StatusBadRequest)
return
}
var trialRequest struct {
Users int `json:"users"`
TermsAccepted bool `json:"terms_accepted"`
ReceiveEmailsAccepted bool `json:"receive_emails_accepted"`
}
b, readErr := io.ReadAll(r.Body)
if readErr != nil {
c.Err = model.NewAppError("requestTrialLicense", "api.license.request-trial.bad-request", nil, "", http.StatusBadRequest)
return
}
json.Unmarshal(b, &trialRequest)
if err := c.App.Channels().RequestTrialLicense(c.AppContext.Session().UserId, trialRequest.Users, trialRequest.TermsAccepted, trialRequest.ReceiveEmailsAccepted); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}
func requestRenewalLink(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("requestRenewalLink", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("requestRenewalLink", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
renewalLink, token, err := c.App.Srv().GenerateLicenseRenewalLink()
if err != nil {
c.Err = err
return
}
if c.App.Cloud() == nil {
c.Err = model.NewAppError("requestRenewalLink", "api.license.upgrade_needed.app_error", nil, "", http.StatusForbidden)
return
}
// check if it is possible to renew license on the portal with generated token
status, e := c.App.Cloud().GetLicenseSelfServeStatus(c.AppContext.Session().UserId, token)
if e != nil {
c.Err = model.NewAppError("requestRenewalLink", "api.license.request_renewal_link.cannot_renew_on_cws", nil, e.Error(), http.StatusInternalServerError)
return
}
if !status.IsRenewable {
c.Err = model.NewAppError("requestRenewalLink", "api.license.request_renewal_link.cannot_renew_on_cws", nil, "License is not self-serve renewable", http.StatusBadRequest)
return
}
auditRec.Success()
c.LogAudit("success")
_, werr := w.Write([]byte(fmt.Sprintf(`{"renewal_link": "%s"}`, renewalLink)))
if werr != nil {
c.Err = model.NewAppError("requestRenewalLink", "api.license.request_renewal_link.app_error", nil, werr.Error(), http.StatusForbidden)
return
}
}
func getPrevTrialLicense(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Srv().Platform().LicenseManager() == nil {
c.Err = model.NewAppError("getPrevTrialLicense", "api.license.upgrade_needed.app_error", nil, "", http.StatusForbidden)
return
}
license, err := c.App.Srv().Platform().LicenseManager().GetPrevTrial()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var clientLicense map[string]string
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadLicenseInformation) {
clientLicense = utils.GetClientLicense(license)
} else {
clientLicense = utils.GetSanitizedClientLicense(utils.GetClientLicense(license))
}
w.Write([]byte(model.MapToJSON(clientLicense)))
}
func requestTrueUpReview(c *Context, w http.ResponseWriter, r *http.Request) {
// Only admins can request a true up review.
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
license := c.App.Channels().License()
if license == nil {
c.Err = model.NewAppError("requestTrueUpReview", "api.license.true_up_review.license_required", nil, "", http.StatusNotImplemented)
return
}
if license.IsCloud() {
c.Err = model.NewAppError("requestTrueUpReview", "api.license.true_up_review.not_allowed_for_cloud", nil, "", http.StatusNotImplemented)
return
}
status, appErr := c.App.GetOrCreateTrueUpReviewStatus()
if appErr != nil {
c.Err = appErr
return
}
// If a true up review has already been submitted for the current due date, complete the request
// with no errors.
if status.Completed {
ReturnStatusOK(w)
}
profileMap, err := c.App.GetTrueUpProfile()
if err != nil {
c.Err = model.NewAppError("requestTrueUpReview", "api.license.true_up_review.get_status_error", nil, "", http.StatusInternalServerError)
return
}
profileMapJson, err := json.Marshal(profileMap)
if err != nil {
c.SetJSONEncodingError(err)
return
}
// Do not send true-up review data if the user has already requested one for the quarter.
// And only send a true-up review via as a one-time telemetry request if telemetry is disabled.
telemetryEnabled := c.App.Config().LogSettings.EnableDiagnostics
if telemetryEnabled != nil && !*telemetryEnabled {
// Send telemetry data
c.App.Srv().GetTelemetryService().SendTelemetry(model.TrueUpReviewTelemetryName, profileMap)
// Update the review status to reflect the completion.
status.Completed = true
c.App.Srv().Store().TrueUpReview().Update(status)
}
// Encode to string rather than byte[] otherwise json.Marshal will encode it further.
encodedData := b64.StdEncoding.EncodeToString(profileMapJson)
responseContent := struct {
Content string `json:"content"`
}{Content: encodedData}
response, _ := json.Marshal(responseContent)
w.Write(response)
}
func trueUpReviewStatus(c *Context, w http.ResponseWriter, r *http.Request) {
// Only admins can request a true up review.
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
// Check for license
license := c.App.Channels().License()
if license == nil {
c.Err = model.NewAppError("cloudTrueUpReviewNotAllowed", "api.license.true_up_review.license_required", nil, "True up review requires a license", http.StatusNotImplemented)
return
}
if license.IsCloud() {
c.Err = model.NewAppError("cloudTrueUpReviewNotAllowed", "api.license.true_up_review.not_allowed_for_cloud", nil, "True up review is not allowed for cloud instances", http.StatusNotImplemented)
return
}
status, appErr := c.App.GetOrCreateTrueUpReviewStatus()
if appErr != nil {
c.Err = appErr
}
json, err := json.Marshal(status)
if err != nil {
c.Err = model.NewAppError("trueUpReviewStatus", "api.marshal_error", nil, "", http.StatusInternalServerError)
return
}
w.Write(json)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"encoding/json"
"io"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitLicenseLocal() {
api.BaseRoutes.APIRoot.Handle("/license", api.APILocal(localAddLicense)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/license", api.APILocal(localRemoveLicense)).Methods("DELETE")
}
func localAddLicense(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("localAddLicense", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
m := r.MultipartForm
fileArray, ok := m.File["license"]
if !ok {
c.Err = model.NewAppError("addLicense", "api.license.add_license.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(fileArray) <= 0 {
c.Err = model.NewAppError("addLicense", "api.license.add_license.array.app_error", nil, "", http.StatusBadRequest)
return
}
fileData := fileArray[0]
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
file, err := fileData.Open()
if err != nil {
c.Err = model.NewAppError("addLicense", "api.license.add_license.open.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
defer file.Close()
buf := bytes.NewBuffer(nil)
io.Copy(buf, file)
license, appErr := c.App.Srv().SaveLicense(buf.Bytes())
if appErr != nil {
if appErr.Id == model.ExpiredLicenseError {
c.LogAudit("failed - expired or non-started license")
} else if appErr.Id == model.InvalidLicenseError {
c.LogAudit("failed - invalid license")
} else {
c.LogAudit("failed - unable to save license")
}
c.Err = appErr
return
}
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(license); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localRemoveLicense(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("localRemoveLicense", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if err := c.App.Srv().RemoveLicense(); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
)
func handleNotifyAdmin(c *Context, w http.ResponseWriter, r *http.Request) {
var notifyAdminRequest *model.NotifyAdminToUpgradeRequest
err := json.NewDecoder(r.Body).Decode(¬ifyAdminRequest)
if err != nil {
c.SetInvalidParamWithErr("notifyAdminRequest", err)
return
}
userId := c.AppContext.Session().UserId
appErr := c.App.SaveAdminNotification(userId, notifyAdminRequest)
if appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
func handleTriggerNotifyAdminPosts(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().ServiceSettings.EnableAPITriggerAdminNotifications {
c.Err = model.NewAppError("Api4.handleTriggerNotifyAdminPosts", "api.cloud.app_error", nil, "Manual triggering of notifications not allowed", http.StatusForbidden)
return
}
var notifyAdminRequest *model.NotifyAdminToUpgradeRequest
err := json.NewDecoder(r.Body).Decode(¬ifyAdminRequest)
if err != nil {
c.SetInvalidParamWithErr("notifyAdminRequest", err)
return
}
// only system admins can manually trigger these notifications
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
appErr := c.App.SendNotifyAdminPosts(c.AppContext, "", "", notifyAdminRequest.TrialNotification)
if appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitOAuth() {
api.BaseRoutes.OAuthApps.Handle("", api.APISessionRequired(createOAuthApp)).Methods("POST")
api.BaseRoutes.OAuthApp.Handle("", api.APISessionRequired(updateOAuthApp)).Methods("PUT")
api.BaseRoutes.OAuthApps.Handle("", api.APISessionRequired(getOAuthApps)).Methods("GET")
api.BaseRoutes.OAuthApp.Handle("", api.APISessionRequired(getOAuthApp)).Methods("GET")
api.BaseRoutes.OAuthApp.Handle("/info", api.APISessionRequired(getOAuthAppInfo)).Methods("GET")
api.BaseRoutes.OAuthApp.Handle("", api.APISessionRequired(deleteOAuthApp)).Methods("DELETE")
api.BaseRoutes.OAuthApp.Handle("/regen_secret", api.APISessionRequired(regenerateOAuthAppSecret)).Methods("POST")
api.BaseRoutes.User.Handle("/oauth/apps/authorized", api.APISessionRequired(getAuthorizedOAuthApps)).Methods("GET")
}
func createOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
var oauthApp model.OAuthApp
if jsonErr := json.NewDecoder(r.Body).Decode(&oauthApp); jsonErr != nil {
c.SetInvalidParamWithErr("oauth_app", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createOAuthApp", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "oauth_app", &oauthApp)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
c.SetPermissionError(model.PermissionManageOAuth)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
oauthApp.IsTrusted = false
}
oauthApp.CreatorId = c.AppContext.Session().UserId
rapp, err := c.App.CreateOAuthApp(&oauthApp)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(rapp)
auditRec.AddEventObjectType("oauth_app")
c.LogAudit("client_id=" + rapp.Id)
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rapp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireAppId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("updateOAuthApp", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "oauth_app_id", c.Params.AppId)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
c.SetPermissionError(model.PermissionManageOAuth)
return
}
var oauthApp model.OAuthApp
if jsonErr := json.NewDecoder(r.Body).Decode(&oauthApp); jsonErr != nil {
c.SetInvalidParamWithErr("oauth_app", jsonErr)
return
}
audit.AddEventParameterAuditable(auditRec, "oauth_app", &oauthApp)
// The app being updated in the payload must be the same one as indicated in the URL.
if oauthApp.Id != c.Params.AppId {
c.SetInvalidParam("app_id")
return
}
oldOAuthApp, err := c.App.GetOAuthApp(c.Params.AppId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(oldOAuthApp)
if c.AppContext.Session().UserId != oldOAuthApp.CreatorId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
c.SetPermissionError(model.PermissionManageSystemWideOAuth)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
oauthApp.IsTrusted = oldOAuthApp.IsTrusted
}
updatedOAuthApp, err := c.App.UpdateOAuthApp(oldOAuthApp, &oauthApp)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(updatedOAuthApp)
auditRec.AddEventObjectType("oauth_app")
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(updatedOAuthApp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getOAuthApps(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
c.Err = model.NewAppError("getOAuthApps", "api.command.admin_only.app_error", nil, "", http.StatusForbidden)
return
}
var apps []*model.OAuthApp
var appErr *model.AppError
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
apps, appErr = c.App.GetOAuthApps(c.Params.Page, c.Params.PerPage)
} else if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
apps, appErr = c.App.GetOAuthAppsByCreator(c.AppContext.Session().UserId, c.Params.Page, c.Params.PerPage)
} else {
c.SetPermissionError(model.PermissionManageOAuth)
return
}
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(apps)
if err != nil {
c.Err = model.NewAppError("getOAuthApps", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireAppId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
c.SetPermissionError(model.PermissionManageOAuth)
return
}
oauthApp, err := c.App.GetOAuthApp(c.Params.AppId)
if err != nil {
c.Err = err
return
}
if oauthApp.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
c.SetPermissionError(model.PermissionManageSystemWideOAuth)
return
}
if err := json.NewEncoder(w).Encode(oauthApp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getOAuthAppInfo(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireAppId()
if c.Err != nil {
return
}
oauthApp, err := c.App.GetOAuthApp(c.Params.AppId)
if err != nil {
c.Err = err
return
}
oauthApp.Sanitize()
if err := json.NewEncoder(w).Encode(oauthApp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireAppId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("deleteOAuthApp", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "oauth_app_id", c.Params.AppId)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
c.SetPermissionError(model.PermissionManageOAuth)
return
}
oauthApp, err := c.App.GetOAuthApp(c.Params.AppId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(oauthApp)
auditRec.AddEventObjectType("oauth_app")
if c.AppContext.Session().UserId != oauthApp.CreatorId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
c.SetPermissionError(model.PermissionManageSystemWideOAuth)
return
}
err = c.App.DeleteOAuthApp(oauthApp.Id)
if err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}
func regenerateOAuthAppSecret(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireAppId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("regenerateOAuthAppSecret", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "oauth_app_id", c.Params.AppId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
c.SetPermissionError(model.PermissionManageOAuth)
return
}
oauthApp, err := c.App.GetOAuthApp(c.Params.AppId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(oauthApp)
auditRec.AddEventObjectType("oauth_app")
if oauthApp.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
c.SetPermissionError(model.PermissionManageSystemWideOAuth)
return
}
oauthApp, err = c.App.RegenerateOAuthAppSecret(oauthApp)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(oauthApp)
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(oauthApp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getAuthorizedOAuthApps(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
apps, appErr := c.App.GetAuthorizedAppsForUser(c.Params.UserId, c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(apps)
if err != nil {
c.Err = model.NewAppError("getAuthorizedOAuthApps", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitOpenGraph() {
api.BaseRoutes.OpenGraph.Handle("", api.APISessionRequired(getOpenGraphMetadata)).Methods("POST")
}
func getOpenGraphMetadata(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().ServiceSettings.EnableLinkPreviews {
c.Err = model.NewAppError("getOpenGraphMetadata", "api.post.link_preview_disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
props := model.StringInterfaceFromJSON(r.Body)
url := ""
ok := false
if url, ok = props["url"].(string); url == "" || !ok {
c.SetInvalidParam("url")
return
}
buf, err := c.App.GetOpenGraphMetadata(url)
if err != nil {
mlog.Warn("GetOpenGraphMetadata request failed",
mlog.String("requestURL", url),
mlog.Err(err))
w.Write([]byte(`{"url": ""}`))
return
}
w.Write(buf)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
)
func (api *API) InitPermissions() {
api.BaseRoutes.Permissions.Handle("/ancillary", api.APISessionRequired(appendAncillaryPermissions)).Methods("GET")
}
func appendAncillaryPermissions(c *Context, w http.ResponseWriter, r *http.Request) {
keys, ok := r.URL.Query()["subsection_permissions"]
if !ok || len(keys[0]) < 1 {
c.SetInvalidURLParam("subsection_permissions")
return
}
permissions := strings.Split(keys[0], ",")
b, err := json.Marshal(model.AddAncillaryPermissions(permissions))
if err != nil {
c.SetJSONEncodingError(err)
return
}
w.Write(b)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/url"
"strconv"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
MaximumPluginFileSize = 50 * 1024 * 1024
)
func (api *API) InitPlugin() {
api.BaseRoutes.Plugins.Handle("", api.APISessionRequired(uploadPlugin)).Methods("POST")
api.BaseRoutes.Plugins.Handle("", api.APISessionRequired(getPlugins)).Methods("GET")
api.BaseRoutes.Plugin.Handle("", api.APISessionRequired(removePlugin)).Methods("DELETE")
api.BaseRoutes.Plugins.Handle("/install_from_url", api.APISessionRequired(installPluginFromURL)).Methods("POST")
api.BaseRoutes.Plugins.Handle("/marketplace", api.APISessionRequired(installMarketplacePlugin)).Methods("POST")
api.BaseRoutes.Plugins.Handle("/statuses", api.APISessionRequired(getPluginStatuses)).Methods("GET")
api.BaseRoutes.Plugin.Handle("/enable", api.APISessionRequired(enablePlugin)).Methods("POST")
api.BaseRoutes.Plugin.Handle("/disable", api.APISessionRequired(disablePlugin)).Methods("POST")
api.BaseRoutes.Plugins.Handle("/webapp", api.APIHandler(getWebappPlugins)).Methods("GET")
api.BaseRoutes.Plugins.Handle("/marketplace", api.APISessionRequired(getMarketplacePlugins)).Methods("GET")
api.BaseRoutes.Plugins.Handle("/marketplace/first_admin_visit", api.APIHandler(setFirstAdminVisitMarketplaceStatus)).Methods("POST")
api.BaseRoutes.Plugins.Handle("/marketplace/first_admin_visit", api.APISessionRequired(getFirstAdminVisitMarketplaceStatus)).Methods("GET")
}
func uploadPlugin(c *Context, w http.ResponseWriter, r *http.Request) {
config := c.App.Config()
if !*config.PluginSettings.Enable || !*config.PluginSettings.EnableUploads || *config.PluginSettings.RequirePluginSignature {
c.Err = model.NewAppError("uploadPlugin", "app.plugin.upload_disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec := c.MakeAuditRecord("uploadPlugin", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
return
}
if err := r.ParseMultipartForm(MaximumPluginFileSize); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
m := r.MultipartForm
pluginArray, ok := m.File["plugin"]
if !ok {
c.Err = model.NewAppError("uploadPlugin", "api.plugin.upload.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(pluginArray) <= 0 {
c.Err = model.NewAppError("uploadPlugin", "api.plugin.upload.array.app_error", nil, "", http.StatusBadRequest)
return
}
audit.AddEventParameter(auditRec, "filename", pluginArray[0].Filename)
file, err := pluginArray[0].Open()
if err != nil {
c.Err = model.NewAppError("uploadPlugin", "api.plugin.upload.file.app_error", nil, "", http.StatusBadRequest)
return
}
defer file.Close()
force := false
if len(m.Value["force"]) > 0 && m.Value["force"][0] == "true" {
force = true
}
installPlugin(c, w, file, force)
auditRec.Success()
}
func installPluginFromURL(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().PluginSettings.Enable ||
*c.App.Config().PluginSettings.RequirePluginSignature ||
!*c.App.Config().PluginSettings.EnableUploads {
c.Err = model.NewAppError("installPluginFromURL", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec := c.MakeAuditRecord("installPluginFromURL", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
return
}
force, _ := strconv.ParseBool(r.URL.Query().Get("force"))
downloadURL := r.URL.Query().Get("plugin_download_url")
audit.AddEventParameter(auditRec, "url", downloadURL)
pluginFileBytes, err := c.App.DownloadFromURL(downloadURL)
if err != nil {
c.Err = model.NewAppError("installPluginFromURL", "api.plugin.install.download_failed.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
installPlugin(c, w, bytes.NewReader(pluginFileBytes), force)
auditRec.Success()
}
func installMarketplacePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("installMarketplacePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
if !*c.App.Config().PluginSettings.EnableMarketplace {
c.Err = model.NewAppError("installMarketplacePlugin", "app.plugin.marketplace_disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec := c.MakeAuditRecord("installMarketplacePlugin", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
return
}
pluginRequest, err := model.PluginRequestFromReader(r.Body)
if err != nil {
c.Err = model.NewAppError("installMarketplacePlugin", "app.plugin.marketplace_plugin_request.app_error", nil, err.Error(), http.StatusNotImplemented)
return
}
audit.AddEventParameter(auditRec, "plugin_id", pluginRequest.Id)
// Always install the latest compatible version
// https://mattermost.atlassian.net/browse/MM-41981
pluginRequest.Version = ""
manifest, appErr := c.App.Channels().InstallMarketplacePlugin(pluginRequest)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddMeta("plugin_name", manifest.Name)
auditRec.AddMeta("plugin_desc", manifest.Description)
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(manifest); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPlugins(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("getPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadPlugins) {
c.SetPermissionError(model.PermissionSysconsoleReadPlugins)
return
}
response, err := c.App.GetPlugins()
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(response); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPluginStatuses(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("getPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadPlugins) {
c.SetPermissionError(model.PermissionSysconsoleReadPlugins)
return
}
response, err := c.App.GetClusterPluginStatuses()
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(response); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func removePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePluginId()
if c.Err != nil {
return
}
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("removePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec := c.MakeAuditRecord("removePlugin", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "plugin_id", c.Params.PluginId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
return
}
err := c.App.Channels().RemovePlugin(c.Params.PluginId)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getWebappPlugins(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("getWebappPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
manifests, appErr := c.App.GetActivePluginManifests()
if appErr != nil {
c.Err = appErr
return
}
clientManifests := []*model.Manifest{}
for _, m := range manifests {
if m.HasClient() {
manifest := m.ClientManifest()
// There is no reason to expose the SettingsSchema in this API call; it's not used in the webapp.
manifest.SettingsSchema = nil
clientManifests = append(clientManifests, manifest)
}
}
js, err := json.Marshal(clientManifests)
if err != nil {
c.Err = model.NewAppError("getWebappPlugins", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getMarketplacePlugins(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("getMarketplacePlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
if !*c.App.Config().PluginSettings.EnableMarketplace {
c.Err = model.NewAppError("getMarketplacePlugins", "app.plugin.marketplace_disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
filter, err := parseMarketplacePluginFilter(r.URL)
if err != nil {
c.Err = model.NewAppError("getMarketplacePlugins", "app.plugin.marshal.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
// if we are looking for remote only, we don't need to check for permissions
if !filter.RemoteOnly && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadPlugins) {
c.SetPermissionError(model.PermissionSysconsoleReadPlugins)
return
}
plugins, appErr := c.App.GetMarketplacePlugins(filter)
if appErr != nil {
c.Err = appErr
return
}
json, err := json.Marshal(plugins)
if err != nil {
c.Err = model.NewAppError("getMarketplacePlugins", "app.plugin.marshal.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func enablePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePluginId()
if c.Err != nil {
return
}
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("activatePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec := c.MakeAuditRecord("enablePlugin", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "plugin_id", c.Params.PluginId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
return
}
if err := c.App.EnablePlugin(c.Params.PluginId); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func disablePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePluginId()
if c.Err != nil {
return
}
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("deactivatePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec := c.MakeAuditRecord("disablePlugin", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "plugin_id", c.Params.PluginId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
return
}
if err := c.App.DisablePlugin(c.Params.PluginId); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func parseMarketplacePluginFilter(u *url.URL) (*model.MarketplacePluginFilter, error) {
page, err := parseInt(u, "page", 0)
if err != nil {
return nil, err
}
perPage, err := parseInt(u, "per_page", 100)
if err != nil {
return nil, err
}
filter := u.Query().Get("filter")
serverVersion := u.Query().Get("server_version")
localOnly, _ := strconv.ParseBool(u.Query().Get("local_only"))
remoteOnly, _ := strconv.ParseBool(u.Query().Get("remote_only"))
if localOnly && remoteOnly {
return nil, errors.New("local_only and remote_only cannot be both true")
}
return &model.MarketplacePluginFilter{
Page: page,
PerPage: perPage,
Filter: filter,
ServerVersion: serverVersion,
LocalOnly: localOnly,
RemoteOnly: remoteOnly,
}, nil
}
func installPlugin(c *Context, w http.ResponseWriter, plugin io.ReadSeeker, force bool) {
manifest, appErr := c.App.InstallPlugin(plugin, force)
if appErr != nil {
c.Err = appErr
return
}
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(manifest); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func setFirstAdminVisitMarketplaceStatus(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("setFirstAdminVisitMarketplaceStatus", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
firstAdminVisitMarketplaceObj := model.System{
Name: model.SystemFirstAdminVisitMarketplace,
Value: "true",
}
if err := c.App.Srv().Store().System().SaveOrUpdate(&firstAdminVisitMarketplaceObj); err != nil {
c.Err = model.NewAppError("setFirstAdminVisitMarketplaceStatus", "api.error_set_first_admin_visit_marketplace_status", nil, err.Error(), http.StatusInternalServerError)
return
}
message := model.NewWebSocketEvent(model.WebsocketFirstAdminVisitMarketplaceStatusReceived, "", "", "", nil, "")
message.Add("firstAdminVisitMarketplaceStatus", firstAdminVisitMarketplaceObj.Value)
c.App.Publish(message)
auditRec.Success()
ReturnStatusOK(w)
}
func getFirstAdminVisitMarketplaceStatus(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("getFirstAdminVisitMarketplaceStatus", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
firstAdminVisitMarketplaceObj, err := c.App.Srv().Store().System().GetByName(model.SystemFirstAdminVisitMarketplace)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
firstAdminVisitMarketplaceObj = &model.System{
Name: model.SystemFirstAdminVisitMarketplace,
Value: "false",
}
default:
c.Err = model.NewAppError("getFirstAdminVisitMarketplaceStatus", "api.error_get_first_admin_visit_marketplace_status", nil, err.Error(), http.StatusInternalServerError)
return
}
}
auditRec.Success()
if err := json.NewEncoder(w).Encode(firstAdminVisitMarketplaceObj); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitPluginLocal() {
api.BaseRoutes.Plugins.Handle("", api.APILocal(uploadPlugin)).Methods("POST")
api.BaseRoutes.Plugins.Handle("", api.APILocal(getPlugins)).Methods("GET")
api.BaseRoutes.Plugins.Handle("/install_from_url", api.APILocal(installPluginFromURL)).Methods("POST")
api.BaseRoutes.Plugin.Handle("", api.APILocal(removePlugin)).Methods("DELETE")
api.BaseRoutes.Plugin.Handle("/enable", api.APILocal(enablePlugin)).Methods("POST")
api.BaseRoutes.Plugin.Handle("/disable", api.APILocal(disablePlugin)).Methods("POST")
api.BaseRoutes.Plugins.Handle("/marketplace", api.APILocal(installMarketplacePlugin)).Methods("POST")
api.BaseRoutes.Plugins.Handle("/marketplace", api.APILocal(getMarketplacePlugins)).Methods("GET")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitPost() {
api.BaseRoutes.Posts.Handle("", api.APISessionRequired(createPost)).Methods("POST")
api.BaseRoutes.Post.Handle("", api.APISessionRequired(getPost)).Methods("GET")
api.BaseRoutes.Post.Handle("", api.APISessionRequired(deletePost)).Methods("DELETE")
api.BaseRoutes.Posts.Handle("/ids", api.APISessionRequired(getPostsByIds)).Methods("POST")
api.BaseRoutes.Posts.Handle("/ephemeral", api.APISessionRequired(createEphemeralPost)).Methods("POST")
api.BaseRoutes.Post.Handle("/edit_history", api.APISessionRequired(getEditHistoryForPost)).Methods("GET")
api.BaseRoutes.Post.Handle("/thread", api.APISessionRequired(getPostThread)).Methods("GET")
api.BaseRoutes.Post.Handle("/info", api.APISessionRequired(getPostInfo)).Methods("GET")
api.BaseRoutes.Post.Handle("/files/info", api.APISessionRequired(getFileInfosForPost)).Methods("GET")
api.BaseRoutes.PostsForChannel.Handle("", api.APISessionRequired(getPostsForChannel)).Methods("GET")
api.BaseRoutes.PostsForUser.Handle("/flagged", api.APISessionRequired(getFlaggedPostsForUser)).Methods("GET")
api.BaseRoutes.ChannelForUser.Handle("/posts/unread", api.APISessionRequired(getPostsForChannelAroundLastUnread)).Methods("GET")
api.BaseRoutes.Team.Handle("/posts/search", api.APISessionRequiredDisableWhenBusy(searchPostsInTeam)).Methods("POST")
api.BaseRoutes.Posts.Handle("/search", api.APISessionRequiredDisableWhenBusy(searchPostsInAllTeams)).Methods("POST")
api.BaseRoutes.Post.Handle("", api.APISessionRequired(updatePost)).Methods("PUT")
api.BaseRoutes.Post.Handle("/patch", api.APISessionRequired(patchPost)).Methods("PUT")
api.BaseRoutes.PostForUser.Handle("/set_unread", api.APISessionRequired(setPostUnread)).Methods("POST")
api.BaseRoutes.PostForUser.Handle("/reminder", api.APISessionRequired(setPostReminder)).Methods("POST")
api.BaseRoutes.Post.Handle("/pin", api.APISessionRequired(pinPost)).Methods("POST")
api.BaseRoutes.Post.Handle("/unpin", api.APISessionRequired(unpinPost)).Methods("POST")
api.BaseRoutes.PostForUser.Handle("/ack", api.APISessionRequired(acknowledgePost)).Methods("POST")
api.BaseRoutes.PostForUser.Handle("/ack", api.APISessionRequired(unacknowledgePost)).Methods("DELETE")
}
func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
var post model.Post
if jsonErr := json.NewDecoder(r.Body).Decode(&post); jsonErr != nil {
c.SetInvalidParamWithErr("post", jsonErr)
return
}
// Strip away delete_at if passed
post.DeleteAt = 0
post.UserId = c.AppContext.Session().UserId
auditRec := c.MakeAuditRecord("createPost", audit.Fail)
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
audit.AddEventParameterAuditable(auditRec, "post", &post)
hasPermission := false
if c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), post.ChannelId, model.PermissionCreatePost) {
hasPermission = true
} else if channel, err := c.App.GetChannel(c.AppContext, post.ChannelId); err == nil {
// Temporary permission check method until advanced permissions, please do not copy
if channel.Type == model.ChannelTypeOpen && c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionCreatePostPublic) {
hasPermission = true
}
}
if !hasPermission {
c.SetPermissionError(model.PermissionCreatePost)
return
}
if post.CreateAt != 0 && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
post.CreateAt = 0
}
setOnline := r.URL.Query().Get("set_online")
setOnlineBool := true // By default, always set online.
var err2 error
if setOnline != "" {
setOnlineBool, err2 = strconv.ParseBool(setOnline)
if err2 != nil {
mlog.Warn("Failed to parse set_online URL query parameter from createPost request", mlog.Err(err2))
setOnlineBool = true // Set online nevertheless.
}
}
rp, err := c.App.CreatePostAsUser(c.AppContext, c.App.PostWithProxyRemovedFromImageURLs(&post), c.AppContext.Session().Id, setOnlineBool)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(rp)
auditRec.AddEventObjectType("post")
if setOnlineBool {
c.App.SetStatusOnline(c.AppContext.Session().UserId, false)
}
c.App.Srv().Platform().UpdateLastActivityAtIfNeeded(*c.AppContext.Session())
c.ExtendSessionExpiryIfNeeded(w, r)
w.WriteHeader(http.StatusCreated)
// Note that rp has already had PreparePostForClient called on it by App.CreatePost
if err := rp.EncodeJSON(w); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func createEphemeralPost(c *Context, w http.ResponseWriter, r *http.Request) {
ephRequest := model.PostEphemeral{}
jsonErr := json.NewDecoder(r.Body).Decode(&ephRequest)
if jsonErr != nil {
c.SetInvalidParamWithErr("body", jsonErr)
return
}
if ephRequest.UserID == "" {
c.SetInvalidParam("user_id")
return
}
if ephRequest.Post == nil {
c.SetInvalidParam("post")
return
}
ephRequest.Post.UserId = c.AppContext.Session().UserId
ephRequest.Post.CreateAt = model.GetMillis()
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreatePostEphemeral) {
c.SetPermissionError(model.PermissionCreatePostEphemeral)
return
}
rp := c.App.SendEphemeralPost(c.AppContext, ephRequest.UserID, c.App.PostWithProxyRemovedFromImageURLs(ephRequest.Post))
w.WriteHeader(http.StatusCreated)
rp = model.AddPostActionCookies(rp, c.App.PostActionCookieSecret())
rp = c.App.PreparePostForClientWithEmbedsAndImages(c.AppContext, rp, true, false, true)
rp, err := c.App.SanitizePostMetadataForUser(c.AppContext, rp, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
if err := rp.EncodeJSON(w); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func getPostsForChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
afterPost := r.URL.Query().Get("after")
if afterPost != "" && !model.IsValidId(afterPost) {
c.SetInvalidParam("after")
return
}
beforePost := r.URL.Query().Get("before")
if beforePost != "" && !model.IsValidId(beforePost) {
c.SetInvalidParam("before")
return
}
sinceString := r.URL.Query().Get("since")
var since int64
var parseError error
if sinceString != "" {
since, parseError = strconv.ParseInt(sinceString, 10, 64)
if parseError != nil {
c.SetInvalidParam("since")
return
}
}
skipFetchThreads := r.URL.Query().Get("skipFetchThreads") == "true"
collapsedThreads := r.URL.Query().Get("collapsedThreads") == "true"
collapsedThreadsExtended := r.URL.Query().Get("collapsedThreadsExtended") == "true"
includeDeleted := r.URL.Query().Get("include_deleted") == "true"
channelId := c.Params.ChannelId
page := c.Params.Page
perPage := c.Params.PerPage
if !c.IsSystemAdmin() && includeDeleted {
c.SetPermissionError(model.PermissionReadDeletedPosts)
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if !*c.App.Config().TeamSettings.ExperimentalViewArchivedChannels {
channel, err := c.App.GetChannel(c.AppContext, channelId)
if err != nil {
c.Err = err
return
}
if channel.DeleteAt != 0 {
c.Err = model.NewAppError("Api4.getPostsForChannel", "api.user.view_archived_channels.get_posts_for_channel.app_error", nil, "", http.StatusForbidden)
return
}
}
var list *model.PostList
var err *model.AppError
etag := ""
if since > 0 {
list, err = c.App.GetPostsSince(model.GetPostsSinceOptions{ChannelId: channelId, Time: since, SkipFetchThreads: skipFetchThreads, CollapsedThreads: collapsedThreads, CollapsedThreadsExtended: collapsedThreadsExtended, UserId: c.AppContext.Session().UserId})
} else if afterPost != "" {
etag = c.App.GetPostsEtag(channelId, collapsedThreads)
if c.HandleEtag(etag, "Get Posts After", w, r) {
return
}
list, err = c.App.GetPostsAfterPost(model.GetPostsOptions{ChannelId: channelId, PostId: afterPost, Page: page, PerPage: perPage, SkipFetchThreads: skipFetchThreads, CollapsedThreads: collapsedThreads, UserId: c.AppContext.Session().UserId, IncludeDeleted: includeDeleted})
} else if beforePost != "" {
etag = c.App.GetPostsEtag(channelId, collapsedThreads)
if c.HandleEtag(etag, "Get Posts Before", w, r) {
return
}
list, err = c.App.GetPostsBeforePost(model.GetPostsOptions{ChannelId: channelId, PostId: beforePost, Page: page, PerPage: perPage, SkipFetchThreads: skipFetchThreads, CollapsedThreads: collapsedThreads, CollapsedThreadsExtended: collapsedThreadsExtended, UserId: c.AppContext.Session().UserId, IncludeDeleted: includeDeleted})
} else {
etag = c.App.GetPostsEtag(channelId, collapsedThreads)
if c.HandleEtag(etag, "Get Posts", w, r) {
return
}
list, err = c.App.GetPostsPage(model.GetPostsOptions{ChannelId: channelId, Page: page, PerPage: perPage, SkipFetchThreads: skipFetchThreads, CollapsedThreads: collapsedThreads, CollapsedThreadsExtended: collapsedThreadsExtended, UserId: c.AppContext.Session().UserId, IncludeDeleted: includeDeleted})
}
if err != nil {
c.Err = err
return
}
if etag != "" {
w.Header().Set(model.HeaderEtagServer, etag)
}
c.App.AddCursorIdsForPostList(list, afterPost, beforePost, since, page, perPage, collapsedThreads)
clientPostList := c.App.PreparePostListForClient(c.AppContext, list)
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
if err := clientPostList.EncodeJSON(w); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func getPostsForChannelAroundLastUnread(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireChannelId()
if c.Err != nil {
return
}
userId := c.Params.UserId
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), userId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
channelId := c.Params.ChannelId
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if c.Params.LimitAfter == 0 {
c.SetInvalidURLParam("limit_after")
return
}
skipFetchThreads := r.URL.Query().Get("skipFetchThreads") == "true"
collapsedThreads := r.URL.Query().Get("collapsedThreads") == "true"
collapsedThreadsExtended := r.URL.Query().Get("collapsedThreadsExtended") == "true"
postList, err := c.App.GetPostsForChannelAroundLastUnread(c.AppContext, channelId, userId, c.Params.LimitBefore, c.Params.LimitAfter, skipFetchThreads, collapsedThreads, collapsedThreadsExtended)
if err != nil {
c.Err = err
return
}
etag := ""
if len(postList.Order) == 0 {
etag = c.App.GetPostsEtag(channelId, collapsedThreads)
if c.HandleEtag(etag, "Get Posts", w, r) {
return
}
postList, err = c.App.GetPostsPage(model.GetPostsOptions{ChannelId: channelId, Page: app.PageDefault, PerPage: c.Params.LimitBefore, SkipFetchThreads: skipFetchThreads, CollapsedThreads: collapsedThreads, CollapsedThreadsExtended: collapsedThreadsExtended, UserId: c.AppContext.Session().UserId})
if err != nil {
c.Err = err
return
}
}
postList.NextPostId = c.App.GetNextPostIdFromPostList(postList, collapsedThreads)
postList.PrevPostId = c.App.GetPrevPostIdFromPostList(postList, collapsedThreads)
clientPostList := c.App.PreparePostListForClient(c.AppContext, postList)
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
if etag != "" {
w.Header().Set(model.HeaderEtagServer, etag)
}
if err := clientPostList.EncodeJSON(w); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func getFlaggedPostsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
channelId := r.URL.Query().Get("channel_id")
teamId := r.URL.Query().Get("team_id")
var posts *model.PostList
var err *model.AppError
if channelId != "" {
posts, err = c.App.GetFlaggedPostsForChannel(c.Params.UserId, channelId, c.Params.Page, c.Params.PerPage)
} else if teamId != "" {
posts, err = c.App.GetFlaggedPostsForTeam(c.Params.UserId, teamId, c.Params.Page, c.Params.PerPage)
} else {
posts, err = c.App.GetFlaggedPosts(c.Params.UserId, c.Params.Page, c.Params.PerPage)
}
if err != nil {
c.Err = err
return
}
pl := model.NewPostList()
channelReadPermission := make(map[string]bool)
for _, post := range posts.Posts {
allowed, ok := channelReadPermission[post.ChannelId]
if !ok {
allowed = false
if c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), post.ChannelId, model.PermissionReadChannel) {
allowed = true
}
channelReadPermission[post.ChannelId] = allowed
}
if !allowed {
continue
}
pl.AddPost(post)
pl.AddOrder(post.Id)
}
pl.SortByCreateAt()
clientPostList := c.App.PreparePostListForClient(c.AppContext, pl)
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
if err := clientPostList.EncodeJSON(w); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
// getPost also sets a header to indicate, if post is inaccessible due to the cloud plan's limit.
func getPost(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
if includeDeleted && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
post, err := c.App.GetPostIfAuthorized(c.AppContext, c.Params.PostId, c.AppContext.Session(), includeDeleted)
if err != nil {
c.Err = err
// Post is inaccessible due to cloud plan's limit.
if err.Id == "app.post.cloud.get.app_error" {
w.Header().Set(model.HeaderFirstInaccessiblePostTime, "1")
}
return
}
post = c.App.PreparePostForClientWithEmbedsAndImages(c.AppContext, post, false, false, true)
post, err = c.App.SanitizePostMetadataForUser(c.AppContext, post, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
if c.HandleEtag(post.Etag(), "Get Post", w, r) {
return
}
w.Header().Set(model.HeaderEtagServer, post.Etag())
if err := post.EncodeJSON(w); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
// getPostsByIds also sets a header to indicate, if posts were truncated as per the cloud plan's limit.
func getPostsByIds(c *Context, w http.ResponseWriter, r *http.Request) {
postIDs := model.ArrayFromJSON(r.Body)
if len(postIDs) == 0 {
c.SetInvalidParam("post_ids")
return
}
if len(postIDs) > 1000 {
c.Err = model.NewAppError("getPostsByIds", "api.post.posts_by_ids.invalid_body.request_error", map[string]any{"MaxLength": 1000}, "", http.StatusBadRequest)
return
}
postsList, firstInaccessiblePostTime, err := c.App.GetPostsByIds(postIDs)
if err != nil {
c.Err = err
return
}
var posts = []*model.Post{}
channelMap := make(map[string]*model.Channel)
for _, post := range postsList {
var channel *model.Channel
if val, ok := channelMap[post.ChannelId]; ok {
channel = val
} else {
channel, err = c.App.GetChannel(c.AppContext, post.ChannelId)
if err != nil {
c.Err = err
return
}
channelMap[channel.Id] = channel
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionReadChannel) {
if channel.Type != model.ChannelTypeOpen || (channel.Type == model.ChannelTypeOpen && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionReadPublicChannel)) {
continue
}
}
post = c.App.PreparePostForClient(c.AppContext, post, false, false, true)
post.StripActionIntegrations()
posts = append(posts, post)
}
w.Header().Set(model.HeaderFirstInaccessiblePostTime, strconv.FormatInt(firstInaccessiblePostTime, 10))
if err := json.NewEncoder(w).Encode(posts); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getEditHistoryForPost(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionEditPost) {
c.SetPermissionError(model.PermissionEditPost)
return
}
originalPost, err := c.App.GetSinglePost(c.Params.PostId, false)
if err != nil {
c.SetPermissionError(model.PermissionEditPost)
return
}
if c.AppContext.Session().UserId != originalPost.UserId {
c.SetPermissionError(model.PermissionEditPost)
return
}
postsList, err := c.App.GetEditHistoryForPost(c.Params.PostId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(postsList); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deletePost(c *Context, w http.ResponseWriter, _ *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("deletePost", audit.Fail)
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
audit.AddEventParameter(auditRec, "post_id", c.Params.PostId)
post, err := c.App.GetSinglePost(c.Params.PostId, false)
if err != nil {
c.SetPermissionError(model.PermissionDeletePost)
return
}
auditRec.AddEventPriorState(post)
auditRec.AddEventObjectType("post")
if c.AppContext.Session().UserId == post.UserId {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), post.ChannelId, model.PermissionDeletePost) {
c.SetPermissionError(model.PermissionDeletePost)
return
}
} else {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), post.ChannelId, model.PermissionDeleteOthersPosts) {
c.SetPermissionError(model.PermissionDeleteOthersPosts)
return
}
}
if _, err := c.App.DeletePost(c.AppContext, c.Params.PostId, c.AppContext.Session().UserId); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getPostThread(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
// For now, by default we return all items unless it's set to maintain
// backwards compatibility with mobile. But when the next ESR passes, we need to
// change this to web.PerPageDefault.
perPage := 0
if perPageStr := r.URL.Query().Get("perPage"); perPageStr != "" {
var err error
perPage, err = strconv.Atoi(perPageStr)
if err != nil || perPage > web.PerPageMaximum {
c.SetInvalidParam("perPage")
return
}
}
var fromCreateAt int64
if fromCreateAtStr := r.URL.Query().Get("fromCreateAt"); fromCreateAtStr != "" {
var err error
fromCreateAt, err = strconv.ParseInt(fromCreateAtStr, 10, 64)
if err != nil {
c.SetInvalidParam("fromCreateAt")
return
}
}
fromPost := r.URL.Query().Get("fromPost")
// Either only fromCreateAt must be set, or both fromPost and fromCreateAt must be set
if fromPost != "" && fromCreateAt == 0 {
c.SetInvalidParam("if fromPost is set, then fromCreatAt must also be set")
}
direction := ""
if dir := r.URL.Query().Get("direction"); dir != "" {
if dir != "up" && dir != "down" {
c.SetInvalidParam("direction")
return
}
direction = dir
}
opts := model.GetPostsOptions{
SkipFetchThreads: r.URL.Query().Get("skipFetchThreads") == "true",
CollapsedThreads: r.URL.Query().Get("collapsedThreads") == "true",
CollapsedThreadsExtended: r.URL.Query().Get("collapsedThreadsExtended") == "true",
PerPage: perPage,
Direction: direction,
FromPost: fromPost,
FromCreateAt: fromCreateAt,
}
list, err := c.App.GetPostThread(c.Params.PostId, opts, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
if list.FirstInaccessiblePostTime != 0 {
// e.g. if root post is archived in a cloud plan,
// we don't want to display the thread,
// but at the same time the request was not bad,
// so we return the time of archival and let the client
// show an error
if err := (&model.PostList{Order: []string{}, FirstInaccessiblePostTime: list.FirstInaccessiblePostTime}).EncodeJSON(w); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
return
}
post, ok := list.Posts[c.Params.PostId]
if !ok {
c.SetInvalidURLParam("post_id")
return
}
if _, err = c.App.GetPostIfAuthorized(c.AppContext, post.Id, c.AppContext.Session(), false); err != nil {
c.Err = err
return
}
if c.HandleEtag(list.Etag(), "Get Post Thread", w, r) {
return
}
clientPostList := c.App.PreparePostListForClient(c.AppContext, list)
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
w.Header().Set(model.HeaderEtagServer, clientPostList.Etag())
if err := clientPostList.EncodeJSON(w); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func searchPostsInTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
searchPosts(c, w, r, c.Params.TeamId)
}
func searchPostsInAllTeams(c *Context, w http.ResponseWriter, r *http.Request) {
searchPosts(c, w, r, "")
}
func searchPosts(c *Context, w http.ResponseWriter, r *http.Request, teamId string) {
var params model.SearchParameter
if jsonErr := json.NewDecoder(r.Body).Decode(¶ms); jsonErr != nil {
c.Err = model.NewAppError("searchPosts", "api.post.search_posts.invalid_body.app_error", nil, "", http.StatusBadRequest).Wrap(jsonErr)
return
}
if params.Terms == nil || *params.Terms == "" {
c.SetInvalidParam("terms")
return
}
terms := *params.Terms
timeZoneOffset := 0
if params.TimeZoneOffset != nil {
timeZoneOffset = *params.TimeZoneOffset
}
isOrSearch := false
if params.IsOrSearch != nil {
isOrSearch = *params.IsOrSearch
}
page := 0
if params.Page != nil {
page = *params.Page
}
perPage := 60
if params.PerPage != nil {
perPage = *params.PerPage
}
includeDeletedChannels := false
if params.IncludeDeletedChannels != nil {
includeDeletedChannels = *params.IncludeDeletedChannels
}
modifier := ""
if params.Modifier != nil {
modifier = *params.Modifier
}
if modifier != "" && modifier != model.ModifierFiles && modifier != model.ModifierMessages {
c.SetInvalidParam("modifier")
return
}
startTime := time.Now()
results, err := c.App.SearchPostsForUser(c.AppContext, terms, c.AppContext.Session().UserId, teamId, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, modifier)
elapsedTime := float64(time.Since(startTime)) / float64(time.Second)
metrics := c.App.Metrics()
if metrics != nil {
metrics.IncrementPostsSearchCounter()
metrics.ObservePostsSearchDuration(elapsedTime)
}
if err != nil {
c.Err = err
return
}
clientPostList := c.App.PreparePostListForClient(c.AppContext, results.PostList)
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
results = model.MakePostSearchResults(clientPostList, results.Matches)
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if err := results.EncodeJSON(w); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func updatePost(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
var post model.Post
if jsonErr := json.NewDecoder(r.Body).Decode(&post); jsonErr != nil {
c.SetInvalidParamWithErr("post", jsonErr)
return
}
auditRec := c.MakeAuditRecord("updatePost", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "post", &post)
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
// The post being updated in the payload must be the same one as indicated in the URL.
if post.Id != c.Params.PostId {
c.SetInvalidParam("id")
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionEditPost) {
c.SetPermissionError(model.PermissionEditPost)
return
}
originalPost, err := c.App.GetSinglePost(c.Params.PostId, false)
if err != nil {
c.SetPermissionError(model.PermissionEditPost)
return
}
auditRec.AddEventPriorState(originalPost)
auditRec.AddEventObjectType("post")
// Updating the file_ids of a post is not a supported operation and will be ignored
post.FileIds = originalPost.FileIds
if c.AppContext.Session().UserId != originalPost.UserId {
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionEditOthersPosts) {
c.SetPermissionError(model.PermissionEditOthersPosts)
return
}
}
post.Id = c.Params.PostId
if *c.App.Config().ServiceSettings.PostEditTimeLimit != -1 && model.GetMillis() > originalPost.CreateAt+int64(*c.App.Config().ServiceSettings.PostEditTimeLimit*1000) && post.Message != originalPost.Message {
c.Err = model.NewAppError("UpdatePost", "api.post.update_post.permissions_time_limit.app_error", map[string]any{"timeLimit": *c.App.Config().ServiceSettings.PostEditTimeLimit}, "", http.StatusBadRequest)
return
}
rpost, err := c.App.UpdatePost(c.AppContext, c.App.PostWithProxyRemovedFromImageURLs(&post), false)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(rpost)
if err := rpost.EncodeJSON(w); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func patchPost(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
var post model.PostPatch
if jsonErr := json.NewDecoder(r.Body).Decode(&post); jsonErr != nil {
c.SetInvalidParamWithErr("post", jsonErr)
return
}
auditRec := c.MakeAuditRecord("patchPost", audit.Fail)
audit.AddEventParameter(auditRec, "id", c.Params.PostId)
audit.AddEventParameterAuditable(auditRec, "patch", &post)
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
// Updating the file_ids of a post is not a supported operation and will be ignored
post.FileIds = nil
originalPost, err := c.App.GetSinglePost(c.Params.PostId, false)
if err != nil {
c.SetPermissionError(model.PermissionEditPost)
return
}
auditRec.AddEventPriorState(originalPost)
auditRec.AddEventObjectType("post")
var permission *model.Permission
if c.AppContext.Session().UserId == originalPost.UserId {
permission = model.PermissionEditPost
} else {
permission = model.PermissionEditOthersPosts
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, permission) {
c.SetPermissionError(permission)
return
}
if *c.App.Config().ServiceSettings.PostEditTimeLimit != -1 && model.GetMillis() > originalPost.CreateAt+int64(*c.App.Config().ServiceSettings.PostEditTimeLimit*1000) && post.Message != nil {
c.Err = model.NewAppError("patchPost", "api.post.update_post.permissions_time_limit.app_error", map[string]any{"timeLimit": *c.App.Config().ServiceSettings.PostEditTimeLimit}, "", http.StatusBadRequest)
return
}
patchedPost, err := c.App.PatchPost(c.AppContext, c.Params.PostId, c.App.PostPatchWithProxyRemovedFromImageURLs(&post))
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(patchedPost)
if err := patchedPost.EncodeJSON(w); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func setPostUnread(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId().RequireUserId()
if c.Err != nil {
return
}
props := model.MapBoolFromJSON(r.Body)
collapsedThreadsSupported := props["collapsed_threads_supported"]
if c.AppContext.Session().UserId != c.Params.UserId && !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
state, err := c.App.MarkChannelAsUnreadFromPost(c.AppContext, c.Params.PostId, c.Params.UserId, collapsedThreadsSupported)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(state); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func setPostReminder(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId().RequireUserId()
if c.Err != nil {
return
}
if c.AppContext.Session().UserId != c.Params.UserId && !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
var reminder model.PostReminder
if jsonErr := json.NewDecoder(r.Body).Decode(&reminder); jsonErr != nil {
c.SetInvalidParamWithErr("target_time", jsonErr)
return
}
appErr := c.App.SetPostReminder(c.Params.PostId, c.Params.UserId, reminder.TargetTime)
if appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
func saveIsPinnedPost(c *Context, w http.ResponseWriter, isPinned bool) {
c.RequirePostId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("saveIsPinnedPost", audit.Fail)
audit.AddEventParameter(auditRec, "post_id", c.Params.PostId)
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
post, err := c.App.GetSinglePost(c.Params.PostId, false)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(post)
auditRec.AddEventObjectType("post")
patch := &model.PostPatch{}
patch.IsPinned = model.NewBool(isPinned)
patchedPost, err := c.App.PatchPost(c.AppContext, c.Params.PostId, patch)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(patchedPost)
auditRec.Success()
ReturnStatusOK(w)
}
func pinPost(c *Context, w http.ResponseWriter, _ *http.Request) {
saveIsPinnedPost(c, w, true)
}
func unpinPost(c *Context, w http.ResponseWriter, _ *http.Request) {
saveIsPinnedPost(c, w, false)
}
func acknowledgePost(c *Context, w http.ResponseWriter, r *http.Request) {
// license check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequirePostId().RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
acknowledgement, appErr := c.App.SaveAcknowledgementForPost(c.AppContext, c.Params.PostId, c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(acknowledgement)
if err != nil {
c.Err = model.NewAppError("acknowledgePost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func unacknowledgePost(c *Context, w http.ResponseWriter, r *http.Request) {
// license check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequirePostId().RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
_, err := c.App.GetSinglePost(c.Params.PostId, false)
if err != nil {
c.Err = err
return
}
appErr := c.App.DeleteAcknowledgementForPost(c.AppContext, c.Params.PostId, c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
if includeDeleted && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
infos, appErr := c.App.GetFileInfosForPostWithMigration(c.Params.PostId, includeDeleted)
if appErr != nil {
c.Err = appErr
return
}
if c.HandleEtag(model.GetEtagForFileInfos(infos), "Get File Infos For Post", w, r) {
return
}
js, err := json.Marshal(infos)
if err != nil {
c.Err = model.NewAppError("getFileInfosForPost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Header().Set("Cache-Control", "max-age=2592000, private")
w.Header().Set(model.HeaderEtagServer, model.GetEtagForFileInfos(infos))
w.Write(js)
}
func getPostInfo(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
info, appErr := c.App.GetPostInfo(c.AppContext, c.Params.PostId)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(info)
if err != nil {
c.Err = model.NewAppError("getPostInfo", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitPostLocal() {
api.BaseRoutes.Post.Handle("", api.APILocal(getPost)).Methods("GET")
api.BaseRoutes.PostsForChannel.Handle("", api.APILocal(getPostsForChannel)).Methods("GET")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitPreference() {
api.BaseRoutes.Preferences.Handle("", api.APISessionRequired(getPreferences)).Methods("GET")
api.BaseRoutes.Preferences.Handle("", api.APISessionRequired(updatePreferences)).Methods("PUT")
api.BaseRoutes.Preferences.Handle("/delete", api.APISessionRequired(deletePreferences)).Methods("POST")
api.BaseRoutes.Preferences.Handle("/{category:[A-Za-z0-9_]+}", api.APISessionRequired(getPreferencesByCategory)).Methods("GET")
api.BaseRoutes.Preferences.Handle("/{category:[A-Za-z0-9_]+}/name/{preference_name:[A-Za-z0-9_]+}", api.APISessionRequired(getPreferenceByCategoryAndName)).Methods("GET")
}
func getPreferences(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
preferences, err := c.App.GetPreferencesForUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(preferences); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPreferencesByCategory(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireCategory()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
preferences, err := c.App.GetPreferenceByCategoryForUser(c.Params.UserId, c.Params.Category)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(preferences); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPreferenceByCategoryAndName(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireCategory().RequirePreferenceName()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
preferences, err := c.App.GetPreferenceByCategoryAndNameForUser(c.Params.UserId, c.Params.Category, c.Params.PreferenceName)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(preferences); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updatePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("updatePreferences", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
var preferences model.Preferences
if jsonErr := json.NewDecoder(r.Body).Decode(&preferences); jsonErr != nil {
c.SetInvalidParamWithErr("preferences", jsonErr)
return
}
var sanitizedPreferences model.Preferences
for _, pref := range preferences {
if pref.Category == model.PreferenceCategoryFlaggedPost {
post, err := c.App.GetSinglePost(pref.Name, false)
if err != nil {
c.SetInvalidParam("preference.name")
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), post.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
}
sanitizedPreferences = append(sanitizedPreferences, pref)
}
if err := c.App.UpdatePreferences(c.Params.UserId, sanitizedPreferences); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func deletePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("deletePreferences", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
var preferences model.Preferences
if jsonErr := json.NewDecoder(r.Body).Decode(&preferences); jsonErr != nil {
c.SetInvalidParamWithErr("preferences", jsonErr)
return
}
if err := c.App.DeletePreferences(c.Params.UserId, preferences); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitReaction() {
api.BaseRoutes.Reactions.Handle("", api.APISessionRequired(saveReaction)).Methods("POST")
api.BaseRoutes.Post.Handle("/reactions", api.APISessionRequired(getReactions)).Methods("GET")
api.BaseRoutes.ReactionByNameForPostForUser.Handle("", api.APISessionRequired(deleteReaction)).Methods("DELETE")
api.BaseRoutes.Posts.Handle("/ids/reactions", api.APISessionRequired(getBulkReactions)).Methods("POST")
}
func saveReaction(c *Context, w http.ResponseWriter, r *http.Request) {
var reaction model.Reaction
if jsonErr := json.NewDecoder(r.Body).Decode(&reaction); jsonErr != nil {
c.SetInvalidParamWithErr("reaction", jsonErr)
return
}
if !model.IsValidId(reaction.UserId) || !model.IsValidId(reaction.PostId) || reaction.EmojiName == "" || len(reaction.EmojiName) > model.EmojiNameMaxLength {
c.Err = model.NewAppError("saveReaction", "api.reaction.save_reaction.invalid.app_error", nil, "", http.StatusBadRequest)
return
}
if reaction.UserId != c.AppContext.Session().UserId {
c.Err = model.NewAppError("saveReaction", "api.reaction.save_reaction.user_id.app_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), reaction.PostId, model.PermissionAddReaction) {
c.SetPermissionError(model.PermissionAddReaction)
return
}
re, err := c.App.SaveReactionForPost(c.AppContext, &reaction)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(re); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getReactions(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
reactions, appErr := c.App.GetReactionsForPost(c.Params.PostId)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(reactions)
if err != nil {
c.Err = model.NewAppError("getReactions", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func deleteReaction(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
c.RequirePostId()
if c.Err != nil {
return
}
c.RequireEmojiName()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionRemoveReaction) {
c.SetPermissionError(model.PermissionRemoveReaction)
return
}
if c.Params.UserId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRemoveOthersReactions) {
c.SetPermissionError(model.PermissionRemoveOthersReactions)
return
}
reaction := &model.Reaction{
UserId: c.Params.UserId,
PostId: c.Params.PostId,
EmojiName: c.Params.EmojiName,
}
err := c.App.DeleteReactionForPost(c.AppContext, reaction)
if err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
func getBulkReactions(c *Context, w http.ResponseWriter, r *http.Request) {
postIds := model.ArrayFromJSON(r.Body)
for _, postId := range postIds {
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), postId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
}
reactions, appErr := c.App.GetBulkReactionsForPosts(postIds)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(reactions)
if err != nil {
c.Err = model.NewAppError("getBulkReactions", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"io"
"net/http"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitRemoteCluster() {
api.BaseRoutes.RemoteCluster.Handle("/ping", api.RemoteClusterTokenRequired(remoteClusterPing)).Methods("POST")
api.BaseRoutes.RemoteCluster.Handle("/msg", api.RemoteClusterTokenRequired(remoteClusterAcceptMessage)).Methods("POST")
api.BaseRoutes.RemoteCluster.Handle("/confirm_invite", api.RemoteClusterTokenRequired(remoteClusterConfirmInvite)).Methods("POST")
api.BaseRoutes.RemoteCluster.Handle("/upload/{upload_id:[A-Za-z0-9]+}", api.RemoteClusterTokenRequired(uploadRemoteData)).Methods("POST")
api.BaseRoutes.RemoteCluster.Handle("/{user_id:[A-Za-z0-9]+}/image", api.RemoteClusterTokenRequired(remoteSetProfileImage)).Methods("POST")
}
func remoteClusterPing(c *Context, w http.ResponseWriter, r *http.Request) {
// make sure remote cluster service is enabled.
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
c.Err = appErr
return
}
var frame model.RemoteClusterFrame
if err := json.NewDecoder(r.Body).Decode(&frame); err != nil {
c.Err = model.NewAppError("remoteClusterPing", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
if appErr := frame.IsValid(); appErr != nil {
c.Err = appErr
return
}
remoteId := c.GetRemoteID(r)
if remoteId != frame.RemoteId {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
rc, appErr := c.App.GetRemoteCluster(frame.RemoteId)
if appErr != nil {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
var ping model.RemoteClusterPing
if err := json.Unmarshal(frame.Msg.Payload, &ping); err != nil {
c.SetInvalidParamWithErr("msg.payload", err)
return
}
ping.RecvAt = model.GetMillis()
if metrics := c.App.Metrics(); metrics != nil {
metrics.IncrementRemoteClusterMsgReceivedCounter(rc.RemoteId)
}
err := json.NewEncoder(w).Encode(ping)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func remoteClusterAcceptMessage(c *Context, w http.ResponseWriter, r *http.Request) {
// make sure remote cluster service is running.
service, appErr := c.App.GetRemoteClusterService()
if appErr != nil {
c.Err = appErr
return
}
var frame model.RemoteClusterFrame
if err := json.NewDecoder(r.Body).Decode(&frame); err != nil {
c.Err = model.NewAppError("remoteClusterAcceptMessage", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
appErr = frame.IsValid()
if appErr != nil {
c.Err = appErr
return
}
auditRec := c.MakeAuditRecord("remoteClusterAcceptMessage", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "remote_cluster_frame", &frame)
defer c.LogAuditRec(auditRec)
remoteId := c.GetRemoteID(r)
if remoteId != frame.RemoteId {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
rc, appErr := c.App.GetRemoteCluster(frame.RemoteId)
if appErr != nil {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
audit.AddEventParameterAuditable(auditRec, "remote_cluster", rc)
// pass message to Remote Cluster Service and write response
resp := service.ReceiveIncomingMsg(rc, frame.Msg)
b, err := json.Marshal(resp)
if err != nil {
c.Err = model.NewAppError("remoteClusterAcceptMessage", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func remoteClusterConfirmInvite(c *Context, w http.ResponseWriter, r *http.Request) {
// make sure remote cluster service is running.
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
c.Err = appErr
return
}
var frame model.RemoteClusterFrame
if jsonErr := json.NewDecoder(r.Body).Decode(&frame); jsonErr != nil {
c.Err = model.NewAppError("remoteClusterConfirmInvite", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(jsonErr)
return
}
if appErr := frame.IsValid(); appErr != nil {
c.Err = appErr
return
}
auditRec := c.MakeAuditRecord("remoteClusterAcceptInvite", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "remote_cluster_frame", &frame)
defer c.LogAuditRec(auditRec)
remoteId := c.GetRemoteID(r)
if remoteId != frame.RemoteId {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
rc, err := c.App.GetRemoteCluster(frame.RemoteId)
if err != nil {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
audit.AddEventParameterAuditable(auditRec, "remote_cluster", rc)
if time.Since(model.GetTimeForMillis(rc.CreateAt)) > remotecluster.InviteExpiresAfter {
c.Err = model.NewAppError("remoteClusterAcceptMessage", "api.context.invitation_expired.error", nil, "", http.StatusBadRequest)
return
}
var confirm model.RemoteClusterInvite
if jsonErr := json.Unmarshal(frame.Msg.Payload, &confirm); jsonErr != nil {
c.SetInvalidParam("msg.payload")
return
}
rc.RemoteTeamId = confirm.RemoteTeamId
rc.SiteURL = confirm.SiteURL
rc.RemoteToken = confirm.Token
if _, err := c.App.UpdateRemoteCluster(rc); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func uploadRemoteData(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().FileSettings.EnableFileAttachments {
c.Err = model.NewAppError("uploadRemoteData", "api.file.attachments.disabled.app_error",
nil, "", http.StatusNotImplemented)
return
}
c.RequireUploadId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("uploadRemoteData", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "upload_id", c.Params.UploadId)
c.AppContext.SetContext(app.WithMaster(c.AppContext.Context()))
us, err := c.App.GetUploadSession(c.AppContext, c.Params.UploadId)
if err != nil {
c.Err = err
return
}
if us.RemoteId != c.GetRemoteID(r) {
c.Err = model.NewAppError("uploadRemoteData", "api.context.remote_id_mismatch.app_error",
nil, "", http.StatusUnauthorized)
return
}
info, err := doUploadData(c, us, r)
if err != nil {
c.Err = err
return
}
auditRec.Success()
if info == nil {
w.WriteHeader(http.StatusNoContent)
return
}
if err := json.NewEncoder(w).Encode(info); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func remoteSetProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
defer io.Copy(io.Discard, r.Body)
c.RequireUserId()
if c.Err != nil {
return
}
if *c.App.Config().FileSettings.DriverName == "" {
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.storage.app_error", nil, "", http.StatusNotImplemented)
return
}
if r.ContentLength > *c.App.Config().FileSettings.MaxFileSize {
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.too_large.app_error", nil, "", http.StatusRequestEntityTooLarge)
return
}
if err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize); err != nil {
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.parse.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
m := r.MultipartForm
imageArray, ok := m.File["image"]
if !ok {
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(imageArray) == 0 {
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.array.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("remoteUploadProfileImage", audit.Fail)
defer c.LogAuditRec(auditRec)
if imageArray[0] != nil {
audit.AddEventParameter(auditRec, "filename", imageArray[0].Filename)
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil || !user.IsRemote() {
c.SetInvalidURLParam("user_id")
return
}
audit.AddEventParameterAuditable(auditRec, "user", user)
imageData := imageArray[0]
if err := c.App.SetProfileImage(c.AppContext, c.Params.UserId, imageData); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"errors"
"fmt"
"github.com/graph-gophers/dataloader/v6"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
)
// cursorPrefix is used to categorize objects
// sent in a cursor. The type is prepended
// to the string with a - to find which
// object the id belongs to.
//
// And after the type is extracted, object
// specific logic can be applied to extract the id.
type cursorPrefix string
const (
channelMemberCursorPrefix cursorPrefix = "channelMember"
channelCursorPrefix cursorPrefix = "channel"
)
type resolver struct {
}
// match with api4.getChannelsForTeamForUser
func (r *resolver) Channels(ctx context.Context, args struct {
TeamID string
UserID string
IncludeDeleted bool
LastDeleteAt float64
LastUpdateAt float64
First int32
After string
}) ([]*channel, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if args.UserID == model.Me {
args.UserID = c.AppContext.Session().UserId
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return nil, c.Err
}
if args.TeamID != "" && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), args.TeamID, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return nil, c.Err
}
limit := int(args.First)
// ensure args.First limit
if limit == 0 {
limit = web.PerPageDefault
} else if limit > web.PerPageMaximum {
return nil, fmt.Errorf("first parameter %d higher than allowed maximum of %d", limit, web.PerPageMaximum)
}
// ensure args.After format
var afterChannel string
var ok bool
if args.After != "" {
afterChannel, ok = parseChannelCursor(args.After)
if !ok {
return nil, fmt.Errorf("after cursor not in the correct format: %s", args.After)
}
}
// TODO: convert this to a streaming API.
channels, appErr := c.App.GetChannelsForTeamForUserWithCursor(c.AppContext, args.TeamID, args.UserID, &model.ChannelSearchOpts{
IncludeDeleted: args.IncludeDeleted,
LastDeleteAt: int(args.LastDeleteAt),
LastUpdateAt: int(args.LastUpdateAt),
PerPage: model.NewInt(limit),
}, afterChannel)
if appErr != nil {
return nil, appErr
}
appErr = c.App.FillInChannelsProps(c.AppContext, channels)
if appErr != nil {
return nil, appErr
}
return postProcessChannels(c, channels)
}
// match with api4.getUser
func (r *resolver) User(ctx context.Context, args struct{ ID string }) (*user, error) {
return getGraphQLUser(ctx, args.ID)
}
// match with api4.getClientConfig
func (r *resolver) Config(ctx context.Context) (model.StringMap, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if c.AppContext.Session().UserId == "" {
return c.App.Srv().Platform().LimitedClientConfigWithComputed(), nil
}
return c.App.Srv().Platform().ClientConfigWithComputed(), nil
}
// match with api4.getClientLicense
func (r *resolver) License(ctx context.Context) (model.StringMap, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadLicenseInformation) {
return c.App.Srv().ClientLicense(), nil
}
return c.App.Srv().GetSanitizedClientLicense(), nil
}
// match with api4.getTeamMembersForUser for teamID=""
// and api4.getTeamMember for teamID != ""
func (r *resolver) TeamMembers(ctx context.Context, args struct {
UserID string
TeamID string
ExcludeTeam bool
}) ([]*teamMember, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if args.UserID == model.Me {
args.UserID = c.AppContext.Session().UserId
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadOtherUsersTeams) {
c.SetPermissionError(model.PermissionReadOtherUsersTeams)
return nil, c.Err
}
canSee, appErr := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, args.UserID)
if appErr != nil {
return nil, appErr
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return nil, c.Err
}
if args.TeamID != "" && !args.ExcludeTeam {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), args.TeamID, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return nil, c.Err
}
tm, appErr2 := c.App.GetTeamMember(args.TeamID, args.UserID)
if appErr2 != nil {
return nil, appErr2
}
return []*teamMember{{*tm}}, nil
}
excludeTeamID := ""
if args.TeamID != "" && args.ExcludeTeam {
excludeTeamID = args.TeamID
}
// Do not return archived team members
members, appErr := c.App.GetTeamMembersForUser(args.UserID, excludeTeamID, false)
if appErr != nil {
return nil, appErr
}
// Convert to the wrapper format.
res := make([]*teamMember, 0, len(members))
for _, tm := range members {
res = append(res, &teamMember{*tm})
}
return res, nil
}
func (*resolver) ChannelsLeft(ctx context.Context, args struct {
UserID string
Since float64
}) ([]string, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if args.UserID == model.Me {
args.UserID = c.AppContext.Session().UserId
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return nil, c.Err
}
return c.App.Srv().Store().ChannelMemberHistory().GetChannelsLeftSince(args.UserID, int64(args.Since))
}
// match with api4.getChannelMember
func (*resolver) ChannelMembers(ctx context.Context, args struct {
UserID string
TeamID string
ChannelID string
ExcludeTeam bool
First int32
After string
LastUpdateAt float64
}) ([]*channelMember, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if args.UserID == model.Me {
args.UserID = c.AppContext.Session().UserId
}
// If it's a single channel
if args.ChannelID != "" {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), args.ChannelID, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return nil, c.Err
}
ctx := c.AppContext
ctx.SetContext(app.WithMaster(ctx.Context()))
member, appErr := c.App.GetChannelMember(ctx, args.ChannelID, args.UserID)
if appErr != nil {
return nil, appErr
}
return []*channelMember{{*member}}, nil
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return nil, c.Err
}
limit := int(args.First)
// ensure args.First limit
if limit == 0 {
limit = web.PerPageDefault
} else if limit > web.PerPageMaximum {
return nil, fmt.Errorf("first parameter %d higher than allowed maximum of %d", limit, web.PerPageMaximum)
}
// ensure args.After format
var afterChannel, afterUser string
var ok bool
if args.After != "" {
afterChannel, afterUser, ok = parseChannelMemberCursor(args.After)
if !ok {
return nil, fmt.Errorf("after cursor not in the correct format: %s", args.After)
}
}
if args.TeamID != "" {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), args.TeamID, model.PermissionViewTeam) {
primaryTeam := *c.App.Config().TeamSettings.ExperimentalPrimaryTeam
if primaryTeam != "" {
team, appErr := c.App.GetTeamByName(primaryTeam)
if appErr != nil {
return []*channelMember{}, appErr
}
args.TeamID = team.Id
} else {
return []*channelMember{}, nil
}
}
}
opts := &store.ChannelMemberGraphQLSearchOpts{
AfterChannel: afterChannel,
AfterUser: afterUser,
Limit: limit,
LastUpdateAt: int(args.LastUpdateAt),
ExcludeTeam: args.ExcludeTeam,
}
members, err := c.App.Srv().Store().Channel().GetMembersForUserWithCursor(args.UserID, args.TeamID, opts)
if err != nil {
return nil, err
}
res := make([]*channelMember, 0, len(members))
for _, cm := range members {
res = append(res, &channelMember{cm})
}
return res, nil
}
// match with api4.getCategoriesForTeamForUser
func (*resolver) SidebarCategories(ctx context.Context, args struct {
UserID string
TeamID string
ExcludeTeam bool
}) ([]*model.SidebarCategoryWithChannels, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
// Fallback to primary team logic
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), args.TeamID, model.PermissionViewTeam) {
primaryTeam := *c.App.Config().TeamSettings.ExperimentalPrimaryTeam
if primaryTeam != "" {
team, appErr := c.App.GetTeamByName(primaryTeam)
if appErr != nil {
return []*model.SidebarCategoryWithChannels{}, appErr
}
args.TeamID = team.Id
} else {
return []*model.SidebarCategoryWithChannels{}, nil
}
}
if args.UserID == model.Me {
args.UserID = c.AppContext.Session().UserId
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return nil, c.Err
}
// If it's only for a single team.
var categories *model.OrderedSidebarCategories
var appErr *model.AppError
if !args.ExcludeTeam {
categories, appErr = c.App.GetSidebarCategoriesForTeamForUser(c.AppContext, args.UserID, args.TeamID)
if appErr != nil {
return nil, appErr
}
} else {
opts := &store.SidebarCategorySearchOpts{
TeamID: args.TeamID,
ExcludeTeam: args.ExcludeTeam,
}
categories, appErr = c.App.GetSidebarCategories(c.AppContext, args.UserID, opts)
if appErr != nil {
return nil, appErr
}
}
// TODO: look into optimizing this.
// create map
orderMap := make(map[string]*model.SidebarCategoryWithChannels, len(categories.Categories))
for _, category := range categories.Categories {
orderMap[category.Id] = category
}
// create a new slice based on the order
res := make([]*model.SidebarCategoryWithChannels, 0, len(categories.Categories))
for _, categoryId := range categories.Order {
res = append(res, orderMap[categoryId])
}
return res, nil
}
// getCtx extracts web.Context out of the usual request context.
// Kind of an anti-pattern, but there are lots of methods attached to *web.Context
// so we use it for now.
func getCtx(ctx context.Context) (*web.Context, error) {
c, ok := ctx.Value(webCtx).(*web.Context)
if !ok {
return nil, errors.New("no web.Context found in context")
}
return c, nil
}
// getRolesLoader returns the roles loader out of the context.
func getRolesLoader(ctx context.Context) (*dataloader.Loader, error) {
l, ok := ctx.Value(rolesLoaderCtx).(*dataloader.Loader)
if !ok {
return nil, errors.New("no dataloader.Loader found in context")
}
return l, nil
}
// getChannelsLoader returns the channels loader out of the context.
func getChannelsLoader(ctx context.Context) (*dataloader.Loader, error) {
l, ok := ctx.Value(channelsLoaderCtx).(*dataloader.Loader)
if !ok {
return nil, errors.New("no dataloader.Loader found in context")
}
return l, nil
}
// getTeamsLoader returns the teams loader out of the context.
func getTeamsLoader(ctx context.Context) (*dataloader.Loader, error) {
l, ok := ctx.Value(teamsLoaderCtx).(*dataloader.Loader)
if !ok {
return nil, errors.New("no dataloader.Loader found in context")
}
return l, nil
}
// getUsersLoader returns the users loader out of the context.
func getUsersLoader(ctx context.Context) (*dataloader.Loader, error) {
l, ok := ctx.Value(usersLoaderCtx).(*dataloader.Loader)
if !ok {
return nil, errors.New("no dataloader.Loader found in context")
}
return l, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"encoding/base64"
"fmt"
"sort"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
)
// channel is an internal graphQL wrapper struct to add resolver methods.
type channel struct {
model.Channel
PrettyDisplayName string
}
// match with api4.getTeam
func (ch *channel) Team(ctx context.Context) (*model.Team, error) {
if ch.TeamId == "" {
return nil, nil
}
return getGraphQLTeam(ctx, ch.TeamId)
}
func (ch *channel) Cursor() *string {
cursor := string(channelCursorPrefix) + "-" + ch.Id
encoded := base64.StdEncoding.EncodeToString([]byte(cursor))
return model.NewString(encoded)
}
func parseChannelCursor(cursor string) (channelID string, ok bool) {
decoded, err := base64.StdEncoding.DecodeString(cursor)
if err != nil {
return "", false
}
prefix, id, found := strings.Cut(string(decoded), "-")
if !found {
return "", false
}
if cursorPrefix(prefix) != channelCursorPrefix {
return "", false
}
return id, true
}
func postProcessChannels(c *web.Context, channels []*model.Channel) ([]*channel, error) {
// This approach becomes effectively similar to a dataloader if the displayName computation
// were to be done at the field level per channel.
// Get DM/GM channelIDs and set empty maps as well.
var channelIDs []string
for _, ch := range channels {
if ch.IsGroupOrDirect() {
channelIDs = append(channelIDs, ch.Id)
}
// This is needed to avoid sending null, which
// does not match with the schema since props is not nullable.
// And making it nullable would mean taking pointer of a map,
// which is not very idiomatic.
ch.MakeNonNil()
}
var nameFormat string
var userInfo map[string][]*model.User
var err error
// Avoiding unnecessary queries unless necessary.
if len(channelIDs) > 0 {
userInfo, err = c.App.Srv().Store().Channel().GetMembersInfoByChannelIds(channelIDs)
if err != nil {
return nil, err
}
user := &model.User{Id: c.AppContext.Session().UserId}
nameFormat = c.App.GetNotificationNameFormat(user)
}
// Convert to the wrapper format.
nameCache := make(map[string]string)
res := make([]*channel, len(channels))
for i, ch := range channels {
prettyName := ch.DisplayName
if ch.IsGroupOrDirect() {
// get users slice for channel id
users := userInfo[ch.Id]
if users == nil {
return nil, fmt.Errorf("user info not found for channel id: %s", ch.Id)
}
prettyName = getPrettyDNForUsers(nameFormat, users, c.AppContext.Session().UserId, nameCache)
}
res[i] = &channel{Channel: *ch, PrettyDisplayName: prettyName}
}
return res, nil
}
func getPrettyDNForUsers(displaySetting string, users []*model.User, omitUserId string, cache map[string]string) string {
displayNames := make([]string, 0, len(users))
for _, u := range users {
if u.Id == omitUserId {
continue
}
displayNames = append(displayNames, getPrettyDNForUser(displaySetting, u, cache))
}
sort.Strings(displayNames)
result := strings.Join(displayNames, ", ")
if result == "" {
// Self DM
result = getPrettyDNForUser(displaySetting, users[0], cache)
}
return result
}
func getPrettyDNForUser(displaySetting string, user *model.User, cache map[string]string) string {
// use the cache first
if name, ok := cache[user.Id]; ok {
return name
}
var displayName string
switch displaySetting {
case "nickname_full_name":
displayName = user.Nickname
if strings.TrimSpace(displayName) == "" {
displayName = user.GetFullName()
}
if strings.TrimSpace(displayName) == "" {
displayName = user.Username
}
case "full_name":
displayName = user.GetFullName()
if strings.TrimSpace(displayName) == "" {
displayName = user.Username
}
default: // the "username" case also falls under this one.
displayName = user.Username
}
// update the cache
cache[user.Id] = displayName
return displayName
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"encoding/base64"
"fmt"
"strings"
"github.com/graph-gophers/dataloader/v6"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
)
// channelMember is an internal graphQL wrapper struct to add resolver methods.
type channelMember struct {
model.ChannelMember
}
// match with api4.getUser
func (cm *channelMember) User(ctx context.Context) (*user, error) {
return getGraphQLUser(ctx, cm.UserId)
}
// match with api4.Channel
func (cm *channelMember) Channel(ctx context.Context) (*channel, error) {
loader, err := getChannelsLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.Load(ctx, dataloader.StringKey(cm.ChannelId))
result, err := thunk()
if err != nil {
return nil, err
}
channel := result.(*channel)
return channel, nil
}
func graphQLChannelsLoader(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
stringKeys := keys.Keys()
result := make([]*dataloader.Result, len(stringKeys))
c, err := getCtx(ctx)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
channels, err := getGraphQLChannels(c, stringKeys)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
for i, ch := range channels {
result[i] = &dataloader.Result{Data: ch}
}
return result
}
func getGraphQLChannels(c *web.Context, channelIDs []string) ([]*channel, error) {
channels, appErr := c.App.GetChannels(c.AppContext, channelIDs)
if appErr != nil {
return nil, appErr
}
if len(channels) != len(channelIDs) {
return nil, fmt.Errorf("all channels were not found. Requested %d; Found %d", len(channelIDs), len(channels))
}
var openChannels, nonOpenChannels, teamsForOpenChannels []string
uniqueTeams := make(map[string]bool)
for _, ch := range channels {
if ch.Type == model.ChannelTypeOpen {
openChannels = append(openChannels, ch.Id)
uniqueTeams[ch.TeamId] = true
} else {
nonOpenChannels = append(nonOpenChannels, ch.Id)
}
}
for teamID := range uniqueTeams {
teamsForOpenChannels = append(teamsForOpenChannels, teamID)
}
if len(openChannels) > 0 && !c.App.SessionHasPermissionToChannels(c.AppContext, *c.AppContext.Session(), openChannels, model.PermissionReadChannel) &&
!c.App.SessionHasPermissionToTeams(c.AppContext, *c.AppContext.Session(), teamsForOpenChannels, model.PermissionReadPublicChannel) {
c.SetPermissionError(model.PermissionReadPublicChannel)
return nil, c.Err
}
if len(nonOpenChannels) > 0 && !c.App.SessionHasPermissionToChannels(c.AppContext, *c.AppContext.Session(), nonOpenChannels, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return nil, c.Err
}
appErr = c.App.FillInChannelsProps(c.AppContext, model.ChannelList(channels))
if appErr != nil {
return nil, appErr
}
res, err := postProcessChannels(c, channels)
if err != nil {
return nil, err
}
// The channels need to be in the exact same order as the input slice.
tmp := make(map[string]*channel)
for _, ch := range res {
tmp[ch.Id] = ch
}
// We reuse the same slice and just rewrite the channels.
for i, id := range channelIDs {
res[i] = tmp[id]
}
return res, nil
}
func (cm *channelMember) Roles_(ctx context.Context) ([]*model.Role, error) {
loader, err := getRolesLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.LoadMany(ctx, dataloader.NewKeysFromStrings(strings.Fields(cm.Roles)))
results, errs := thunk()
// All errors are the same. We just return the first one.
if len(errs) > 0 && errs[0] != nil {
return nil, err
}
roles := make([]*model.Role, len(results))
for i, res := range results {
roles[i] = res.(*model.Role)
}
return roles, nil
}
func (cm *channelMember) Cursor() *string {
cursor := string(channelMemberCursorPrefix) + "-" + cm.ChannelId + "-" + cm.UserId
encoded := base64.StdEncoding.EncodeToString([]byte(cursor))
return model.NewString(encoded)
}
func graphQLRolesLoader(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
stringKeys := keys.Keys()
result := make([]*dataloader.Result, len(stringKeys))
c, err := getCtx(ctx)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
roles, err := getGraphQLRoles(c, stringKeys)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
for i, role := range roles {
result[i] = &dataloader.Result{Data: role}
}
return result
}
func getGraphQLRoles(c *web.Context, roleNames []string) ([]*model.Role, error) {
cleanedRoleNames, valid := model.CleanRoleNames(roleNames)
if !valid {
c.SetInvalidParam("rolename")
return nil, c.Err
}
roles, appErr := c.App.GetRolesByNames(cleanedRoleNames)
if appErr != nil {
return nil, appErr
}
// The roles need to be in the exact same order as the input slice.
tmp := make(map[string]*model.Role)
for _, r := range roles {
tmp[r.Name] = r
}
// We reuse the same slice and just rewrite the roles.
for i, roleName := range roleNames {
roles[i] = tmp[roleName]
}
return roles, nil
}
func parseChannelMemberCursor(cursor string) (channelID, userID string, ok bool) {
decoded, err := base64.StdEncoding.DecodeString(cursor)
if err != nil {
return "", "", false
}
parts := strings.Split(string(decoded), "-")
if len(parts) != 3 {
return "", "", false
}
if cursorPrefix(parts[0]) != channelMemberCursorPrefix {
return "", "", false
}
return parts[1], parts[2], true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"fmt"
"github.com/graph-gophers/dataloader/v6"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
)
func getGraphQLTeam(ctx context.Context, id string) (*model.Team, error) {
loader, err := getTeamsLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.Load(ctx, dataloader.StringKey(id))
result, err := thunk()
if err != nil {
return nil, err
}
team := result.(*model.Team)
return team, nil
}
func graphQLTeamsLoader(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
stringKeys := keys.Keys()
result := make([]*dataloader.Result, len(stringKeys))
c, err := getCtx(ctx)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
teams, err := getGraphQLTeams(c, stringKeys)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
for i, ch := range teams {
result[i] = &dataloader.Result{Data: ch}
}
return result
}
func getGraphQLTeams(c *web.Context, teamIDs []string) ([]*model.Team, error) {
teams, appErr := c.App.GetTeams(teamIDs)
if appErr != nil {
return nil, appErr
}
if len(teams) != len(teamIDs) {
return nil, fmt.Errorf("all teams were not found. Requested %d; Found %d", len(teamIDs), len(teams))
}
var teamsToCheck []string
for _, team := range teams {
if !team.AllowOpenInvite || team.Type != model.TeamOpen {
teamsToCheck = append(teamsToCheck, team.Id)
}
}
if !c.App.SessionHasPermissionToTeams(c.AppContext, *c.AppContext.Session(), teamsToCheck, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return nil, c.Err
}
for i, team := range teams {
teams[i] = c.App.SanitizeTeam(*c.AppContext.Session(), team)
}
// The teams need to be in the exact same order as the input slice.
tmp := make(map[string]*model.Team, len(teams))
for _, ch := range teams {
tmp[ch.Id] = ch
}
// We reuse the same slice and just rewrite the teams.
for i, id := range teamIDs {
teams[i] = tmp[id]
}
return teams, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"strings"
"github.com/graph-gophers/dataloader/v6"
"github.com/mattermost/mattermost-server/v6/model"
)
// teamMember is an internal graphQL wrapper struct to add resolver methods.
type teamMember struct {
model.TeamMember
}
// match with api4.getTeam
func (tm *teamMember) Team(ctx context.Context) (*model.Team, error) {
return getGraphQLTeam(ctx, tm.TeamId)
}
// match with api4.getUser
func (tm *teamMember) User(ctx context.Context) (*user, error) {
return getGraphQLUser(ctx, tm.UserId)
}
// match with api4.getRolesByNames
func (tm *teamMember) Roles_(ctx context.Context) ([]*model.Role, error) {
loader, err := getRolesLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.LoadMany(ctx, dataloader.NewKeysFromStrings(strings.Fields(tm.Roles)))
results, errs := thunk()
// All errors are the same. We just return the first one.
if len(errs) > 0 && errs[0] != nil {
return nil, err
}
roles := make([]*model.Role, len(results))
for i, res := range results {
roles[i] = res.(*model.Role)
}
return roles, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"net/http"
"github.com/graph-gophers/dataloader/v6"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
)
// user is an internal graphQL wrapper struct to add resolver methods.
type user struct {
model.User
}
// match with api4.getUser
func getGraphQLUser(ctx context.Context, id string) (*user, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if id == model.Me {
id = c.AppContext.Session().UserId
}
if !model.IsValidId(id) {
return nil, web.NewInvalidParamError("user_id")
}
loader, err := getUsersLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.Load(ctx, dataloader.StringKey(id))
result, err := thunk()
if err != nil {
return nil, err
}
usr := result.(*model.User)
if c.IsSystemAdmin() || c.AppContext.Session().UserId == usr.Id {
userTermsOfService, appErr := c.App.GetUserTermsOfService(usr.Id)
if appErr != nil && appErr.StatusCode != http.StatusNotFound {
return nil, appErr
}
if userTermsOfService != nil {
usr.TermsOfServiceId = userTermsOfService.TermsOfServiceId
usr.TermsOfServiceCreateAt = userTermsOfService.CreateAt
}
}
c.App.Srv().Platform().UpdateLastActivityAtIfNeeded(*c.AppContext.Session())
return &user{*usr}, nil
}
// match with api4.getRolesByNames
func (u *user) Roles(ctx context.Context) ([]*model.Role, error) {
roleNames := u.GetRoles()
if len(roleNames) == 0 {
return nil, nil
}
loader, err := getRolesLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.LoadMany(ctx, dataloader.NewKeysFromStrings(roleNames))
results, errs := thunk()
// All errors are the same. We just return the first one.
if len(errs) > 0 && errs[0] != nil {
return nil, err
}
roles := make([]*model.Role, len(results))
for i, res := range results {
roles[i] = res.(*model.Role)
}
return roles, nil
}
// match with api4.getPreferences
func (u *user) Preferences(ctx context.Context) ([]model.Preference, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), u.Id) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return nil, c.Err
}
preferences, appErr := c.App.GetPreferencesForUser(u.Id)
if appErr != nil {
return nil, appErr
}
return preferences, nil
}
// match with api4.getUserStatus
func (u *user) Status(ctx context.Context) (*model.Status, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
statuses, appErr := c.App.GetUserStatusesByIds([]string{u.Id})
if appErr != nil {
return nil, appErr
}
if len(statuses) == 0 {
return nil, model.NewAppError("UserStatus", "api.status.user_not_found.app_error", nil, "", http.StatusNotFound)
}
return statuses[0], nil
}
// match with api4.getSessions
func (u *user) Sessions(ctx context.Context) ([]*model.Session, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), u.Id) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return nil, c.Err
}
sessions, appErr := c.App.GetSessions(u.Id)
if appErr != nil {
return nil, appErr
}
for _, session := range sessions {
session.Sanitize()
}
return sessions, nil
}
func graphQLUsersLoader(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
stringKeys := keys.Keys()
result := make([]*dataloader.Result, len(stringKeys))
c, err := getCtx(ctx)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
users, err := getGraphQLUsers(c, stringKeys)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
for i, user := range users {
result[i] = &dataloader.Result{Data: user}
}
return result
}
func getGraphQLUsers(c *web.Context, userIDs []string) ([]*model.User, error) {
// Usually this will be called only for one user
// and cached for the rest of the query. So it's not an issue
// to run this in a loop.
for _, id := range userIDs {
canSee, appErr := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, id)
if appErr != nil || !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return nil, c.Err
}
}
users, appErr := c.App.GetUsers(userIDs)
if appErr != nil {
return nil, appErr
}
// Same as earlier, we want to pre-compute this only once
// because otherwise the resolvers run in multiple goroutines
// and *User.Sanitize causes a race, and we want to avoid
// deep-copying every user in all goroutines.
for _, user := range users {
if c.AppContext.Session().UserId == user.Id {
user.Sanitize(map[string]bool{})
} else {
c.App.SanitizeProfile(user, c.IsSystemAdmin())
}
}
// The users need to be in the exact same order as the input slice.
tmp := make(map[string]*model.User)
for _, u := range users {
tmp[u.Id] = u
}
// We reuse the same slice and just rewrite the roles.
for i, uID := range userIDs {
users[i] = tmp[uID]
}
return users, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var notAllowedPermissions = []string{
model.PermissionSysconsoleWriteUserManagementSystemRoles.Id,
model.PermissionSysconsoleReadUserManagementSystemRoles.Id,
model.PermissionManageRoles.Id,
}
func (api *API) InitRole() {
api.BaseRoutes.Roles.Handle("", api.APISessionRequired(getAllRoles)).Methods("GET")
api.BaseRoutes.Roles.Handle("/{role_id:[A-Za-z0-9]+}", api.APISessionRequiredTrustRequester(getRole)).Methods("GET")
api.BaseRoutes.Roles.Handle("/name/{role_name:[a-z0-9_]+}", api.APISessionRequiredTrustRequester(getRoleByName)).Methods("GET")
api.BaseRoutes.Roles.Handle("/names", api.APISessionRequiredTrustRequester(getRolesByNames)).Methods("POST")
api.BaseRoutes.Roles.Handle("/{role_id:[A-Za-z0-9]+}/patch", api.APISessionRequired(patchRole)).Methods("PUT")
}
func getAllRoles(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
roles, appErr := c.App.GetAllRoles()
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(roles)
if err != nil {
c.Err = model.NewAppError("getAllRoles", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getRole(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireRoleId()
if c.Err != nil {
return
}
role, err := c.App.GetRole(c.Params.RoleId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(role); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getRoleByName(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireRoleName()
if c.Err != nil {
return
}
role, err := c.App.GetRoleByName(r.Context(), c.Params.RoleName)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(role); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getRolesByNames(c *Context, w http.ResponseWriter, r *http.Request) {
rolenames := model.ArrayFromJSON(r.Body)
if len(rolenames) == 0 {
c.SetInvalidParam("rolenames")
return
}
cleanedRoleNames, valid := model.CleanRoleNames(rolenames)
if !valid {
c.SetInvalidParam("rolename")
return
}
roles, appErr := c.App.GetRolesByNames(cleanedRoleNames)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(roles)
if err != nil {
c.Err = model.NewAppError("getRolesByNames", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func patchRole(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireRoleId()
if c.Err != nil {
return
}
var patch model.RolePatch
if err := json.NewDecoder(r.Body).Decode(&patch); err != nil {
c.SetInvalidParamWithErr("role", err)
return
}
auditRec := c.MakeAuditRecord("patchRole", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "role_patch", &patch)
defer c.LogAuditRec(auditRec)
oldRole, appErr := c.App.GetRole(c.Params.RoleId)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventPriorState(oldRole)
auditRec.AddEventObjectType("role")
// manage_system permission is required to patch system_admin
requiredPermission := model.PermissionSysconsoleWriteUserManagementPermissions
specialProtectedSystemRoles := append(model.NewSystemRoleIDs, model.SystemAdminRoleId)
for _, roleID := range specialProtectedSystemRoles {
if oldRole.Name == roleID {
requiredPermission = model.PermissionManageSystem
}
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), requiredPermission) {
c.SetPermissionError(requiredPermission)
return
}
isGuest := oldRole.Name == model.SystemGuestRoleId || oldRole.Name == model.TeamGuestRoleId || oldRole.Name == model.ChannelGuestRoleId
if c.App.Channels().License() == nil && patch.Permissions != nil {
if isGuest {
c.Err = model.NewAppError("Api4.PatchRoles", "api.roles.patch_roles.license.error", nil, "", http.StatusNotImplemented)
return
}
}
// Licensed instances can not change permissions in the blacklist set.
if patch.Permissions != nil {
deltaPermissions := model.PermissionsChangedByPatch(oldRole, &patch)
for _, permission := range deltaPermissions {
notAllowed := false
for _, notAllowedPermission := range notAllowedPermissions {
if permission == notAllowedPermission {
notAllowed = true
}
}
if notAllowed {
c.Err = model.NewAppError("Api4.PatchRoles", "api.roles.patch_roles.not_allowed_permission.error", nil, "Cannot add or remove permission: "+permission, http.StatusNotImplemented)
return
}
}
*patch.Permissions = model.RemoveDuplicateStrings(*patch.Permissions)
}
if c.App.Channels().License() != nil && isGuest && !*c.App.Channels().License().Features.GuestAccountsPermissions {
c.Err = model.NewAppError("Api4.PatchRoles", "api.roles.patch_roles.license.error", nil, "", http.StatusNotImplemented)
return
}
if oldRole.Name == model.TeamAdminRoleId ||
oldRole.Name == model.ChannelAdminRoleId ||
oldRole.Name == model.SystemUserRoleId ||
oldRole.Name == model.TeamUserRoleId ||
oldRole.Name == model.ChannelUserRoleId ||
oldRole.Name == model.SystemGuestRoleId ||
oldRole.Name == model.TeamGuestRoleId ||
oldRole.Name == model.ChannelGuestRoleId ||
oldRole.Name == model.PlaybookAdminRoleId ||
oldRole.Name == model.PlaybookMemberRoleId ||
oldRole.Name == model.RunAdminRoleId ||
oldRole.Name == model.RunMemberRoleId {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementPermissions) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementPermissions)
return
}
} else {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementSystemRoles) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementSystemRoles)
return
}
}
role, appErr := c.App.PatchRole(oldRole, &patch)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(role)
auditRec.Success()
c.LogAudit("")
if err := json.NewEncoder(w).Encode(role); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitRoleLocal() {
api.BaseRoutes.Roles.Handle("", api.APILocal(getAllRoles)).Methods("GET")
api.BaseRoutes.Roles.Handle("/{role_id:[A-Za-z0-9]+}", api.APILocal(getRole)).Methods("GET")
api.BaseRoutes.Roles.Handle("/name/{role_name:[a-z0-9_]+}", api.APILocal(getRoleByName)).Methods("GET")
api.BaseRoutes.Roles.Handle("/names", api.APILocal(getRolesByNames)).Methods("POST")
api.BaseRoutes.Roles.Handle("/{role_id:[A-Za-z0-9]+}/patch", api.APILocal(patchRole)).Methods("PUT")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"io"
"mime"
"mime/multipart"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitSaml() {
api.BaseRoutes.SAML.Handle("/metadata", api.APIHandler(getSamlMetadata)).Methods("GET")
api.BaseRoutes.SAML.Handle("/certificate/public", api.APISessionRequired(addSamlPublicCertificate)).Methods("POST")
api.BaseRoutes.SAML.Handle("/certificate/private", api.APISessionRequired(addSamlPrivateCertificate)).Methods("POST")
api.BaseRoutes.SAML.Handle("/certificate/idp", api.APISessionRequired(addSamlIdpCertificate)).Methods("POST")
api.BaseRoutes.SAML.Handle("/certificate/public", api.APISessionRequired(removeSamlPublicCertificate)).Methods("DELETE")
api.BaseRoutes.SAML.Handle("/certificate/private", api.APISessionRequired(removeSamlPrivateCertificate)).Methods("DELETE")
api.BaseRoutes.SAML.Handle("/certificate/idp", api.APISessionRequired(removeSamlIdpCertificate)).Methods("DELETE")
api.BaseRoutes.SAML.Handle("/certificate/status", api.APISessionRequired(getSamlCertificateStatus)).Methods("GET")
api.BaseRoutes.SAML.Handle("/metadatafromidp", api.APIHandler(getSamlMetadataFromIdp)).Methods("POST")
api.BaseRoutes.SAML.Handle("/reset_auth_data", api.APISessionRequired(resetAuthDataToEmail)).Methods("POST")
}
func (api *API) InitSamlLocal() {
api.BaseRoutes.SAML.Handle("/reset_auth_data", api.APILocal(resetAuthDataToEmail)).Methods("POST")
}
func getSamlMetadata(c *Context, w http.ResponseWriter, r *http.Request) {
metadata, err := c.App.GetSamlMetadata()
if err != nil {
c.Err = err
return
}
w.Header().Set("Content-Type", "application/xml")
w.Header().Set("Content-Disposition", "attachment; filename=\"metadata.xml\"")
w.Write([]byte(metadata))
}
func parseSamlCertificateRequest(r *http.Request, maxFileSize int64) (*multipart.FileHeader, *model.AppError) {
err := r.ParseMultipartForm(maxFileSize)
if err != nil {
return nil, model.NewAppError("addSamlCertificate", "api.admin.add_certificate.no_file.app_error", nil, err.Error(), http.StatusBadRequest)
}
m := r.MultipartForm
fileArray, ok := m.File["certificate"]
if !ok {
return nil, model.NewAppError("addSamlCertificate", "api.admin.add_certificate.no_file.app_error", nil, "", http.StatusBadRequest)
}
if len(fileArray) <= 0 {
return nil, model.NewAppError("addSamlCertificate", "api.admin.add_certificate.array.app_error", nil, "", http.StatusBadRequest)
}
return fileArray[0], nil
}
func addSamlPublicCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionAddSamlPublicCert) {
c.SetPermissionError(model.PermissionAddSamlPublicCert)
return
}
fileData, err := parseSamlCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("addSamlPublicCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
if err := c.App.AddSamlPublicCertificate(fileData); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func addSamlPrivateCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionAddSamlPrivateCert) {
c.SetPermissionError(model.PermissionAddSamlPrivateCert)
return
}
fileData, err := parseSamlCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("addSamlPrivateCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
if err := c.App.AddSamlPrivateCertificate(fileData); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func addSamlIdpCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionAddSamlIdpCert) {
c.SetPermissionError(model.PermissionAddSamlIdpCert)
return
}
v := r.Header.Get("Content-Type")
if v == "" {
c.Err = model.NewAppError("addSamlIdpCertificate", "api.admin.saml.set_certificate_from_metadata.missing_content_type.app_error", nil, "", http.StatusBadRequest)
return
}
d, _, err := mime.ParseMediaType(v)
if err != nil {
c.Err = model.NewAppError("addSamlIdpCertificate", "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("addSamlIdpCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
auditRec.AddMeta("type", d)
if d == "application/x-pem-file" {
body, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("addSamlIdpCertificate", "api.admin.saml.set_certificate_from_metadata.invalid_body.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
if err := c.App.SetSamlIdpCertificateFromMetadata(body); err != nil {
c.Err = err
return
}
} else if d == "multipart/form-data" {
fileData, err := parseSamlCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize)
if err != nil {
c.Err = err
return
}
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
if err := c.App.AddSamlIdpCertificate(fileData); err != nil {
c.Err = err
return
}
} else {
c.Err = model.NewAppError("addSamlIdpCertificate", "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func removeSamlPublicCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRemoveSamlPublicCert) {
c.SetPermissionError(model.PermissionRemoveSamlPublicCert)
return
}
auditRec := c.MakeAuditRecord("removeSamlPublicCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
if err := c.App.RemoveSamlPublicCertificate(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func removeSamlPrivateCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRemoveSamlPrivateCert) {
c.SetPermissionError(model.PermissionRemoveSamlPrivateCert)
return
}
auditRec := c.MakeAuditRecord("removeSamlPrivateCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
if err := c.App.RemoveSamlPrivateCertificate(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func removeSamlIdpCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRemoveSamlIdpCert) {
c.SetPermissionError(model.PermissionRemoveSamlIdpCert)
return
}
auditRec := c.MakeAuditRecord("removeSamlIdpCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
if err := c.App.RemoveSamlIdpCertificate(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getSamlCertificateStatus(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionGetSamlCertStatus) {
c.SetPermissionError(model.PermissionGetSamlCertStatus)
return
}
status := c.App.GetSamlCertificateStatus()
if err := json.NewEncoder(w).Encode(status); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getSamlMetadataFromIdp(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionGetSamlMetadataFromIdp) {
c.SetPermissionError(model.PermissionGetSamlMetadataFromIdp)
return
}
props := model.MapFromJSON(r.Body)
url := props["saml_metadata_url"]
if url == "" {
c.SetInvalidParam("saml_metadata_url")
return
}
metadata, err := c.App.GetSamlMetadataFromIdp(url)
if err != nil {
c.Err = model.NewAppError("getSamlMetadataFromIdp", "api.admin.saml.failure_get_metadata_from_idp.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
if err := json.NewEncoder(w).Encode(metadata); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func resetAuthDataToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
type ResetAuthDataParams struct {
IncludeDeleted bool `json:"include_deleted"`
DryRun bool `json:"dry_run"`
SpecifiedUserIDs []string `json:"user_ids"`
}
var params *ResetAuthDataParams
jsonErr := json.NewDecoder(r.Body).Decode(¶ms)
if jsonErr != nil {
c.Err = model.NewAppError("resetAuthDataToEmail", "model.utils.decode_json.app_error", nil, "", http.StatusBadRequest).Wrap(jsonErr)
return
}
numAffected, appErr := c.App.ResetSamlAuthDataToEmail(params.IncludeDeleted, params.DryRun, params.SpecifiedUserIDs)
if appErr != nil {
c.Err = appErr
return
}
n := struct {
NumAffected int `json:"num_affected"`
}{
NumAffected: numAffected,
}
if err := json.NewEncoder(w).Encode(n); err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitScheme() {
api.BaseRoutes.Schemes.Handle("", api.APISessionRequired(getSchemes)).Methods("GET")
api.BaseRoutes.Schemes.Handle("", api.APISessionRequired(createScheme)).Methods("POST")
api.BaseRoutes.Schemes.Handle("/{scheme_id:[A-Za-z0-9]+}", api.APISessionRequired(deleteScheme)).Methods("DELETE")
api.BaseRoutes.Schemes.Handle("/{scheme_id:[A-Za-z0-9]+}", api.APISessionRequiredTrustRequester(getScheme)).Methods("GET")
api.BaseRoutes.Schemes.Handle("/{scheme_id:[A-Za-z0-9]+}/patch", api.APISessionRequired(patchScheme)).Methods("PUT")
api.BaseRoutes.Schemes.Handle("/{scheme_id:[A-Za-z0-9]+}/teams", api.APISessionRequiredTrustRequester(getTeamsForScheme)).Methods("GET")
api.BaseRoutes.Schemes.Handle("/{scheme_id:[A-Za-z0-9]+}/channels", api.APISessionRequiredTrustRequester(getChannelsForScheme)).Methods("GET")
}
func createScheme(c *Context, w http.ResponseWriter, r *http.Request) {
var scheme model.Scheme
if jsonErr := json.NewDecoder(r.Body).Decode(&scheme); jsonErr != nil {
c.SetInvalidParamWithErr("scheme", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createScheme", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "scheme", &scheme)
if c.App.Channels().License() == nil || (!*c.App.Channels().License().Features.CustomPermissionsSchemes && c.App.Channels().License().SkuShortName != model.LicenseShortSkuProfessional) {
c.Err = model.NewAppError("Api4.CreateScheme", "api.scheme.create_scheme.license.error", nil, "", http.StatusNotImplemented)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementPermissions) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementPermissions)
return
}
returnedScheme, err := c.App.CreateScheme(&scheme)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(returnedScheme)
auditRec.AddEventObjectType("scheme")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(returnedScheme); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getScheme(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireSchemeId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementPermissions) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementPermissions)
return
}
scheme, err := c.App.GetScheme(c.Params.SchemeId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(scheme); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getSchemes(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementPermissions) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementPermissions)
return
}
scope := c.Params.Scope
if scope != "" && scope != model.SchemeScopeTeam && scope != model.SchemeScopeChannel {
c.SetInvalidParam("scope")
return
}
schemes, appErr := c.App.GetSchemesPage(c.Params.Scope, c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(schemes)
if err != nil {
c.Err = model.NewAppError("getSchemes", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getTeamsForScheme(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireSchemeId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementTeams) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementTeams)
return
}
scheme, appErr := c.App.GetScheme(c.Params.SchemeId)
if appErr != nil {
c.Err = appErr
return
}
if scheme.Scope != model.SchemeScopeTeam {
c.Err = model.NewAppError("Api4.GetTeamsForScheme", "api.scheme.get_teams_for_scheme.scope.error", nil, "", http.StatusBadRequest)
return
}
teams, appErr := c.App.GetTeamsForSchemePage(scheme, c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(teams)
if err != nil {
c.Err = model.NewAppError("getTeamsForScheme", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getChannelsForScheme(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireSchemeId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementChannels) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementChannels)
return
}
scheme, err := c.App.GetScheme(c.Params.SchemeId)
if err != nil {
c.Err = err
return
}
if scheme.Scope != model.SchemeScopeChannel {
c.Err = model.NewAppError("Api4.GetChannelsForScheme", "api.scheme.get_channels_for_scheme.scope.error", nil, "", http.StatusBadRequest)
return
}
channels, err := c.App.GetChannelsForSchemePage(scheme, c.Params.Page, c.Params.PerPage)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(channels); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func patchScheme(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireSchemeId()
if c.Err != nil {
return
}
var patch model.SchemePatch
if jsonErr := json.NewDecoder(r.Body).Decode(&patch); jsonErr != nil {
c.SetInvalidParamWithErr("scheme", jsonErr)
return
}
auditRec := c.MakeAuditRecord("patchScheme", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "scheme_patch", &patch)
defer c.LogAuditRec(auditRec)
if c.App.Channels().License() == nil || (!*c.App.Channels().License().Features.CustomPermissionsSchemes && c.App.Channels().License().SkuShortName != model.LicenseShortSkuProfessional) {
c.Err = model.NewAppError("Api4.PatchScheme", "api.scheme.patch_scheme.license.error", nil, "", http.StatusNotImplemented)
return
}
audit.AddEventParameter(auditRec, "scheme_id", c.Params.SchemeId)
scheme, err := c.App.GetScheme(c.Params.SchemeId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(scheme)
auditRec.AddEventObjectType("scheme")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementPermissions) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementPermissions)
return
}
scheme, err = c.App.PatchScheme(scheme, &patch)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(scheme)
auditRec.Success()
c.LogAudit("")
if err := json.NewEncoder(w).Encode(scheme); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteScheme(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireSchemeId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("deleteScheme", audit.Fail)
audit.AddEventParameter(auditRec, "scheme_id", c.Params.SchemeId)
defer c.LogAuditRec(auditRec)
if c.App.Channels().License() == nil || (!*c.App.Channels().License().Features.CustomPermissionsSchemes && c.App.Channels().License().SkuShortName != model.LicenseShortSkuProfessional) {
c.Err = model.NewAppError("Api4.DeleteScheme", "api.scheme.delete_scheme.license.error", nil, "", http.StatusNotImplemented)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementPermissions) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementPermissions)
return
}
scheme, err := c.App.DeleteScheme(c.Params.SchemeId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(scheme)
auditRec.AddEventObjectType("scheme")
auditRec.Success()
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
)
func (api *API) InitSharedChannels() {
api.BaseRoutes.SharedChannels.Handle("/{team_id:[A-Za-z0-9]+}", api.APISessionRequired(getSharedChannels)).Methods("GET")
api.BaseRoutes.SharedChannels.Handle("/remote_info/{remote_id:[A-Za-z0-9]+}", api.APISessionRequired(getRemoteClusterInfo)).Methods("GET")
}
func getSharedChannels(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
// make sure remote cluster service is enabled.
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
c.Err = appErr
return
}
// make sure user has access to the team.
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
opts := model.SharedChannelFilterOpts{
TeamId: c.Params.TeamId,
}
// only return channels the user is a member of, unless they are a shared channels manager.
if !c.App.HasPermissionTo(c.AppContext.Session().UserId, model.PermissionManageSharedChannels) {
opts.MemberId = c.AppContext.Session().UserId
}
channels, appErr := c.App.GetSharedChannels(c.Params.Page, c.Params.PerPage, opts)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(channels)
if err != nil {
c.SetJSONEncodingError(err)
return
}
w.Write(b)
}
func getRemoteClusterInfo(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireRemoteId()
if c.Err != nil {
return
}
// make sure remote cluster service is enabled.
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
c.Err = appErr
return
}
// GetRemoteClusterForUser will only return a remote if the user is a member of at
// least one channel shared by the remote. All other cases return error.
rc, appErr := c.App.GetRemoteClusterForUser(c.Params.RemoteId, c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
remoteInfo := rc.ToRemoteClusterInfo()
b, err := json.Marshal(remoteInfo)
if err != nil {
c.SetJSONEncodingError(err)
return
}
w.Write(b)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitStatus() {
api.BaseRoutes.User.Handle("/status", api.APISessionRequired(getUserStatus)).Methods("GET")
api.BaseRoutes.Users.Handle("/status/ids", api.APISessionRequired(getUserStatusesByIds)).Methods("POST")
api.BaseRoutes.User.Handle("/status", api.APISessionRequired(updateUserStatus)).Methods("PUT")
api.BaseRoutes.User.Handle("/status/custom", api.APISessionRequired(updateUserCustomStatus)).Methods("PUT")
api.BaseRoutes.User.Handle("/status/custom", api.APISessionRequired(removeUserCustomStatus)).Methods("DELETE")
// Both these handlers are for removing the recent custom status but the one with the POST method should be preferred
// as DELETE method doesn't support request body in the mobile app.
api.BaseRoutes.User.Handle("/status/custom/recent", api.APISessionRequired(removeUserRecentCustomStatus)).Methods("DELETE")
api.BaseRoutes.User.Handle("/status/custom/recent/delete", api.APISessionRequired(removeUserRecentCustomStatus)).Methods("POST")
}
func getUserStatus(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
// No permission check required
statusMap, err := c.App.GetUserStatusesByIds([]string{c.Params.UserId})
if err != nil {
c.Err = err
return
}
if len(statusMap) == 0 {
c.Err = model.NewAppError("UserStatus", "api.status.user_not_found.app_error", nil, "", http.StatusNotFound)
return
}
if err := json.NewEncoder(w).Encode(statusMap[0]); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUserStatusesByIds(c *Context, w http.ResponseWriter, r *http.Request) {
userIds := model.ArrayFromJSON(r.Body)
if len(userIds) == 0 {
c.SetInvalidParam("user_ids")
return
}
for _, userId := range userIds {
if len(userId) != 26 {
c.SetInvalidParam("user_ids")
return
}
}
// No permission check required
statuses, appErr := c.App.GetUserStatusesByIds(userIds)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(statuses)
if err != nil {
c.Err = model.NewAppError("getUserStatusesByIds", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func updateUserStatus(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
var status model.Status
if jsonErr := json.NewDecoder(r.Body).Decode(&status); jsonErr != nil {
c.SetInvalidParamWithErr("status", jsonErr)
return
}
// The user being updated in the payload must be the same one as indicated in the URL.
if status.UserId != c.Params.UserId {
c.SetInvalidParam("user_id")
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
currentStatus, err := c.App.GetStatus(c.Params.UserId)
if err == nil && currentStatus.Status == model.StatusOutOfOffice && status.Status != model.StatusOutOfOffice {
c.App.DisableAutoResponder(c.AppContext, c.Params.UserId, c.IsSystemAdmin())
}
switch status.Status {
case "online":
c.App.SetStatusOnline(c.Params.UserId, true)
case "offline":
c.App.SetStatusOffline(c.Params.UserId, true)
case "away":
c.App.SetStatusAwayIfNeeded(c.Params.UserId, true)
case "dnd":
c.App.SetStatusDoNotDisturbTimed(c.Params.UserId, status.DNDEndTime)
default:
c.SetInvalidParam("status")
return
}
getUserStatus(c, w, r)
}
func updateUserCustomStatus(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !*c.App.Config().TeamSettings.EnableCustomUserStatuses {
c.Err = model.NewAppError("updateUserCustomStatus", "api.custom_status.disabled", nil, "", http.StatusNotImplemented)
return
}
var customStatus model.CustomStatus
jsonErr := json.NewDecoder(r.Body).Decode(&customStatus)
if jsonErr != nil || (customStatus.Emoji == "" && customStatus.Text == "") || !customStatus.AreDurationAndExpirationTimeValid() {
c.SetInvalidParamWithErr("custom_status", jsonErr)
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
customStatus.PreSave()
err := c.App.SetCustomStatus(c.AppContext, c.Params.UserId, &customStatus)
if err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
func removeUserCustomStatus(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !*c.App.Config().TeamSettings.EnableCustomUserStatuses {
c.Err = model.NewAppError("removeUserCustomStatus", "api.custom_status.disabled", nil, "", http.StatusNotImplemented)
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if err := c.App.RemoveCustomStatus(c.AppContext, c.Params.UserId); err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
func removeUserRecentCustomStatus(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !*c.App.Config().TeamSettings.EnableCustomUserStatuses {
c.Err = model.NewAppError("removeUserRecentCustomStatus", "api.custom_status.disabled", nil, "", http.StatusNotImplemented)
return
}
var recentCustomStatus model.CustomStatus
if jsonErr := json.NewDecoder(r.Body).Decode(&recentCustomStatus); jsonErr != nil {
c.SetInvalidParamWithErr("recent_custom_status", jsonErr)
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if err := c.App.RemoveRecentCustomStatus(c.Params.UserId, &recentCustomStatus); err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"path"
"reflect"
"runtime"
"strconv"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/services/cache"
"github.com/mattermost/mattermost-server/v6/server/platform/services/upgrader"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/web"
)
const (
RedirectLocationCacheSize = 10000
DefaultServerBusySeconds = 3600
MaxServerBusySeconds = 86400
)
var redirectLocationDataCache = cache.NewLRU(cache.LRUOptions{
Size: RedirectLocationCacheSize,
})
func (api *API) InitSystem() {
api.BaseRoutes.System.Handle("/ping", api.APIHandler(getSystemPing)).Methods("GET")
api.BaseRoutes.System.Handle("/timezones", api.APISessionRequired(getSupportedTimezones)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/audits", api.APISessionRequired(getAudits)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/email/test", api.APISessionRequired(testEmail)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/site_url/test", api.APISessionRequired(testSiteURL)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/file/s3_test", api.APISessionRequired(testS3)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/database/recycle", api.APISessionRequired(databaseRecycle)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/caches/invalidate", api.APISessionRequired(invalidateCaches)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/logs", api.APISessionRequired(getLogs)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/logs/query", api.APISessionRequired(queryLogs)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/logs", api.APIHandler(postLog)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/analytics/old", api.APISessionRequired(getAnalytics)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/latest_version", api.APISessionRequired(getLatestVersion)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/redirect_location", api.APISessionRequiredTrustRequester(getRedirectLocation)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/notifications/ack", api.APISessionRequired(pushNotificationAck)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/server_busy", api.APISessionRequired(setServerBusy)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/server_busy", api.APISessionRequired(getServerBusyExpires)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/server_busy", api.APISessionRequired(clearServerBusy)).Methods("DELETE")
api.BaseRoutes.APIRoot.Handle("/upgrade_to_enterprise", api.APISessionRequired(upgradeToEnterprise)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/upgrade_to_enterprise/status", api.APISessionRequired(upgradeToEnterpriseStatus)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/restart", api.APISessionRequired(restart)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/warn_metrics/status", api.APISessionRequired(getWarnMetricsStatus)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/warn_metrics/ack/{warn_metric_id:[A-Za-z0-9-_]+}", api.APIHandler(sendWarnMetricAckEmail)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/warn_metrics/trial-license-ack/{warn_metric_id:[A-Za-z0-9-_]+}", api.APIHandler(requestTrialLicenseAndAckWarnMetric)).Methods("POST")
api.BaseRoutes.System.Handle("/notices/{team_id:[A-Za-z0-9]+}", api.APISessionRequired(getProductNotices)).Methods("GET")
api.BaseRoutes.System.Handle("/notices/view", api.APISessionRequired(updateViewedProductNotices)).Methods("PUT")
api.BaseRoutes.System.Handle("/support_packet", api.APISessionRequired(generateSupportPacket)).Methods("GET")
api.BaseRoutes.System.Handle("/onboarding/complete", api.APISessionRequired(getOnboarding)).Methods("GET")
api.BaseRoutes.System.Handle("/onboarding/complete", api.APISessionRequired(completeOnboarding)).Methods("POST")
api.BaseRoutes.System.Handle("/schema/version", api.APISessionRequired(getAppliedSchemaMigrations)).Methods("GET")
}
func generateSupportPacket(c *Context, w http.ResponseWriter, r *http.Request) {
const FileMime = "application/zip"
const OutputDirectory = "support_packet"
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("generateSupportPacket", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
// Support packet generation is limited to system admins (MM-42271).
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
// Checking to see if the server has a e10 or e20 license (this feature is only permitted for servers with licenses)
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("Api4.generateSupportPacket", "api.no_license", nil, "", http.StatusForbidden)
return
}
fileDatas := c.App.GenerateSupportPacket()
// Constructing the ZIP file name as per spec (mattermost_support_packet_YYYY-MM-DD-HH-MM.zip)
now := time.Now()
outputZipFilename := fmt.Sprintf("mattermost_support_packet_%s.zip", now.Format("2006-01-02-03-04"))
fileStorageBackend := c.App.FileBackend()
// We do this incase we get concurrent requests, we will always have a unique directory.
// This is to avoid the situation where we try to write to the same directory while we are trying to delete it (further down)
outputDirectoryToUse := OutputDirectory + "_" + model.NewId()
err := c.App.CreateZipFileAndAddFiles(fileStorageBackend, fileDatas, outputZipFilename, outputDirectoryToUse)
if err != nil {
c.Err = model.NewAppError("Api4.generateSupportPacket", "api.unable_to_create_zip_file", nil, err.Error(), http.StatusForbidden)
return
}
fileBytes, err := fileStorageBackend.ReadFile(path.Join(outputDirectoryToUse, outputZipFilename))
defer fileStorageBackend.RemoveDirectory(outputDirectoryToUse)
if err != nil {
c.Err = model.NewAppError("Api4.generateSupportPacket", "api.unable_to_read_file_from_backend", nil, err.Error(), http.StatusForbidden)
return
}
fileBytesReader := bytes.NewReader(fileBytes)
// Send the zip file back to client
// We are able to pass 0 for content size due to the fact that Golang's serveContent (https://golang.org/src/net/http/fs.go)
// already sets that for us
web.WriteFileResponse(outputZipFilename, FileMime, 0, now, *c.App.Config().ServiceSettings.WebserverMode, fileBytesReader, true, w, r)
}
func getSystemPing(c *Context, w http.ResponseWriter, r *http.Request) {
reqs := c.App.Config().ClientRequirements
s := make(map[string]string)
s[model.STATUS] = model.StatusOk
s["AndroidLatestVersion"] = reqs.AndroidLatestVersion
s["AndroidMinVersion"] = reqs.AndroidMinVersion
s["IosLatestVersion"] = reqs.IosLatestVersion
s["IosMinVersion"] = reqs.IosMinVersion
testflag := c.App.Config().FeatureFlags.TestFeature
if testflag != "off" {
s["TestFeatureFlag"] = testflag
}
actualGoroutines := runtime.NumGoroutine()
if *c.App.Config().ServiceSettings.GoroutineHealthThreshold > 0 && actualGoroutines >= *c.App.Config().ServiceSettings.GoroutineHealthThreshold {
mlog.Warn("The number of running goroutines is over the health threshold", mlog.Int("goroutines", actualGoroutines), mlog.Int("health_threshold", *c.App.Config().ServiceSettings.GoroutineHealthThreshold))
s[model.STATUS] = model.StatusUnhealthy
}
// Enhanced ping health check:
// If an extra form value is provided then perform extra health checks for
// database and file storage backends.
if r.FormValue("get_server_status") != "" {
dbStatusKey := "database_status"
s[dbStatusKey] = model.StatusOk
writeErr := c.App.DBHealthCheckWrite()
if writeErr != nil {
mlog.Warn("Unable to write to database.", mlog.Err(writeErr))
s[dbStatusKey] = model.StatusUnhealthy
s[model.STATUS] = model.StatusUnhealthy
}
writeErr = c.App.DBHealthCheckDelete()
if writeErr != nil {
mlog.Warn("Unable to remove ping health check value from database.", mlog.Err(writeErr))
s[dbStatusKey] = model.StatusUnhealthy
s[model.STATUS] = model.StatusUnhealthy
}
if s[dbStatusKey] == model.StatusOk {
mlog.Debug("Able to write to database.")
}
filestoreStatusKey := "filestore_status"
s[filestoreStatusKey] = model.StatusOk
appErr := c.App.TestFileStoreConnection()
if appErr != nil {
s[filestoreStatusKey] = model.StatusUnhealthy
s[model.STATUS] = model.StatusUnhealthy
}
w.Header().Set(model.STATUS, s[model.STATUS])
w.Header().Set(dbStatusKey, s[dbStatusKey])
w.Header().Set(filestoreStatusKey, s[filestoreStatusKey])
}
if deviceID := r.FormValue("device_id"); deviceID != "" {
s["CanReceiveNotifications"] = c.App.SendTestPushNotification(deviceID)
}
if s[model.STATUS] != model.StatusOk {
w.WriteHeader(http.StatusInternalServerError)
}
w.Write([]byte(model.MapToJSON(s)))
}
func testEmail(c *Context, w http.ResponseWriter, r *http.Request) {
var cfg *model.Config
err := json.NewDecoder(r.Body).Decode(&cfg)
if err != nil {
c.Logger.Warn("Error decoding the config", mlog.Err(err))
}
if cfg == nil {
cfg = c.App.Config()
}
if checkHasNilFields(&cfg.EmailSettings) {
c.Err = model.NewAppError("testEmail", "api.file.test_connection_email_settings_nil.app_error", nil, "", http.StatusBadRequest)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionTestEmail) {
c.SetPermissionError(model.PermissionTestEmail)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("testEmail", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
appErr := c.App.TestEmail(c.AppContext.Session().UserId, cfg)
if appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
func testSiteURL(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionTestSiteURL) {
c.SetPermissionError(model.PermissionTestSiteURL)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("testSiteURL", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
props := model.MapFromJSON(r.Body)
siteURL := props["site_url"]
if siteURL == "" {
c.SetInvalidParam("site_url")
return
}
appErr := c.App.TestSiteURL(siteURL)
if appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
func getAudits(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("getAudits", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadAudits) {
c.SetPermissionError(model.PermissionReadAudits)
return
}
audits, appErr := c.App.GetAuditsPage("", c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
audit.AddEventParameter(auditRec, "page", c.Params.Page)
audit.AddEventParameter(auditRec, "audits_per_page", c.Params.LogsPerPage)
if err := json.NewEncoder(w).Encode(audits); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func databaseRecycle(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRecycleDatabaseConnections) {
c.SetPermissionError(model.PermissionRecycleDatabaseConnections)
return
}
auditRec := c.MakeAuditRecord("databaseRecycle", audit.Fail)
defer c.LogAuditRec(auditRec)
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("databaseRecycle", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
c.App.RecycleDatabaseConnection()
auditRec.Success()
ReturnStatusOK(w)
}
func invalidateCaches(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionInvalidateCaches) {
c.SetPermissionError(model.PermissionInvalidateCaches)
return
}
auditRec := c.MakeAuditRecord("invalidateCaches", audit.Fail)
defer c.LogAuditRec(auditRec)
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("invalidateCaches", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
appErr := c.App.Srv().InvalidateAllCaches()
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
ReturnStatusOK(w)
}
func queryLogs(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("queryLogs", audit.Fail)
defer c.LogAuditRec(auditRec)
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("queryLogs", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionGetLogs) {
c.SetPermissionError(model.PermissionGetLogs)
return
}
var logFilter *model.LogFilter
err := json.NewDecoder(r.Body).Decode(&logFilter)
if err != nil {
c.Err = model.NewAppError("queryLogs", "api.system.logs.invalidFilter", nil, "", http.StatusInternalServerError)
return
}
logs, logerr := c.App.QueryLogs(c.Params.Page, c.Params.LogsPerPage, logFilter)
if logerr != nil {
c.Err = logerr
return
}
logsJSON := make(map[string][]interface{})
var result interface{}
for node, logLines := range logs {
for _, log := range logLines {
err2 := json.Unmarshal([]byte(log), &result)
if err2 == nil {
logsJSON[node] = append(logsJSON[node], result)
} else {
mlog.Warn("Error parsing log line in Server Logs")
}
}
}
auditRec.AddMeta("page", c.Params.Page)
auditRec.AddMeta("logs_per_page", c.Params.LogsPerPage)
w.Write(model.ToJSON(logsJSON))
}
func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("getLogs", audit.Fail)
defer c.LogAuditRec(auditRec)
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("getLogs", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionGetLogs) {
c.SetPermissionError(model.PermissionGetLogs)
return
}
lines, appErr := c.App.GetLogs(c.Params.Page, c.Params.LogsPerPage)
if appErr != nil {
c.Err = appErr
return
}
audit.AddEventParameter(auditRec, "page", c.Params.Page)
audit.AddEventParameter(auditRec, "logs_per_page", c.Params.LogsPerPage)
w.Write([]byte(model.ArrayToJSON(lines)))
}
func postLog(c *Context, w http.ResponseWriter, r *http.Request) {
forceToDebug := false
if !*c.App.Config().ServiceSettings.EnableDeveloper {
if c.AppContext.Session().UserId == "" {
c.Err = model.NewAppError("postLog", "api.context.permissions.app_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
forceToDebug = true
}
}
var m map[string]string
err := json.NewDecoder(r.Body).Decode(&m)
if err != nil {
c.Logger.Warn("Error decoding request.", mlog.Err(err))
}
if m == nil {
m = map[string]string{}
}
lvl := m["level"]
msg := m["message"]
if len(msg) > 400 {
msg = msg[0:399]
}
msg = "Client Logs API Endpoint Message: " + msg
fields := []mlog.Field{
mlog.String("type", "client_message"),
mlog.String("user_agent", c.AppContext.UserAgent()),
}
if !forceToDebug && lvl == "ERROR" {
mlog.Error(msg, fields...)
} else {
mlog.Debug(msg, fields...)
}
m["message"] = msg
err = json.NewEncoder(w).Encode(m)
if err != nil {
c.Logger.Warn("Error while writing response.", mlog.Err(err))
}
}
func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
teamId := r.URL.Query().Get("team_id")
if name == "" {
name = "standard"
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionGetAnalytics) {
c.SetPermissionError(model.PermissionGetAnalytics)
return
}
rows, appErr := c.App.GetAnalytics(name, teamId)
if appErr != nil {
c.Err = appErr
return
}
if rows == nil {
c.SetInvalidParam("name")
return
}
if err := json.NewEncoder(w).Encode(rows); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getLatestVersion(c *Context, w http.ResponseWriter, r *http.Request) {
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("latestVersion", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
resp, appErr := c.App.GetLatestVersion("https://api.github.com/repos/mattermost/mattermost-server/releases/latest")
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(resp)
if err != nil {
c.Logger.Warn("Unable to marshal JSON for latest version.", mlog.Err(err))
w.WriteHeader(http.StatusInternalServerError)
}
w.Write(b)
}
func getSupportedTimezones(c *Context, w http.ResponseWriter, r *http.Request) {
supportedTimezones := c.App.Timezones().GetSupported()
if supportedTimezones == nil {
supportedTimezones = make([]string, 0)
}
b, err := json.Marshal(supportedTimezones)
if err != nil {
c.Logger.Warn("Unable to marshal JSON in timezones.", mlog.Err(err))
w.WriteHeader(http.StatusInternalServerError)
}
w.Write(b)
}
func testS3(c *Context, w http.ResponseWriter, r *http.Request) {
var cfg *model.Config
err := json.NewDecoder(r.Body).Decode(&cfg)
if err != nil {
c.Logger.Warn("Error decoding the config", mlog.Err(err))
}
if cfg == nil {
cfg = c.App.Config()
}
if checkHasNilFields(&cfg.FileSettings) {
c.Err = model.NewAppError("testS3", "api.file.test_connection_s3_settings_nil.app_error", nil, "", http.StatusBadRequest)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionTestS3) {
c.SetPermissionError(model.PermissionTestS3)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("testS3", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
appErr := c.App.CheckMandatoryS3Fields(&cfg.FileSettings)
if appErr != nil {
c.Err = appErr
return
}
if *cfg.FileSettings.AmazonS3SecretAccessKey == model.FakeSetting {
cfg.FileSettings.AmazonS3SecretAccessKey = c.App.Config().FileSettings.AmazonS3SecretAccessKey
}
appErr = c.App.TestFileStoreConnectionWithConfig(&cfg.FileSettings)
if appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
func getRedirectLocation(c *Context, w http.ResponseWriter, r *http.Request) {
m := make(map[string]string)
m["location"] = ""
if !*c.App.Config().ServiceSettings.EnableLinkPreviews {
w.Write([]byte(model.MapToJSON(m)))
return
}
url := r.URL.Query().Get("url")
if url == "" {
c.SetInvalidParam("url")
return
}
var location string
if err := redirectLocationDataCache.Get(url, &location); err == nil {
m["location"] = location
w.Write([]byte(model.MapToJSON(m)))
return
}
client := c.App.HTTPService().MakeClient(false)
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
res, err := client.Head(url)
if err != nil {
// Cache failures to prevent retries.
redirectLocationDataCache.SetWithExpiry(url, "", 1*time.Hour)
// Always return a success status and a JSON string to limit information returned to client.
w.Write([]byte(model.MapToJSON(m)))
return
}
defer func() {
io.Copy(io.Discard, res.Body)
res.Body.Close()
}()
location = res.Header.Get("Location")
redirectLocationDataCache.SetWithExpiry(url, location, 1*time.Hour)
m["location"] = location
w.Write([]byte(model.MapToJSON(m)))
}
func pushNotificationAck(c *Context, w http.ResponseWriter, r *http.Request) {
var ack model.PushNotificationAck
if jsonErr := json.NewDecoder(r.Body).Decode(&ack); jsonErr != nil {
c.Err = model.NewAppError("pushNotificationAck",
"api.push_notifications_ack.message.parse.app_error",
nil,
"",
http.StatusBadRequest,
).Wrap(jsonErr)
return
}
if !*c.App.Config().EmailSettings.SendPushNotifications {
c.Err = model.NewAppError("pushNotificationAck", "api.push_notification.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
err := c.App.SendAckToPushProxy(&ack)
if ack.IsIdLoaded {
if err != nil {
// Log the error only, then continue to fetch notification message
c.App.NotificationsLog().Error("Notification ack not sent to push proxy",
mlog.String("ackId", ack.Id),
mlog.String("type", ack.NotificationType),
mlog.String("postId", ack.PostId),
mlog.String("status", err.Error()),
)
}
// Return post data only when PostId is passed.
if ack.PostId != "" && ack.NotificationType == model.PushTypeMessage {
if _, appErr := c.App.GetPostIfAuthorized(c.AppContext, ack.PostId, c.AppContext.Session(), false); appErr != nil {
c.Err = appErr
return
}
notificationInterface := c.App.Notification()
if notificationInterface == nil {
c.Err = model.NewAppError("pushNotificationAck", "api.system.id_loaded.not_available.app_error", nil, "", http.StatusFound)
return
}
msg, appError := notificationInterface.GetNotificationMessage(&ack, c.AppContext.Session().UserId)
if appError != nil {
c.Err = model.NewAppError("pushNotificationAck", "api.push_notification.id_loaded.fetch.app_error", nil, appError.Error(), http.StatusInternalServerError)
return
}
if err2 := json.NewEncoder(w).Encode(msg); err2 != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err2))
}
}
return
} else if err != nil {
c.Err = model.NewAppError("pushNotificationAck", "api.push_notifications_ack.forward.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
ReturnStatusOK(w)
}
func setServerBusy(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
// number of seconds to keep server marked busy
secs := r.URL.Query().Get("seconds")
if secs == "" {
secs = strconv.FormatInt(DefaultServerBusySeconds, 10)
}
i, err := strconv.ParseInt(secs, 10, 64)
if err != nil || i <= 0 || i > MaxServerBusySeconds {
c.SetInvalidURLParam(fmt.Sprintf("seconds must be 1 - %d", MaxServerBusySeconds))
return
}
auditRec := c.MakeAuditRecord("setServerBusy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "seconds", i)
c.App.Srv().Platform().Busy.Set(time.Second * time.Duration(i))
mlog.Warn("server busy state activated - non-critical services disabled", mlog.Int64("seconds", i))
auditRec.Success()
ReturnStatusOK(w)
}
func clearServerBusy(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
auditRec := c.MakeAuditRecord("clearServerBusy", audit.Fail)
defer c.LogAuditRec(auditRec)
c.App.Srv().Platform().Busy.Clear()
mlog.Info("server busy state cleared - non-critical services enabled")
auditRec.Success()
ReturnStatusOK(w)
}
func getServerBusyExpires(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
// We call to ToJSON because it actually returns a different struct
// along with doing some computations.
sbsJSON, jsonErr := c.App.Srv().Platform().Busy.ToJSON()
if jsonErr != nil {
mlog.Warn(jsonErr.Error())
}
if _, err := w.Write(sbsJSON); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func upgradeToEnterprise(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("upgradeToEnterprise", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if model.BuildEnterpriseReady == "true" {
c.Err = model.NewAppError("upgradeToEnterprise", "api.upgrade_to_enterprise.already-enterprise.app_error", nil, "", http.StatusTooManyRequests)
return
}
percentage, _ := c.App.Srv().UpgradeToE0Status()
if percentage > 0 {
c.Err = model.NewAppError("upgradeToEnterprise", "api.upgrade_to_enterprise.app_error", nil, "", http.StatusTooManyRequests)
return
}
if percentage == 100 {
c.Err = model.NewAppError("upgradeToEnterprise", "api.upgrade_to_enterprise.already-done.app_error", nil, "", http.StatusTooManyRequests)
return
}
if err := c.App.Srv().CanIUpgradeToE0(); err != nil {
var ipErr *upgrader.InvalidPermissions
var iaErr *upgrader.InvalidArch
switch {
case errors.As(err, &ipErr):
params := map[string]any{
"MattermostUsername": ipErr.MattermostUsername,
"FileUsername": ipErr.FileUsername,
"Path": ipErr.Path,
}
if ipErr.ErrType == "invalid-user-and-permission" {
c.Err = model.NewAppError("upgradeToEnterprise", "api.upgrade_to_enterprise.invalid-user-and-permission.app_error", params, err.Error(), http.StatusForbidden)
} else if ipErr.ErrType == "invalid-user" {
c.Err = model.NewAppError("upgradeToEnterprise", "api.upgrade_to_enterprise.invalid-user.app_error", params, err.Error(), http.StatusForbidden)
} else if ipErr.ErrType == "invalid-permission" {
c.Err = model.NewAppError("upgradeToEnterprise", "api.upgrade_to_enterprise.invalid-permission.app_error", params, err.Error(), http.StatusForbidden)
}
case errors.As(err, &iaErr):
c.Err = model.NewAppError("upgradeToEnterprise", "api.upgrade_to_enterprise.system_not_supported.app_error", nil, err.Error(), http.StatusForbidden)
default:
c.Err = model.NewAppError("upgradeToEnterprise", "api.upgrade_to_enterprise.generic_error.app_error", nil, err.Error(), http.StatusForbidden)
}
return
}
c.App.Srv().Go(func() {
c.App.Srv().UpgradeToE0()
})
auditRec.Success()
w.WriteHeader(http.StatusAccepted)
ReturnStatusOK(w)
}
func upgradeToEnterpriseStatus(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
percentage, err := c.App.Srv().UpgradeToE0Status()
var s map[string]any
if err != nil {
var isErr *upgrader.InvalidSignature
switch {
case errors.As(err, &isErr):
appErr := model.NewAppError("upgradeToEnterpriseStatus", "api.upgrade_to_enterprise_status.app_error", nil, err.Error(), http.StatusBadRequest)
s = map[string]any{"percentage": 0, "error": appErr.Message}
default:
appErr := model.NewAppError("upgradeToEnterpriseStatus", "api.upgrade_to_enterprise_status.signature.app_error", nil, err.Error(), http.StatusBadRequest)
s = map[string]any{"percentage": 0, "error": appErr.Message}
}
} else {
s = map[string]any{"percentage": percentage, "error": nil}
}
w.Write([]byte(model.StringInterfaceToJSON(s)))
}
func restart(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("restartServer", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
auditRec.Success()
ReturnStatusOK(w)
time.Sleep(1 * time.Second)
go func() {
c.App.Srv().Restart()
}()
}
func getWarnMetricsStatus(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleReadPermissions) {
c.SetPermissionError(model.SysconsoleReadPermissions...)
return
}
license := c.App.Channels().License()
if license != nil {
mlog.Debug("License is present, skip.")
return
}
status, appErr := c.App.GetWarnMetricsStatus()
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(status)
if err != nil {
c.Err = model.NewAppError("getWarnMetricsStatus", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func sendWarnMetricAckEmail(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("sendWarnMetricAckEmail", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
license := c.App.Channels().License()
if license != nil {
mlog.Debug("License is present, skip.")
return
}
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
var ack model.SendWarnMetricAck
if jsonErr := json.NewDecoder(r.Body).Decode(&ack); jsonErr != nil {
c.SetInvalidParamWithErr("ack", jsonErr)
return
}
appErr = c.App.NotifyAndSetWarnMetricAck(c.Params.WarnMetricId, user, ack.ForceAck, false)
if appErr != nil {
c.Err = appErr
}
auditRec.Success()
ReturnStatusOK(w)
}
func requestTrialLicenseAndAckWarnMetric(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("requestTrialLicenseAndAckWarnMetric", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if model.BuildEnterpriseReady != "true" {
mlog.Debug("Not Enterprise Edition, skip.")
return
}
license := c.App.Channels().License()
if license != nil {
mlog.Debug("License is present, skip.")
return
}
if err := c.App.RequestLicenseAndAckWarnMetric(c.AppContext, c.Params.WarnMetricId, false); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getProductNotices(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
client, parseError := model.NoticeClientTypeFromString(r.URL.Query().Get("client"))
if parseError != nil {
c.SetInvalidParam("client")
return
}
clientVersion := r.URL.Query().Get("clientVersion")
locale := r.URL.Query().Get("locale")
notices, appErr := c.App.GetProductNotices(c.AppContext, c.AppContext.Session().UserId, c.Params.TeamId, client, clientVersion, locale)
if appErr != nil {
c.Err = appErr
return
}
result, _ := notices.Marshal()
_, _ = w.Write(result)
}
func updateViewedProductNotices(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("updateViewedProductNotices", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
ids := model.ArrayFromJSON(r.Body)
appErr := c.App.UpdateViewedProductNotices(c.AppContext.Session().UserId, ids)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getOnboarding(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("getOnboarding", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
firstAdminCompleteSetupObj, err := c.App.GetOnboarding()
if err != nil {
c.Err = model.NewAppError("getOnboarding", "app.system.get_onboarding_request.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
if err := json.NewEncoder(w).Encode(firstAdminCompleteSetupObj); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func completeOnboarding(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.Err = model.NewAppError("completeOnboarding", "app.system.complete_onboarding_request.no_first_user", nil, "", http.StatusForbidden)
return
}
auditRec := c.MakeAuditRecord("completeOnboarding", audit.Fail)
defer c.LogAuditRec(auditRec)
onboardingRequest, err := model.CompleteOnboardingRequestFromReader(r.Body)
if err != nil {
c.Err = model.NewAppError("completeOnboarding", "app.system.complete_onboarding_request.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
audit.AddEventParameter(auditRec, "install_plugin", onboardingRequest.InstallPlugins)
audit.AddEventParameterAuditable(auditRec, "onboarding_request", onboardingRequest)
appErr := c.App.CompleteOnboarding(c.AppContext, onboardingRequest)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getAppliedSchemaMigrations(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleReadPermissions) {
c.SetPermissionError(model.SysconsoleReadPermissions...)
return
}
auditRec := c.MakeAuditRecord("getAppliedSchemaMigrations", audit.Fail)
defer c.LogAuditRec(auditRec)
migrations, appErr := c.App.GetAppliedSchemaMigrations()
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(migrations)
if err != nil {
c.Err = model.NewAppError("getAppliedMigrations", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
auditRec.Success()
}
// returns true if the data has nil fields
// this is being used for testS3 and testEmail methods
func checkHasNilFields(value any) bool {
v := reflect.Indirect(reflect.ValueOf(value))
if v.Kind() != reflect.Struct {
return false
}
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if field.Kind() == reflect.Ptr && field.IsNil() {
return true
}
}
return false
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
)
func (api *API) InitSystemLocal() {
api.BaseRoutes.System.Handle("/ping", api.APILocal(getSystemPing)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/logs", api.APILocal(getLogs)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/server_busy", api.APILocal(setServerBusy)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/server_busy", api.APILocal(getServerBusyExpires)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/server_busy", api.APILocal(clearServerBusy)).Methods("DELETE")
api.BaseRoutes.APIRoot.Handle("/integrity", api.APILocal(localCheckIntegrity)).Methods("POST")
api.BaseRoutes.System.Handle("/schema/version", api.APILocal(getAppliedSchemaMigrations)).Methods("GET")
}
func localCheckIntegrity(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("localCheckIntegrity", audit.Fail)
defer c.LogAuditRec(auditRec)
var results []model.IntegrityCheckResult
resultsChan := c.App.CheckIntegrity()
for result := range resultsChan {
results = append(results, result)
}
data, err := json.Marshal(results)
if err != nil {
c.Err = model.NewAppError("Api4.localCheckIntegrity", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(data)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
MaxAddMembersBatch = 256
MaximumBulkImportSize = 10 * 1024 * 1024
groupIDsParamPattern = "[^a-zA-Z0-9,]*"
)
var groupIDsQueryParamRegex *regexp.Regexp
func init() {
groupIDsQueryParamRegex = regexp.MustCompile(groupIDsParamPattern)
}
func (api *API) InitTeam() {
api.BaseRoutes.Teams.Handle("", api.APISessionRequired(createTeam)).Methods("POST")
api.BaseRoutes.Teams.Handle("", api.APISessionRequired(getAllTeams)).Methods("GET")
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/scheme", api.APISessionRequired(updateTeamScheme)).Methods("PUT")
api.BaseRoutes.Teams.Handle("/search", api.APISessionRequiredDisableWhenBusy(searchTeams)).Methods("POST")
api.BaseRoutes.TeamsForUser.Handle("", api.APISessionRequired(getTeamsForUser)).Methods("GET")
api.BaseRoutes.TeamsForUser.Handle("/unread", api.APISessionRequired(getTeamsUnreadForUser)).Methods("GET")
api.BaseRoutes.Team.Handle("", api.APISessionRequired(getTeam)).Methods("GET")
api.BaseRoutes.Team.Handle("", api.APISessionRequired(updateTeam)).Methods("PUT")
api.BaseRoutes.Team.Handle("", api.APISessionRequired(deleteTeam)).Methods("DELETE")
api.BaseRoutes.Team.Handle("/except", api.APISessionRequired(softDeleteTeamsExcept)).Methods("DELETE")
api.BaseRoutes.Team.Handle("/patch", api.APISessionRequired(patchTeam)).Methods("PUT")
api.BaseRoutes.Team.Handle("/restore", api.APISessionRequired(restoreTeam)).Methods("POST")
api.BaseRoutes.Team.Handle("/privacy", api.APISessionRequired(updateTeamPrivacy)).Methods("PUT")
api.BaseRoutes.Team.Handle("/stats", api.APISessionRequired(getTeamStats)).Methods("GET")
api.BaseRoutes.Team.Handle("/regenerate_invite_id", api.APISessionRequired(regenerateTeamInviteId)).Methods("POST")
api.BaseRoutes.Team.Handle("/image", api.APISessionRequiredTrustRequester(getTeamIcon)).Methods("GET")
api.BaseRoutes.Team.Handle("/image", api.APISessionRequired(setTeamIcon)).Methods("POST")
api.BaseRoutes.Team.Handle("/image", api.APISessionRequired(removeTeamIcon)).Methods("DELETE")
api.BaseRoutes.TeamMembers.Handle("", api.APISessionRequired(getTeamMembers)).Methods("GET")
api.BaseRoutes.TeamMembers.Handle("/ids", api.APISessionRequired(getTeamMembersByIds)).Methods("POST")
api.BaseRoutes.TeamMembersForUser.Handle("", api.APISessionRequired(getTeamMembersForUser)).Methods("GET")
api.BaseRoutes.TeamMembers.Handle("", api.APISessionRequired(addTeamMember)).Methods("POST")
api.BaseRoutes.Teams.Handle("/members/invite", api.APISessionRequired(addUserToTeamFromInvite)).Methods("POST")
api.BaseRoutes.TeamMembers.Handle("/batch", api.APISessionRequired(addTeamMembers)).Methods("POST")
api.BaseRoutes.TeamMember.Handle("", api.APISessionRequired(removeTeamMember)).Methods("DELETE")
api.BaseRoutes.TeamForUser.Handle("/unread", api.APISessionRequired(getTeamUnread)).Methods("GET")
api.BaseRoutes.TeamByName.Handle("", api.APISessionRequired(getTeamByName)).Methods("GET")
api.BaseRoutes.TeamMember.Handle("", api.APISessionRequired(getTeamMember)).Methods("GET")
api.BaseRoutes.TeamByName.Handle("/exists", api.APISessionRequired(teamExists)).Methods("GET")
api.BaseRoutes.TeamMember.Handle("/roles", api.APISessionRequired(updateTeamMemberRoles)).Methods("PUT")
api.BaseRoutes.TeamMember.Handle("/schemeRoles", api.APISessionRequired(updateTeamMemberSchemeRoles)).Methods("PUT")
api.BaseRoutes.Team.Handle("/import", api.APISessionRequired(importTeam)).Methods("POST")
api.BaseRoutes.Team.Handle("/invite/email", api.APISessionRequired(inviteUsersToTeam)).Methods("POST")
api.BaseRoutes.Team.Handle("/invite-guests/email", api.APISessionRequired(inviteGuestsToChannels)).Methods("POST")
api.BaseRoutes.Teams.Handle("/invites/email", api.APISessionRequired(invalidateAllEmailInvites)).Methods("DELETE")
api.BaseRoutes.Teams.Handle("/invite/{invite_id:[A-Za-z0-9]+}", api.APIHandler(getInviteInfo)).Methods("GET")
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/members_minus_group_members", api.APISessionRequired(teamMembersMinusGroupMembers)).Methods("GET")
}
func createTeam(c *Context, w http.ResponseWriter, r *http.Request) {
var team model.Team
if jsonErr := json.NewDecoder(r.Body).Decode(&team); jsonErr != nil {
c.SetInvalidParamWithErr("team", jsonErr)
return
}
team.Email = strings.ToLower(team.Email)
auditRec := c.MakeAuditRecord("createTeam", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "team", &team)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateTeam) {
c.Err = model.NewAppError("createTeam", "api.team.is_team_creation_allowed.disabled.app_error", nil, "", http.StatusForbidden)
return
}
// On a cloud license, we must check limits before allowing to create
if c.App.Channels().License().IsCloud() {
limits, err := c.App.Cloud().GetCloudLimits(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.createTeam", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
// If there are no limits for teams, for active teams, or the limit for active teams is less than 0, do nothing
if !(limits == nil || limits.Teams == nil || limits.Teams.Active == nil || *limits.Teams.Active <= 0) {
teamsUsage, appErr := c.App.GetTeamsUsage()
if appErr != nil {
c.Err = appErr
return
}
// if the number of active teams is greater than or equal to the limit, return 400
if teamsUsage.Active >= int64(*limits.Teams.Active) {
c.Err = model.NewAppError("Api4.createTeam", "api.cloud.teams_limit_reached.create", nil, "", http.StatusBadRequest)
return
}
}
}
rteam, err := c.App.CreateTeamWithUser(c.AppContext, &team, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
// Don't sanitize the team here since the user will be a team admin and their session won't reflect that yet
auditRec.Success()
auditRec.AddEventResultState(&team)
auditRec.AddEventObjectType("team")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rteam); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
team, err := c.App.GetTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
if (!team.AllowOpenInvite || team.Type != model.TeamOpen) && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
c.App.SanitizeTeam(*c.AppContext.Session(), team)
if err := json.NewEncoder(w).Encode(team); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getTeamByName(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamName()
if c.Err != nil {
return
}
team, err := c.App.GetTeamByName(c.Params.TeamName)
if err != nil {
c.Err = err
return
}
if (!team.AllowOpenInvite || team.Type != model.TeamOpen) && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
c.App.SanitizeTeam(*c.AppContext.Session(), team)
if err := json.NewEncoder(w).Encode(team); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
var team model.Team
if jsonErr := json.NewDecoder(r.Body).Decode(&team); jsonErr != nil {
c.SetInvalidParamWithErr("team", jsonErr)
return
}
team.Email = strings.ToLower(team.Email)
// The team being updated in the payload must be the same one as indicated in the URL.
if team.Id != c.Params.TeamId {
c.SetInvalidParam("id")
return
}
auditRec := c.MakeAuditRecord("updateTeam", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "team", &team)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeam) {
c.SetPermissionError(model.PermissionManageTeam)
return
}
updatedTeam, err := c.App.UpdateTeam(&team)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(updatedTeam)
auditRec.AddEventObjectType("team")
c.App.SanitizeTeam(*c.AppContext.Session(), updatedTeam)
if err := json.NewEncoder(w).Encode(updatedTeam); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func patchTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
var team model.TeamPatch
if jsonErr := json.NewDecoder(r.Body).Decode(&team); jsonErr != nil {
c.SetInvalidParamWithErr("team", jsonErr)
return
}
auditRec := c.MakeAuditRecord("patchTeam", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "team_patch", &team)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeam) {
c.SetPermissionError(model.PermissionManageTeam)
return
}
if oldTeam, err := c.App.GetTeam(c.Params.TeamId); err == nil {
auditRec.AddEventPriorState(oldTeam)
auditRec.AddEventObjectType("team")
}
patchedTeam, err := c.App.PatchTeam(c.Params.TeamId, &team)
if err != nil {
c.Err = err
return
}
c.App.SanitizeTeam(*c.AppContext.Session(), patchedTeam)
auditRec.Success()
auditRec.AddEventResultState(patchedTeam)
c.LogAudit("")
if err := json.NewEncoder(w).Encode(patchedTeam); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func restoreTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("restoreTeam", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeam) {
c.SetPermissionError(model.PermissionManageTeam)
return
}
// On a cloud license, we must check limits before allowing to restore
if c.App.Channels().License().IsCloud() {
limits, err := c.App.Cloud().GetCloudLimits(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.restoreTeam", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
// If there are no limits for teams, for active teams, or the limit for active teams is less than 0, do nothing
if !(limits == nil || limits.Teams == nil || limits.Teams.Active == nil || *limits.Teams.Active <= 0) {
teamsUsage, appErr := c.App.GetTeamsUsage()
if appErr != nil {
c.Err = appErr
return
}
// if the number of active teams is greater than or equal to the limit, return 400
if teamsUsage.Active >= int64(*limits.Teams.Active) {
c.Err = model.NewAppError("Api4.restoreTeam", "api.cloud.teams_limit_reached.restore", nil, "", http.StatusBadRequest)
return
}
}
}
err := c.App.RestoreTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
// Return the restored team to be consistent with RestoreChannel.
team, err := c.App.GetTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(team)
auditRec.AddEventObjectType("team")
auditRec.Success()
if err := json.NewEncoder(w).Encode(team); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateTeamPrivacy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
props := model.StringInterfaceFromJSON(r.Body)
privacy, ok := props["privacy"].(string)
if !ok {
c.SetInvalidParam("privacy")
return
}
var openInvite bool
switch privacy {
case model.TeamOpen:
openInvite = true
case model.TeamInvite:
openInvite = false
default:
c.SetInvalidParam("privacy")
return
}
auditRec := c.MakeAuditRecord("updateTeamPrivacy", audit.Fail)
audit.AddEventParameter(auditRec, "privacy", privacy)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeam) {
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
c.SetPermissionError(model.PermissionManageTeam)
return
}
if err := c.App.UpdateTeamPrivacy(c.Params.TeamId, privacy, openInvite); err != nil {
c.Err = err
return
}
// Return the updated team to be consistent with UpdateChannelPrivacy
team, err := c.App.GetTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(team)
auditRec.AddEventObjectType("team")
auditRec.Success()
if err := json.NewEncoder(w).Encode(team); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func regenerateTeamInviteId(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeam) {
c.SetPermissionError(model.PermissionManageTeam)
return
}
auditRec := c.MakeAuditRecord("regenerateTeamInviteId", audit.Fail)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
defer c.LogAuditRec(auditRec)
patchedTeam, err := c.App.RegenerateTeamInviteId(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
c.App.SanitizeTeam(*c.AppContext.Session(), patchedTeam)
if !*c.App.Config().PrivacySettings.ShowEmailAddress && !c.IsSystemAdmin() {
patchedTeam.Email = ""
}
auditRec.Success()
auditRec.AddEventResultState(patchedTeam)
auditRec.AddEventObjectType("team")
c.LogAudit("")
if err := json.NewEncoder(w).Encode(patchedTeam); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeam) {
c.SetPermissionError(model.PermissionManageTeam)
return
}
auditRec := c.MakeAuditRecord("deleteTeam", audit.Fail)
defer c.LogAuditRec(auditRec)
if team, err := c.App.GetTeam(c.Params.TeamId); err == nil {
audit.AddEventParameterAuditable(auditRec, "team", team)
}
var err *model.AppError
if c.Params.Permanent {
if *c.App.Config().ServiceSettings.EnableAPITeamDeletion {
err = c.App.PermanentDeleteTeamId(c.AppContext, c.Params.TeamId)
} else {
err = model.NewAppError("deleteTeam", "api.user.delete_team.not_enabled.app_error", nil, "teamId="+c.Params.TeamId, http.StatusUnauthorized)
}
} else {
err = c.App.SoftDeleteTeam(c.Params.TeamId)
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func softDeleteTeamsExcept(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeam) {
c.SetPermissionError(model.PermissionManageTeam)
return
}
err := c.App.SoftDeleteAllTeamsExcept(c.Params.TeamId)
if err != nil {
c.Err = err
}
ReturnStatusOK(w)
}
func getTeamsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if c.AppContext.Session().UserId != c.Params.UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementUsers) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementUsers)
return
}
teams, appErr := c.App.GetTeamsForUser(c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
c.App.SanitizeTeams(*c.AppContext.Session(), teams)
if !*c.App.Config().PrivacySettings.ShowEmailAddress && !c.IsSystemAdmin() {
for _, team := range teams {
team.Email = ""
}
}
js, err := json.Marshal(teams)
if err != nil {
c.Err = model.NewAppError("getTeamsForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getTeamsUnreadForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if c.AppContext.Session().UserId != c.Params.UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
// optional team id to be excluded from the result
teamId := r.URL.Query().Get("exclude_team")
includeCollapsedThreads := r.URL.Query().Get("include_collapsed_threads") == "true"
unreadTeamsList, appErr := c.App.GetTeamsUnreadForUser(teamId, c.Params.UserId, includeCollapsedThreads)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(unreadTeamsList)
if err != nil {
c.Err = model.NewAppError("getTeamsUnreadForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getTeamMember(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId().RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
canSee, appErr := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
team, appErr := c.App.GetTeamMember(c.Params.TeamId, c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(team); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
sort := r.URL.Query().Get("sort")
excludeDeletedUsers := r.URL.Query().Get("exclude_deleted_users")
excludeDeletedUsersBool, _ := strconv.ParseBool(excludeDeletedUsers)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
teamMembersGetOptions := &model.TeamMembersGetOptions{
Sort: sort,
ExcludeDeletedUsers: excludeDeletedUsersBool,
ViewRestrictions: restrictions,
}
members, appErr := c.App.GetTeamMembers(c.Params.TeamId, c.Params.Page*c.Params.PerPage, c.Params.PerPage, teamMembersGetOptions)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(members)
if err != nil {
c.Err = model.NewAppError("getTeamMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getTeamMembersForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadOtherUsersTeams) {
c.SetPermissionError(model.PermissionReadOtherUsersTeams)
return
}
canSee, appErr := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
members, appErr := c.App.GetTeamMembersForUser(c.Params.UserId, "", true)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(members)
if err != nil {
c.Err = model.NewAppError("getTeamMembersForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getTeamMembersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
var userIDs []string
err := json.NewDecoder(r.Body).Decode(&userIDs)
if err != nil || len(userIDs) == 0 {
c.SetInvalidParamWithErr("user_ids", err)
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
members, appErr := c.App.GetTeamMembersByIds(c.Params.TeamId, userIDs, restrictions)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(members)
if err != nil {
c.Err = model.NewAppError("getTeamMembersByIds", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func addTeamMember(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
var err *model.AppError
var member model.TeamMember
if jsonErr := json.NewDecoder(r.Body).Decode(&member); jsonErr != nil {
c.Err = model.NewAppError("addTeamMember", "api.team.add_team_member.invalid_body.app_error", nil, "Error in model.TeamMemberFromJSON()", http.StatusBadRequest).Wrap(jsonErr)
return
}
if member.TeamId != c.Params.TeamId {
c.SetInvalidParam("team_id")
return
}
if !model.IsValidId(member.UserId) {
c.SetInvalidParam("user_id")
return
}
auditRec := c.MakeAuditRecord("addTeamMember", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "member", &member)
defer c.LogAuditRec(auditRec)
if member.UserId == c.AppContext.Session().UserId {
var team *model.Team
team, err = c.App.GetTeam(member.TeamId)
if err != nil {
c.Err = err
return
}
if team.AllowOpenInvite && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionJoinPublicTeams) {
c.SetPermissionError(model.PermissionJoinPublicTeams)
return
}
if !team.AllowOpenInvite && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionJoinPrivateTeams) {
c.SetPermissionError(model.PermissionJoinPrivateTeams)
return
}
} else {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), member.TeamId, model.PermissionAddUserToTeam) {
c.SetPermissionError(model.PermissionAddUserToTeam)
return
}
}
team, err := c.App.GetTeam(member.TeamId)
if err != nil {
c.Err = err
return
}
audit.AddEventParameterAuditable(auditRec, "team", team)
if team.IsGroupConstrained() {
nonMembers, err := c.App.FilterNonGroupTeamMembers([]string{member.UserId}, team)
if err != nil {
if v, ok := err.(*model.AppError); ok {
c.Err = v
} else {
c.Err = model.NewAppError("addTeamMember", "api.team.add_members.error", nil, err.Error(), http.StatusBadRequest)
}
return
}
if len(nonMembers) > 0 {
c.Err = model.NewAppError("addTeamMember", "api.team.add_members.user_denied", map[string]any{"UserIDs": nonMembers}, "", http.StatusBadRequest)
return
}
}
var tm *model.TeamMember
tm, err = c.App.AddTeamMember(c.AppContext, member.TeamId, member.UserId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(tm)
auditRec.AddEventObjectType("team_member") // TODO verify this is the final state. should it be the team instead?
auditRec.Success()
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(tm); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func addUserToTeamFromInvite(c *Context, w http.ResponseWriter, r *http.Request) {
tokenId := r.URL.Query().Get("token")
inviteId := r.URL.Query().Get("invite_id")
var member *model.TeamMember
var err *model.AppError
auditRec := c.MakeAuditRecord("addUserToTeamFromInvite", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "invite_id", inviteId)
if tokenId != "" {
member, err = c.App.AddTeamMemberByToken(c.AppContext, c.AppContext.Session().UserId, tokenId)
} else if inviteId != "" {
if c.AppContext.Session().Props[model.SessionPropIsGuest] == "true" {
c.Err = model.NewAppError("addUserToTeamFromInvite", "api.team.add_user_to_team_from_invite.guest.app_error", nil, "", http.StatusForbidden)
return
}
member, err = c.App.AddTeamMemberByInviteId(c.AppContext, inviteId, c.AppContext.Session().UserId)
} else {
err = model.NewAppError("addTeamMember", "api.team.add_user_to_team.missing_parameter.app_error", nil, "", http.StatusBadRequest)
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
if member != nil {
auditRec.AddMeta("member", member)
}
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(member); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func addTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) {
graceful := r.URL.Query().Get("graceful") != ""
c.RequireTeamId()
if c.Err != nil {
return
}
var appErr *model.AppError
var members []*model.TeamMember
if jsonErr := json.NewDecoder(r.Body).Decode(&members); jsonErr != nil {
c.SetInvalidParamWithErr("members", jsonErr)
return
}
if len(members) > MaxAddMembersBatch {
c.SetInvalidParam("too many members in batch")
return
}
if len(members) == 0 {
c.SetInvalidParam("no members in batch")
return
}
auditRec := c.MakeAuditRecord("addTeamMembers", audit.Fail)
audit.AddEventParameterAuditableArray(auditRec, "members", members)
defer c.LogAuditRec(auditRec)
auditRec.AddMeta("count", len(members))
var memberIDs []string
for _, member := range members {
memberIDs = append(memberIDs, member.UserId)
}
auditRec.AddMeta("user_ids", memberIDs)
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
audit.AddEventParameterAuditable(auditRec, "team", team)
if team.IsGroupConstrained() {
nonMembers, err := c.App.FilterNonGroupTeamMembers(memberIDs, team)
if err != nil {
if v, ok := err.(*model.AppError); ok {
c.Err = v
} else {
c.Err = model.NewAppError("addTeamMembers", "api.team.add_members.error", nil, "", http.StatusBadRequest).Wrap(err)
}
return
}
if len(nonMembers) > 0 {
c.Err = model.NewAppError("addTeamMembers", "api.team.add_members.user_denied", map[string]any{"UserIDs": nonMembers}, "", http.StatusBadRequest)
return
}
}
var userIDs []string
for _, member := range members {
if member.TeamId != c.Params.TeamId {
c.SetInvalidParam("team_id for member with user_id=" + member.UserId)
return
}
if !model.IsValidId(member.UserId) {
c.SetInvalidParam("user_id")
return
}
userIDs = append(userIDs, member.UserId)
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionAddUserToTeam) {
c.SetPermissionError(model.PermissionAddUserToTeam)
return
}
membersWithErrors, appErr := c.App.AddTeamMembers(c.AppContext, c.Params.TeamId, userIDs, c.AppContext.Session().UserId, graceful)
if len(membersWithErrors) != 0 {
errList := make([]string, 0, len(membersWithErrors))
for _, m := range membersWithErrors {
if m.Error != nil {
errList = append(errList, model.TeamMemberWithErrorToString(m))
}
}
auditRec.AddMeta("errors", errList)
}
if appErr != nil {
c.Err = appErr
return
}
var (
js []byte
err error
)
if graceful {
// in 'graceful' mode we allow a different return value, notifying the client which users were not added
js, err = json.Marshal(membersWithErrors)
} else {
js, err = json.Marshal(model.TeamMembersWithErrorToTeamMembers(membersWithErrors))
}
if err != nil {
c.Err = model.NewAppError("addTeamMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.WriteHeader(http.StatusCreated)
w.Write(js)
}
func removeTeamMember(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId().RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("removeTeamMember", audit.Fail)
defer c.LogAuditRec(auditRec)
if c.AppContext.Session().UserId != c.Params.UserId {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionRemoveUserFromTeam) {
c.SetPermissionError(model.PermissionRemoveUserFromTeam)
return
}
}
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
team, err := c.App.GetTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
audit.AddEventParameterAuditable(auditRec, "team", team)
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
audit.AddEventParameterAuditable(auditRec, "user", user)
if team.IsGroupConstrained() && (c.Params.UserId != c.AppContext.Session().UserId) && !user.IsBot {
c.Err = model.NewAppError("removeTeamMember", "api.team.remove_member.group_constrained.app_error", nil, "", http.StatusBadRequest)
return
}
if err := c.App.RemoveUserFromTeam(c.AppContext, c.Params.TeamId, c.Params.UserId, c.AppContext.Session().UserId); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getTeamUnread(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId().RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
unreadTeam, err := c.App.GetTeamUnread(c.Params.TeamId, c.Params.UserId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(unreadTeam); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getTeamStats(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
restrictions, err := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
stats, err := c.App.GetTeamStats(c.Params.TeamId, restrictions)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(stats); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateTeamMemberRoles(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId().RequireUserId()
if c.Err != nil {
return
}
props := model.MapFromJSON(r.Body)
newRoles := props["roles"]
if !model.IsValidUserRoles(newRoles) {
c.SetInvalidParam("team_member_roles")
return
}
auditRec := c.MakeAuditRecord("updateTeamMemberRoles", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "roles", newRoles)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeamRoles) {
c.SetPermissionError(model.PermissionManageTeamRoles)
return
}
teamMember, err := c.App.UpdateTeamMemberRoles(c.Params.TeamId, c.Params.UserId, newRoles)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(teamMember)
auditRec.AddEventObjectType("team_member")
ReturnStatusOK(w)
}
func updateTeamMemberSchemeRoles(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId().RequireUserId()
if c.Err != nil {
return
}
var schemeRoles model.SchemeRoles
if jsonErr := json.NewDecoder(r.Body).Decode(&schemeRoles); jsonErr != nil {
c.SetInvalidParamWithErr("scheme_roles", jsonErr)
return
}
auditRec := c.MakeAuditRecord("updateTeamMemberSchemeRoles", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "scheme_roles", &schemeRoles)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeamRoles) {
c.SetPermissionError(model.PermissionManageTeamRoles)
return
}
teamMember, err := c.App.UpdateTeamMemberSchemeRoles(c.Params.TeamId, c.Params.UserId, schemeRoles.SchemeGuest, schemeRoles.SchemeUser, schemeRoles.SchemeAdmin)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(teamMember)
auditRec.AddEventObjectType("team_member")
ReturnStatusOK(w)
}
func getAllTeams(c *Context, w http.ResponseWriter, r *http.Request) {
teams := []*model.Team{}
var appErr *model.AppError
var teamsWithCount *model.TeamsWithCount
opts := &model.TeamSearch{}
if c.Params.ExcludePolicyConstrained {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
opts.ExcludePolicyConstrained = model.NewBool(true)
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
opts.IncludePolicyID = model.NewBool(true)
}
listPrivate := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionListPrivateTeams)
listPublic := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionListPublicTeams)
limit := c.Params.PerPage
offset := limit * c.Params.Page
if listPrivate && listPublic {
} else if listPrivate {
opts.AllowOpenInvite = model.NewBool(false)
} else if listPublic {
opts.AllowOpenInvite = model.NewBool(true)
} else {
// The user doesn't have permissions to list private as well as public teams.
c.Err = model.NewAppError("getAllTeams", "api.team.get_all_teams.insufficient_permissions", nil, "", http.StatusForbidden)
return
}
if c.Params.IncludeTotalCount {
teamsWithCount, appErr = c.App.GetAllTeamsPageWithCount(offset, limit, opts)
} else {
teams, appErr = c.App.GetAllTeamsPage(offset, limit, opts)
}
if appErr != nil {
c.Err = appErr
return
}
var (
js []byte
err error
)
if c.Params.IncludeTotalCount {
c.App.SanitizeTeams(*c.AppContext.Session(), teamsWithCount.Teams)
js, err = json.Marshal(teamsWithCount)
} else {
c.App.SanitizeTeams(*c.AppContext.Session(), teams)
js, err = json.Marshal(teams)
}
if err != nil {
c.Err = model.NewAppError("getAllTeams", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func searchTeams(c *Context, w http.ResponseWriter, r *http.Request) {
var props model.TeamSearch
if err := json.NewDecoder(r.Body).Decode(&props); err != nil {
c.SetInvalidParamWithErr("team_search", err)
return
}
// Only system managers may use the ExcludePolicyConstrained field
if props.ExcludePolicyConstrained != nil && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
// policy ID may only be used through the /data_retention/policies endpoint
props.PolicyID = nil
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
props.IncludePolicyID = model.NewBool(true)
}
var (
teams []*model.Team
totalCount int64
appErr *model.AppError
)
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionListPrivateTeams) && c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionListPublicTeams) {
teams, totalCount, appErr = c.App.SearchAllTeams(&props)
} else if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionListPrivateTeams) {
if props.Page != nil || props.PerPage != nil {
c.Err = model.NewAppError("searchTeams", "api.team.search_teams.pagination_not_implemented.private_team_search", nil, "", http.StatusNotImplemented)
return
}
teams, appErr = c.App.SearchPrivateTeams(&props)
} else if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionListPublicTeams) {
if props.Page != nil || props.PerPage != nil {
c.Err = model.NewAppError("searchTeams", "api.team.search_teams.pagination_not_implemented.public_team_search", nil, "", http.StatusNotImplemented)
return
}
teams, appErr = c.App.SearchPublicTeams(&props)
} else {
teams = []*model.Team{}
}
if appErr != nil {
c.Err = appErr
return
}
c.App.SanitizeTeams(*c.AppContext.Session(), teams)
var payload []byte
if props.Page != nil && props.PerPage != nil {
twc := map[string]any{"teams": teams, "total_count": totalCount}
payload = model.ToJSON(twc)
} else {
js, err := json.Marshal(teams)
if err != nil {
c.Err = model.NewAppError("searchTeams", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
payload = js
}
w.Write(payload)
}
func teamExists(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamName()
if c.Err != nil {
return
}
team, err := c.App.GetTeamByName(c.Params.TeamName)
if err != nil && err.StatusCode != http.StatusNotFound {
c.Err = err
return
}
exists := false
if team != nil {
var teamMember *model.TeamMember
teamMember, err = c.App.GetTeamMember(team.Id, c.AppContext.Session().UserId)
if err != nil && err.StatusCode != http.StatusNotFound {
c.Err = err
return
}
// Verify that the user can see the team (be a member or have the permission to list the team)
if (teamMember != nil && teamMember.DeleteAt == 0) ||
(team.AllowOpenInvite && c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionListPublicTeams)) ||
(!team.AllowOpenInvite && c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionListPrivateTeams)) {
exists = true
}
}
resp := map[string]bool{"exists": exists}
w.Write([]byte(model.MapBoolToJSON(resp)))
}
func importTeam(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("importTeam", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionImportTeam) {
c.SetPermissionError(model.PermissionImportTeam)
return
}
if err := r.ParseMultipartForm(MaximumBulkImportSize); err != nil {
c.Err = model.NewAppError("importTeam", "api.team.import_team.parse.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
importFromArray, ok := r.MultipartForm.Value["importFrom"]
if !ok || len(importFromArray) < 1 {
c.Err = model.NewAppError("importTeam", "api.team.import_team.no_import_from.app_error", nil, "", http.StatusBadRequest)
return
}
importFrom := importFromArray[0]
fileSizeStr, ok := r.MultipartForm.Value["filesize"]
if !ok || len(fileSizeStr) < 1 {
c.Err = model.NewAppError("importTeam", "api.team.import_team.unavailable.app_error", nil, "", http.StatusBadRequest)
return
}
fileSize, err := strconv.ParseInt(fileSizeStr[0], 10, 64)
if err != nil {
c.Err = model.NewAppError("importTeam", "api.team.import_team.integer.app_error", nil, "", http.StatusBadRequest)
return
}
fileInfoArray, ok := r.MultipartForm.File["file"]
if !ok {
c.Err = model.NewAppError("importTeam", "api.team.import_team.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(fileInfoArray) <= 0 {
c.Err = model.NewAppError("importTeam", "api.team.import_team.array.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("importTeam", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
fileInfo := fileInfoArray[0]
fileData, err := fileInfo.Open()
if err != nil {
c.Err = model.NewAppError("importTeam", "api.team.import_team.open.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
defer fileData.Close()
audit.AddEventParameter(auditRec, "filename", fileInfo.Filename)
audit.AddEventParameter(auditRec, "filesize", fileSize)
audit.AddEventParameter(auditRec, "from", importFrom)
var log *bytes.Buffer
data := map[string]string{}
switch importFrom {
case "slack":
var err *model.AppError
if err, log = c.App.SlackImport(c.AppContext, fileData, fileSize, c.Params.TeamId); err != nil {
c.Err = err
c.Err.StatusCode = http.StatusBadRequest
}
data["results"] = base64.StdEncoding.EncodeToString(log.Bytes())
default:
c.Err = model.NewAppError("importTeam", "api.team.import_team.unknown_import_from.app_error", nil, "", http.StatusBadRequest)
}
if c.Err != nil {
w.WriteHeader(c.Err.StatusCode)
return
}
auditRec.Success()
w.Write([]byte(model.MapToJSON(data)))
}
func inviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request) {
graceful := r.URL.Query().Get("graceful") != ""
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionInviteUser) {
c.SetPermissionError(model.PermissionInviteUser)
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionAddUserToTeam) {
c.SetPermissionError(model.PermissionInviteUser)
return
}
bf, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.inviteUsersToTeams", "api.team.invite_members_to_team_and_channels.invalid_body.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
memberInvite := &model.MemberInvite{}
if err := json.Unmarshal(bf, memberInvite); err != nil {
c.Err = model.NewAppError("Api4.inviteUsersToTeams", "api.team.invite_members_to_team_and_channels.invalid_body_parsing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
emailList := memberInvite.Emails
if len(emailList) == 0 {
c.SetInvalidParam("user_email")
return
}
for i := range emailList {
emailList[i] = strings.ToLower(emailList[i])
}
auditRec := c.MakeAuditRecord("inviteUsersToTeam", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "member_invite", memberInvite)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
auditRec.AddMeta("count", len(emailList))
auditRec.AddMeta("emails", emailList)
if len(memberInvite.ChannelIds) > 0 {
// Check if the user sending the invitation has access to the channels where the invitation is being sent
memberInvite.ChannelIds = c.App.ValidateUserPermissionsOnChannels(c.AppContext, c.AppContext.Session().UserId, memberInvite.ChannelIds)
auditRec.AddMeta("channel_count", len(memberInvite.ChannelIds))
auditRec.AddMeta("channels", memberInvite.ChannelIds)
}
if graceful {
var invitesWithError []*model.EmailInviteWithError
var appErr *model.AppError
if emailList != nil {
invitesWithError, appErr = c.App.InviteNewUsersToTeamGracefully(memberInvite, c.Params.TeamId, c.AppContext.Session().UserId, "")
}
if invitesWithError != nil {
errList := make([]string, 0, len(invitesWithError))
for _, inv := range invitesWithError {
if inv.Error != nil {
errList = append(errList, model.EmailInviteWithErrorToString(inv))
}
}
auditRec.AddMeta("errors", errList)
}
if appErr != nil {
c.Err = appErr
return
}
// we get the emailList after it has finished checks like the emails over the list
scheduledAt := model.GetMillis()
jobData := map[string]string{
"emailList": model.ArrayToJSON(emailList),
"teamID": c.Params.TeamId,
"senderID": c.AppContext.Session().UserId,
"scheduledAt": strconv.FormatInt(scheduledAt, 10),
}
if len(memberInvite.ChannelIds) > 0 {
jobData["channelList"] = model.ArrayToJSON(memberInvite.ChannelIds)
}
// we then manually schedule the job to send another invite after 48 hours
_, appErr = c.App.Srv().Jobs.CreateJob(model.JobTypeResendInvitationEmail, jobData)
if appErr != nil {
c.Err = model.NewAppError("Api4.inviteUsersToTeam", appErr.Id, nil, appErr.Error(), appErr.StatusCode)
return
}
// in graceful mode we return both the successful ones and the failed ones
js, err := json.Marshal(invitesWithError)
if err != nil {
c.Err = model.NewAppError("inviteUsersToTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
} else {
appErr := c.App.InviteNewUsersToTeam(emailList, c.Params.TeamId, c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
auditRec.Success()
}
func inviteGuestsToChannels(c *Context, w http.ResponseWriter, r *http.Request) {
graceful := r.URL.Query().Get("graceful") != ""
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("Api4.InviteGuestsToChannels", "api.team.invite_guests_to_channels.license.error", nil, "", http.StatusNotImplemented)
return
}
if !*c.App.Config().GuestAccountsSettings.Enable {
c.Err = model.NewAppError("Api4.InviteGuestsToChannels", "api.team.invite_guests_to_channels.disabled.error", nil, "", http.StatusNotImplemented)
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("inviteGuestsToChannels", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionInviteGuest) {
c.SetPermissionError(model.PermissionInviteGuest)
return
}
guestEnabled := c.App.Channels().License() != nil && *c.App.Channels().License().Features.GuestAccounts
if !guestEnabled {
c.Err = model.NewAppError("Api4.InviteGuestsToChannels", "api.team.invite_guests_to_channels.disabled.error", nil, "", http.StatusForbidden)
return
}
var guestsInvite model.GuestsInvite
if err := json.NewDecoder(r.Body).Decode(&guestsInvite); err != nil {
c.Err = model.NewAppError("Api4.inviteGuestsToChannels", "api.team.invite_guests_to_channels.invalid_body.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
audit.AddEventParameterAuditable(auditRec, "guests_invite", &guestsInvite)
for i, email := range guestsInvite.Emails {
guestsInvite.Emails[i] = strings.ToLower(email)
}
if appErr := guestsInvite.IsValid(); appErr != nil {
c.Err = appErr
return
}
auditRec.AddMeta("email_count", len(guestsInvite.Emails))
auditRec.AddMeta("emails", guestsInvite.Emails)
auditRec.AddMeta("channel_count", len(guestsInvite.Channels))
auditRec.AddMeta("channels", guestsInvite.Channels)
// Check if the user sending the invitation has access to the channels where the invitation is being sent
guestsInvite.Channels = c.App.ValidateUserPermissionsOnChannels(c.AppContext, c.AppContext.Session().UserId, guestsInvite.Channels)
if graceful {
var invitesWithError []*model.EmailInviteWithError
var appErr *model.AppError
if guestsInvite.Emails != nil {
invitesWithError, appErr = c.App.InviteGuestsToChannelsGracefully(c.Params.TeamId, &guestsInvite, c.AppContext.Session().UserId)
}
if appErr != nil {
errList := make([]string, 0, len(invitesWithError))
for _, inv := range invitesWithError {
errList = append(errList, model.EmailInviteWithErrorToString(inv))
}
auditRec.AddMeta("errors", errList)
c.Err = appErr
return
}
// in graceful mode we return both the successful ones and the failed ones
js, err := json.Marshal(invitesWithError)
if err != nil {
c.Err = model.NewAppError("inviteGuestsToChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
} else {
appErr := c.App.InviteGuestsToChannels(c.Params.TeamId, &guestsInvite, c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
auditRec.Success()
}
func getInviteInfo(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireInviteId()
if c.Err != nil {
return
}
team, appErr := c.App.GetTeamByInviteId(c.Params.InviteId)
if appErr != nil {
c.Err = appErr
return
}
if team.Type != model.TeamOpen {
c.Err = model.NewAppError("getInviteInfo", "api.team.get_invite_info.not_open_team", nil, "id="+c.Params.InviteId, http.StatusForbidden)
return
}
result := struct {
DisplayName string `json:"display_name"`
Description string `json:"description"`
Name string `json:"name"`
ID string `json:"id"`
}{
DisplayName: team.DisplayName,
Description: team.Description,
Name: team.Name,
ID: team.Id,
}
err := json.NewEncoder(w).Encode(result)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func invalidateAllEmailInvites(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionInvalidateEmailInvite) {
c.SetPermissionError(model.PermissionInvalidateEmailInvite)
return
}
auditRec := c.MakeAuditRecord("invalidateAllEmailInvites", audit.Fail)
defer c.LogAuditRec(auditRec)
if err := c.App.InvalidateAllEmailInvites(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getTeamIcon(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
team, err := c.App.GetTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) &&
(team.Type != model.TeamOpen || !team.AllowOpenInvite) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
etag := strconv.FormatInt(team.LastTeamIconUpdate, 10)
if c.HandleEtag(etag, "Get Team Icon", w, r) {
return
}
img, err := c.App.GetTeamIcon(team)
if err != nil {
c.Err = err
return
}
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%v, private", model.DayInSeconds)) // 24 hrs
w.Header().Set(model.HeaderEtagServer, etag)
w.Write(img)
}
func setTeamIcon(c *Context, w http.ResponseWriter, r *http.Request) {
defer io.Copy(io.Discard, r.Body)
c.RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("setTeamIcon", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeam) {
c.SetPermissionError(model.PermissionManageTeam)
return
}
if r.ContentLength > *c.App.Config().FileSettings.MaxFileSize {
c.Err = model.NewAppError("setTeamIcon", "api.team.set_team_icon.too_large.app_error", nil, "", http.StatusBadRequest)
return
}
if err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize); err != nil {
c.Err = model.NewAppError("setTeamIcon", "api.team.set_team_icon.parse.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
m := r.MultipartForm
imageArray, ok := m.File["image"]
if !ok {
c.Err = model.NewAppError("setTeamIcon", "api.team.set_team_icon.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(imageArray) <= 0 {
c.Err = model.NewAppError("setTeamIcon", "api.team.set_team_icon.array.app_error", nil, "", http.StatusBadRequest)
return
}
imageData := imageArray[0]
if err := c.App.SetTeamIcon(c.Params.TeamId, imageData); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
func removeTeamIcon(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("removeTeamIcon", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeam) {
c.SetPermissionError(model.PermissionManageTeam)
return
}
if err := c.App.RemoveTeamIcon(c.Params.TeamId); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
func updateTeamScheme(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
var p model.SchemeIDPatch
if jsonErr := json.NewDecoder(r.Body).Decode(&p); jsonErr != nil {
c.SetInvalidParamWithErr("scheme_id", jsonErr)
return
}
schemeID := p.SchemeID
if p.SchemeID == nil || (!model.IsValidId(*p.SchemeID) && *p.SchemeID != "") {
c.SetInvalidParam("scheme_id")
return
}
auditRec := c.MakeAuditRecord("updateTeamScheme", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "scheme_id_patch", &p)
defer c.LogAuditRec(auditRec)
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("Api4.UpdateTeamScheme", "api.team.update_team_scheme.license.error", nil, "", http.StatusNotImplemented)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementPermissions) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementPermissions)
return
}
if *schemeID != "" {
scheme, err := c.App.GetScheme(*schemeID)
if err != nil {
c.Err = err
return
}
audit.AddEventParameterAuditable(auditRec, "scheme", scheme)
if scheme.Scope != model.SchemeScopeTeam {
c.Err = model.NewAppError("Api4.UpdateTeamScheme", "api.team.update_team_scheme.scheme_scope.error", nil, "", http.StatusBadRequest)
return
}
}
team, err := c.App.GetTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
audit.AddEventParameterAuditable(auditRec, "team", team)
team.SchemeId = schemeID
team, err = c.App.UpdateTeamScheme(team)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(team)
auditRec.AddEventObjectType("team")
auditRec.Success()
ReturnStatusOK(w)
}
func teamMembersMinusGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
groupIDsParam := groupIDsQueryParamRegex.ReplaceAllString(c.Params.GroupIDs, "")
if len(groupIDsParam) < 26 {
c.SetInvalidParam("group_ids")
return
}
groupIDs := []string{}
for _, gid := range strings.Split(c.Params.GroupIDs, ",") {
if !model.IsValidId(gid) {
c.SetInvalidParam("group_ids")
return
}
groupIDs = append(groupIDs, gid)
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups)
return
}
users, totalCount, appErr := c.App.TeamMembersMinusGroupMembers(
c.Params.TeamId,
groupIDs,
c.Params.Page,
c.Params.PerPage,
)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(&model.UsersWithGroupsAndCount{
Users: users,
Count: totalCount,
})
if err != nil {
c.Err = model.NewAppError("Api4.teamMembersMinusGroupMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/email"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitTeamLocal() {
api.BaseRoutes.Teams.Handle("", api.APILocal(localCreateTeam)).Methods("POST")
api.BaseRoutes.Teams.Handle("", api.APILocal(getAllTeams)).Methods("GET")
api.BaseRoutes.Teams.Handle("/search", api.APILocal(searchTeams)).Methods("POST")
api.BaseRoutes.Team.Handle("", api.APILocal(getTeam)).Methods("GET")
api.BaseRoutes.Team.Handle("", api.APILocal(updateTeam)).Methods("PUT")
api.BaseRoutes.Team.Handle("", api.APILocal(localDeleteTeam)).Methods("DELETE")
api.BaseRoutes.Team.Handle("/invite/email", api.APILocal(localInviteUsersToTeam)).Methods("POST")
api.BaseRoutes.Team.Handle("/patch", api.APILocal(patchTeam)).Methods("PUT")
api.BaseRoutes.Team.Handle("/privacy", api.APILocal(updateTeamPrivacy)).Methods("PUT")
api.BaseRoutes.Team.Handle("/restore", api.APILocal(restoreTeam)).Methods("POST")
api.BaseRoutes.TeamByName.Handle("", api.APILocal(getTeamByName)).Methods("GET")
api.BaseRoutes.TeamMembers.Handle("", api.APILocal(addTeamMember)).Methods("POST")
api.BaseRoutes.TeamMember.Handle("", api.APILocal(removeTeamMember)).Methods("DELETE")
}
func localDeleteTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("localDeleteTeam", audit.Fail)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
defer c.LogAuditRec(auditRec)
if team, err := c.App.GetTeam(c.Params.TeamId); err == nil {
auditRec.AddEventPriorState(team)
auditRec.AddEventObjectType("team")
}
var err *model.AppError
if c.Params.Permanent {
err = c.App.PermanentDeleteTeamId(c.AppContext, c.Params.TeamId)
} else {
err = c.App.SoftDeleteTeam(c.Params.TeamId)
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func localInviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !*c.App.Config().ServiceSettings.EnableEmailInvitations {
c.Err = model.NewAppError("localInviteUsersToTeam", "api.team.invite_members.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
bf, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.inviteUsersToTeams", "api.team.invite_members_to_team_and_channels.invalid_body.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
memberInvite := &model.MemberInvite{}
err = json.Unmarshal(bf, memberInvite)
if err != nil {
c.Err = model.NewAppError("Api4.inviteUsersToTeams", "api.team.invite_members_to_team_and_channels.invalid_body_parsing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
emailList := memberInvite.Emails
if len(emailList) == 0 {
c.SetInvalidParam("user_email")
return
}
for i := range emailList {
email := strings.ToLower(emailList[i])
if !model.IsValidEmail(email) {
c.Err = model.NewAppError("localInviteUsersToTeam", "api.team.invite_members.invalid_email.app_error", map[string]any{"Address": email}, "", http.StatusBadRequest)
return
}
emailList[i] = email
}
auditRec := c.MakeAuditRecord("localInviteUsersToTeam", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "member_invite", memberInvite)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
auditRec.AddMeta("count", len(emailList))
auditRec.AddMeta("emails", emailList)
if len(memberInvite.ChannelIds) > 0 {
auditRec.AddMeta("channel_count", len(memberInvite.ChannelIds))
auditRec.AddMeta("channels", memberInvite.ChannelIds)
}
team, err := c.App.Srv().Store().Team().Get(c.Params.TeamId)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
c.Err = model.NewAppError("localInviteUsersToTeam", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
c.Err = model.NewAppError("localInviteUsersToTeam", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return
}
allowedDomains := []string{team.AllowedDomains, *c.App.Config().TeamSettings.RestrictCreationToDomains}
var channels []*model.Channel
if len(memberInvite.ChannelIds) > 0 {
channels, err = c.App.Srv().Store().Channel().GetChannelsByIds(memberInvite.ChannelIds, false)
if err != nil {
c.Err = model.NewAppError("prepareLocalInviteNewUsersToTeam", "app.channel.get_channels_by_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if r.URL.Query().Get("graceful") != "" {
var invitesWithErrors []*model.EmailInviteWithError
var goodEmails, errList []string
for _, email := range emailList {
invite := &model.EmailInviteWithError{
Email: email,
Error: nil,
}
if !isEmailAddressAllowed(email, allowedDomains) {
invite.Error = model.NewAppError("localInviteUsersToTeam", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": email}, "", http.StatusBadRequest)
errList = append(errList, model.EmailInviteWithErrorToString(invite))
} else {
goodEmails = append(goodEmails, email)
}
invitesWithErrors = append(invitesWithErrors, invite)
}
auditRec.AddMeta("errors", errList)
if len(goodEmails) > 0 {
var invitesWithErrors2 []*model.EmailInviteWithError
if len(channels) > 0 {
invitesWithErrors2, err = c.App.Srv().EmailService.SendInviteEmailsToTeamAndChannels(team, channels, "Administrator", "mmctl "+model.NewId(), nil, goodEmails, *c.App.Config().ServiceSettings.SiteURL, nil, memberInvite.Message, true, true, false)
invitesWithErrors = append(invitesWithErrors, invitesWithErrors2...)
} else {
err = c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), goodEmails, *c.App.Config().ServiceSettings.SiteURL, nil, false, true, false)
}
if err != nil {
switch {
case errors.Is(err, email.NoRateLimiterError):
c.Err = model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("team_id=%s", team.Id), http.StatusInternalServerError).Wrap(err)
case errors.Is(err, email.SetupRateLimiterError):
c.Err = model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("team_id=%s, error=%v", team.Id, err), http.StatusInternalServerError).Wrap(err)
default:
c.Err = model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("team_id=%s, error=%v", team.Id, err), http.StatusRequestEntityTooLarge).Wrap(err)
}
return
}
}
// in graceful mode we return both the successful ones and the failed ones
js, err := json.Marshal(invitesWithErrors)
if err != nil {
c.Err = model.NewAppError("localInviteUsersToTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
} else {
var invalidEmailList []string
for _, email := range emailList {
if !isEmailAddressAllowed(email, allowedDomains) {
invalidEmailList = append(invalidEmailList, email)
}
}
if len(invalidEmailList) > 0 {
s := strings.Join(invalidEmailList, ", ")
c.Err = model.NewAppError("localInviteUsersToTeam", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": s}, "", http.StatusBadRequest)
return
}
err := c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), emailList, *c.App.Config().ServiceSettings.SiteURL, nil, false, true, false)
if err != nil {
switch {
case errors.Is(err, email.NoRateLimiterError):
c.Err = model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("team_id=%s", team.Id), http.StatusInternalServerError).Wrap(err)
case errors.Is(err, email.SetupRateLimiterError):
c.Err = model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("team_id=%s, error=%v", team.Id, err), http.StatusInternalServerError).Wrap(err)
default:
c.Err = model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("team_id=%s, error=%v", team.Id, err), http.StatusRequestEntityTooLarge).Wrap(err)
}
return
}
ReturnStatusOK(w)
}
auditRec.Success()
}
func isEmailAddressAllowed(email string, allowedDomains []string) bool {
for _, restriction := range allowedDomains {
domains := normalizeDomains(restriction)
if len(domains) <= 0 {
continue
}
matched := false
for _, d := range domains {
if strings.HasSuffix(email, "@"+d) {
matched = true
break
}
}
if !matched {
return false
}
}
return true
}
func normalizeDomains(domains string) []string {
// commas and @ signs are optional
// can be in the form of "@corp.mattermost.com, mattermost.com mattermost.org" -> corp.mattermost.com mattermost.com mattermost.org
return strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(domains, "@", " ", -1), ",", " ", -1))))
}
func localCreateTeam(c *Context, w http.ResponseWriter, r *http.Request) {
var team model.Team
if jsonErr := json.NewDecoder(r.Body).Decode(&team); jsonErr != nil {
c.SetInvalidParamWithErr("team", jsonErr)
return
}
team.Email = strings.ToLower(team.Email)
auditRec := c.MakeAuditRecord("localCreateTeam", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "team", &team)
rteam, err := c.App.CreateTeam(c.AppContext, &team)
if err != nil {
c.Err = err
return
}
// Don't sanitize the team here since the user will be a team admin and their session won't reflect that yet
auditRec.AddEventResultState(rteam)
auditRec.AddEventObjectType("type")
auditRec.Success()
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rteam); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitTermsOfService() {
api.BaseRoutes.TermsOfService.Handle("", api.APISessionRequired(getLatestTermsOfService)).Methods("GET")
api.BaseRoutes.TermsOfService.Handle("", api.APISessionRequired(createTermsOfService)).Methods("POST")
}
func getLatestTermsOfService(c *Context, w http.ResponseWriter, r *http.Request) {
termsOfService, err := c.App.GetLatestTermsOfService()
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(termsOfService); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func createTermsOfService(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if license := c.App.Channels().License(); license == nil || !*license.Features.CustomTermsOfService {
c.Err = model.NewAppError("createTermsOfService", "api.create_terms_of_service.custom_terms_of_service_disabled.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("createTermsOfService", audit.Fail)
defer c.LogAuditRec(auditRec)
props := model.MapFromJSON(r.Body)
text := props["text"]
userId := c.AppContext.Session().UserId
if text == "" {
c.Err = model.NewAppError("Config.IsValid", "api.create_terms_of_service.empty_text.app_error", nil, "", http.StatusBadRequest)
return
}
oldTermsOfService, err := c.App.GetLatestTermsOfService()
if err != nil && err.Id != app.ErrorTermsOfServiceNoRowsFound {
c.Err = err
return
}
if oldTermsOfService == nil || oldTermsOfService.Text != text {
termsOfService, err := c.App.CreateTermsOfService(text, userId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(termsOfService); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
} else {
if err := json.NewEncoder(w).Encode(oldTermsOfService); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
auditRec.Success()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"errors"
"io"
"mime/multipart"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitUpload() {
api.BaseRoutes.Uploads.Handle("", api.APISessionRequired(createUpload)).Methods("POST")
api.BaseRoutes.Upload.Handle("", api.APISessionRequired(getUpload)).Methods("GET")
api.BaseRoutes.Upload.Handle("", api.APISessionRequired(uploadData)).Methods("POST")
}
func createUpload(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().FileSettings.EnableFileAttachments {
c.Err = model.NewAppError("createUpload",
"api.file.attachments.disabled.app_error",
nil, "", http.StatusNotImplemented)
return
}
var us model.UploadSession
if jsonErr := json.NewDecoder(r.Body).Decode(&us); jsonErr != nil {
c.SetInvalidParamWithErr("upload", jsonErr)
return
}
// these are not supported for client uploads; shared channels only.
us.RemoteId = ""
us.ReqFileId = ""
auditRec := c.MakeAuditRecord("createUpload", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "upload", &us)
if us.Type == model.UploadTypeImport {
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if c.App.Srv().License().IsCloud() {
c.Err = model.NewAppError("createUpload", "api.file.cloud_upload.app_error", nil, "", http.StatusBadRequest)
return
}
} else {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), us.ChannelId, model.PermissionUploadFile) {
c.SetPermissionError(model.PermissionUploadFile)
return
}
us.Type = model.UploadTypeAttachment
}
us.Id = model.NewId()
if c.AppContext.Session().UserId != "" {
us.UserId = c.AppContext.Session().UserId
}
if us.FileSize > *c.App.Config().FileSettings.MaxFileSize {
c.Err = model.NewAppError("createUpload", "api.upload.create.upload_too_large.app_error",
map[string]any{"channelId": us.ChannelId}, "", http.StatusRequestEntityTooLarge)
return
}
rus, err := c.App.CreateUploadSession(c.AppContext, &us)
if err != nil {
c.Err = err
return
}
auditRec.Success()
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rus); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUpload(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUploadId()
if c.Err != nil {
return
}
us, err := c.App.GetUploadSession(c.AppContext, c.Params.UploadId)
if err != nil {
c.Err = err
return
}
if us.UserId != c.AppContext.Session().UserId && !c.IsSystemAdmin() {
c.Err = model.NewAppError("getUpload", "api.upload.get_upload.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
if err := json.NewEncoder(w).Encode(us); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func uploadData(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().FileSettings.EnableFileAttachments {
c.Err = model.NewAppError("uploadData", "api.file.attachments.disabled.app_error",
nil, "", http.StatusNotImplemented)
return
}
c.RequireUploadId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("uploadData", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "upload_id", c.Params.UploadId)
c.AppContext.SetContext(app.WithMaster(c.AppContext.Context()))
us, err := c.App.GetUploadSession(c.AppContext, c.Params.UploadId)
if err != nil {
c.Err = err
return
}
if us.Type == model.UploadTypeImport {
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if c.App.Srv().License().IsCloud() {
c.Err = model.NewAppError("UploadData", "api.file.cloud_upload.app_error", nil, "", http.StatusBadRequest)
return
}
} else {
if us.UserId != c.AppContext.Session().UserId || !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), us.ChannelId, model.PermissionUploadFile) {
c.SetPermissionError(model.PermissionUploadFile)
return
}
}
info, err := doUploadData(c, us, r)
if err != nil {
c.Err = err
return
}
auditRec.Success()
if info == nil {
w.WriteHeader(http.StatusNoContent)
return
}
if err := json.NewEncoder(w).Encode(info); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func doUploadData(c *Context, us *model.UploadSession, r *http.Request) (*model.FileInfo, *model.AppError) {
boundary, parseErr := parseMultipartRequestHeader(r)
if parseErr != nil && !errors.Is(parseErr, http.ErrNotMultipart) {
return nil, model.NewAppError("uploadData", "api.upload.upload_data.invalid_content_type",
nil, parseErr.Error(), http.StatusBadRequest)
}
var rd io.Reader
if boundary != "" {
mr := multipart.NewReader(r.Body, boundary)
p, partErr := mr.NextPart()
if partErr != nil {
return nil, model.NewAppError("uploadData", "api.upload.upload_data.multipart_error",
nil, partErr.Error(), http.StatusBadRequest)
}
rd = p
} else {
if r.ContentLength > (us.FileSize - us.FileOffset) {
return nil, model.NewAppError("uploadData", "api.upload.upload_data.invalid_content_length",
nil, "", http.StatusBadRequest)
}
rd = r.Body
}
return c.App.UploadData(c.AppContext, us, rd)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitUploadLocal() {
api.BaseRoutes.Uploads.Handle("", api.APILocal(createUpload)).Methods("POST")
api.BaseRoutes.Upload.Handle("", api.APILocal(getUpload)).Methods("GET")
api.BaseRoutes.Upload.Handle("", api.APILocal(uploadData)).Methods("POST")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
func (api *API) InitUsage() {
// GET /api/v4/usage/posts
api.BaseRoutes.Usage.Handle("/posts", api.APISessionRequired(getPostsUsage)).Methods("GET")
// GET /api/v4/usage/storage
api.BaseRoutes.Usage.Handle("/storage", api.APISessionRequired(getStorageUsage)).Methods("GET")
// GET /api/v4/usage/teams
api.BaseRoutes.Usage.Handle("/teams", api.APISessionRequired(getTeamsUsage)).Methods("GET")
}
func getPostsUsage(c *Context, w http.ResponseWriter, r *http.Request) {
count, appErr := c.App.GetPostsUsage()
if appErr != nil {
c.Err = model.NewAppError("Api4.getPostsUsage", "app.post.analytics_posts_count.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
return
}
json, err := json.Marshal(&model.PostsUsage{Count: count})
if err != nil {
c.Err = model.NewAppError("Api4.getPostsUsage", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func getStorageUsage(c *Context, w http.ResponseWriter, r *http.Request) {
usage, appErr := c.App.GetStorageUsage()
if appErr != nil {
c.Err = model.NewAppError("Api4.getStorageUsage", "app.usage.get_storage_usage.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
return
}
usage = utils.RoundOffToZeroesResolution(float64(usage), 8)
json, err := json.Marshal(&model.StorageUsage{Bytes: usage})
if err != nil {
c.Err = model.NewAppError("Api4.getStorageUsage", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func getTeamsUsage(c *Context, w http.ResponseWriter, r *http.Request) {
teamsUsage, appErr := c.App.GetTeamsUsage()
if appErr != nil {
c.Err = model.NewAppError("Api4.getTeamsUsage", "app.teams.analytics_teams_count.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
return
}
if teamsUsage == nil {
c.Err = model.NewAppError("Api4.getTeamsUsage", "app.teams.analytics_teams_count.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
}
json, err := json.Marshal(teamsUsage)
if err != nil {
c.Err = model.NewAppError("Api4.getTeamsUsage", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitUser() {
api.BaseRoutes.Users.Handle("", api.APIHandler(createUser)).Methods("POST")
api.BaseRoutes.Users.Handle("", api.APISessionRequired(getUsers)).Methods("GET")
api.BaseRoutes.Users.Handle("/ids", api.APISessionRequired(getUsersByIds)).Methods("POST")
api.BaseRoutes.Users.Handle("/usernames", api.APISessionRequired(getUsersByNames)).Methods("POST")
api.BaseRoutes.Users.Handle("/known", api.APISessionRequired(getKnownUsers)).Methods("GET")
api.BaseRoutes.Users.Handle("/search", api.APISessionRequiredDisableWhenBusy(searchUsers)).Methods("POST")
api.BaseRoutes.Users.Handle("/autocomplete", api.APISessionRequired(autocompleteUsers)).Methods("GET")
api.BaseRoutes.Users.Handle("/stats", api.APISessionRequired(getTotalUsersStats)).Methods("GET")
api.BaseRoutes.Users.Handle("/stats/filtered", api.APISessionRequired(getFilteredUsersStats)).Methods("GET")
api.BaseRoutes.Users.Handle("/group_channels", api.APISessionRequired(getUsersByGroupChannelIds)).Methods("POST")
api.BaseRoutes.User.Handle("", api.APISessionRequired(getUser)).Methods("GET")
api.BaseRoutes.User.Handle("/image/default", api.APISessionRequiredTrustRequester(getDefaultProfileImage)).Methods("GET")
api.BaseRoutes.User.Handle("/image", api.APISessionRequiredTrustRequester(getProfileImage)).Methods("GET")
api.BaseRoutes.User.Handle("/image", api.APISessionRequired(setProfileImage)).Methods("POST")
api.BaseRoutes.User.Handle("/image", api.APISessionRequired(setDefaultProfileImage)).Methods("DELETE")
api.BaseRoutes.User.Handle("", api.APISessionRequired(updateUser)).Methods("PUT")
api.BaseRoutes.User.Handle("/patch", api.APISessionRequired(patchUser)).Methods("PUT")
api.BaseRoutes.User.Handle("", api.APISessionRequired(deleteUser)).Methods("DELETE")
api.BaseRoutes.User.Handle("/roles", api.APISessionRequired(updateUserRoles)).Methods("PUT")
api.BaseRoutes.User.Handle("/active", api.APISessionRequired(updateUserActive)).Methods("PUT")
api.BaseRoutes.User.Handle("/password", api.APISessionRequired(updatePassword)).Methods("PUT")
api.BaseRoutes.User.Handle("/promote", api.APISessionRequired(promoteGuestToUser)).Methods("POST")
api.BaseRoutes.User.Handle("/demote", api.APISessionRequired(demoteUserToGuest)).Methods("POST")
api.BaseRoutes.User.Handle("/convert_to_bot", api.APISessionRequired(convertUserToBot)).Methods("POST")
api.BaseRoutes.Users.Handle("/password/reset", api.APIHandler(resetPassword)).Methods("POST")
api.BaseRoutes.Users.Handle("/password/reset/send", api.APIHandler(sendPasswordReset)).Methods("POST")
api.BaseRoutes.Users.Handle("/email/verify", api.APIHandler(verifyUserEmail)).Methods("POST")
api.BaseRoutes.Users.Handle("/email/verify/send", api.APIHandler(sendVerificationEmail)).Methods("POST")
api.BaseRoutes.User.Handle("/email/verify/member", api.APISessionRequired(verifyUserEmailWithoutToken)).Methods("POST")
api.BaseRoutes.User.Handle("/terms_of_service", api.APISessionRequired(saveUserTermsOfService)).Methods("POST")
api.BaseRoutes.User.Handle("/terms_of_service", api.APISessionRequired(getUserTermsOfService)).Methods("GET")
api.BaseRoutes.User.Handle("/auth", api.APISessionRequiredTrustRequester(updateUserAuth)).Methods("PUT")
api.BaseRoutes.User.Handle("/mfa", api.APISessionRequiredMfa(updateUserMfa)).Methods("PUT")
api.BaseRoutes.User.Handle("/mfa/generate", api.APISessionRequiredMfa(generateMfaSecret)).Methods("POST")
api.BaseRoutes.Users.Handle("/login", api.APIHandler(login)).Methods("POST")
api.BaseRoutes.Users.Handle("/login/switch", api.APIHandler(switchAccountType)).Methods("POST")
api.BaseRoutes.Users.Handle("/login/cws", api.APIHandlerTrustRequester(loginCWS)).Methods("POST")
api.BaseRoutes.Users.Handle("/logout", api.APIHandler(logout)).Methods("POST")
api.BaseRoutes.UserByUsername.Handle("", api.APISessionRequired(getUserByUsername)).Methods("GET")
api.BaseRoutes.UserByEmail.Handle("", api.APISessionRequired(getUserByEmail)).Methods("GET")
api.BaseRoutes.User.Handle("/sessions", api.APISessionRequired(getSessions)).Methods("GET")
api.BaseRoutes.User.Handle("/sessions/revoke", api.APISessionRequired(revokeSession)).Methods("POST")
api.BaseRoutes.User.Handle("/sessions/revoke/all", api.APISessionRequired(revokeAllSessionsForUser)).Methods("POST")
api.BaseRoutes.Users.Handle("/sessions/revoke/all", api.APISessionRequired(revokeAllSessionsAllUsers)).Methods("POST")
api.BaseRoutes.Users.Handle("/sessions/device", api.APISessionRequired(attachDeviceId)).Methods("PUT")
api.BaseRoutes.User.Handle("/audits", api.APISessionRequired(getUserAudits)).Methods("GET")
api.BaseRoutes.User.Handle("/tokens", api.APISessionRequired(createUserAccessToken)).Methods("POST")
api.BaseRoutes.User.Handle("/tokens", api.APISessionRequired(getUserAccessTokensForUser)).Methods("GET")
api.BaseRoutes.Users.Handle("/tokens", api.APISessionRequired(getUserAccessTokens)).Methods("GET")
api.BaseRoutes.Users.Handle("/tokens/search", api.APISessionRequired(searchUserAccessTokens)).Methods("POST")
api.BaseRoutes.Users.Handle("/tokens/{token_id:[A-Za-z0-9]+}", api.APISessionRequired(getUserAccessToken)).Methods("GET")
api.BaseRoutes.Users.Handle("/tokens/revoke", api.APISessionRequired(revokeUserAccessToken)).Methods("POST")
api.BaseRoutes.Users.Handle("/tokens/disable", api.APISessionRequired(disableUserAccessToken)).Methods("POST")
api.BaseRoutes.Users.Handle("/tokens/enable", api.APISessionRequired(enableUserAccessToken)).Methods("POST")
api.BaseRoutes.User.Handle("/typing", api.APISessionRequiredDisableWhenBusy(publishUserTyping)).Methods("POST")
api.BaseRoutes.Users.Handle("/migrate_auth/ldap", api.APISessionRequired(migrateAuthToLDAP)).Methods("POST")
api.BaseRoutes.Users.Handle("/migrate_auth/saml", api.APISessionRequired(migrateAuthToSaml)).Methods("POST")
api.BaseRoutes.User.Handle("/uploads", api.APISessionRequired(getUploadsForUser)).Methods("GET")
api.BaseRoutes.User.Handle("/channel_members", api.APISessionRequired(getChannelMembersForUser)).Methods("GET")
api.BaseRoutes.User.Handle("/recent_searches", api.APISessionRequiredDisableWhenBusy(getRecentSearches)).Methods("GET")
api.BaseRoutes.Users.Handle("/invalid_emails", api.APISessionRequired(getUsersWithInvalidEmails)).Methods("GET")
api.BaseRoutes.UserThreads.Handle("", api.APISessionRequired(getThreadsForUser)).Methods("GET")
api.BaseRoutes.UserThreads.Handle("/read", api.APISessionRequired(updateReadStateAllThreadsByUser)).Methods("PUT")
api.BaseRoutes.UserThread.Handle("", api.APISessionRequired(getThreadForUser)).Methods("GET")
api.BaseRoutes.UserThread.Handle("/following", api.APISessionRequired(followThreadByUser)).Methods("PUT")
api.BaseRoutes.UserThread.Handle("/following", api.APISessionRequired(unfollowThreadByUser)).Methods("DELETE")
api.BaseRoutes.UserThread.Handle("/read/{timestamp:[0-9]+}", api.APISessionRequired(updateReadStateThreadByUser)).Methods("PUT")
api.BaseRoutes.UserThread.Handle("/set_unread/{post_id:[A-Za-z0-9]+}", api.APISessionRequired(setUnreadThreadByPostId)).Methods("POST")
api.BaseRoutes.Users.Handle("/notify-admin", api.APISessionRequired(handleNotifyAdmin)).Methods("POST")
api.BaseRoutes.Users.Handle("/trigger-notify-admin-posts", api.APISessionRequired(handleTriggerNotifyAdminPosts)).Methods("POST")
}
func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
var user model.User
if jsonErr := json.NewDecoder(r.Body).Decode(&user); jsonErr != nil {
c.SetInvalidParamWithErr("user", jsonErr)
return
}
user.SanitizeInput(c.IsSystemAdmin())
tokenId := r.URL.Query().Get("t")
inviteId := r.URL.Query().Get("iid")
redirect := r.URL.Query().Get("r")
auditRec := c.MakeAuditRecord("createUser", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "invite_id", inviteId)
audit.AddEventParameter(auditRec, "redirect", redirect)
audit.AddEventParameterAuditable(auditRec, "user", &user)
// No permission check required
var ruser *model.User
var err *model.AppError
if tokenId != "" {
token, appErr := c.App.GetTokenById(tokenId)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddMeta("token_type", token.Type)
if token.Type == app.TokenTypeGuestInvitation {
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("CreateUserWithToken", "api.user.create_user.guest_accounts.license.app_error", nil, "", http.StatusBadRequest)
return
}
if !*c.App.Config().GuestAccountsSettings.Enable {
c.Err = model.NewAppError("CreateUserWithToken", "api.user.create_user.guest_accounts.disabled.app_error", nil, "", http.StatusBadRequest)
return
}
}
ruser, err = c.App.CreateUserWithToken(c.AppContext, &user, token)
} else if inviteId != "" {
ruser, err = c.App.CreateUserWithInviteId(c.AppContext, &user, inviteId, redirect)
} else if c.IsSystemAdmin() {
ruser, err = c.App.CreateUserAsAdmin(c.AppContext, &user, redirect)
auditRec.AddMeta("admin", true)
} else {
ruser, err = c.App.CreateUserFromSignup(c.AppContext, &user, redirect)
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(ruser)
auditRec.AddEventObjectType("user")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(ruser); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, c.Params.UserId)
if err != nil {
c.SetPermissionError(model.PermissionViewMembers)
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
if c.IsSystemAdmin() || c.AppContext.Session().UserId == user.Id {
userTermsOfService, err := c.App.GetUserTermsOfService(user.Id)
if err != nil && err.StatusCode != http.StatusNotFound {
c.Err = err
return
}
if userTermsOfService != nil {
user.TermsOfServiceId = userTermsOfService.TermsOfServiceId
user.TermsOfServiceCreateAt = userTermsOfService.CreateAt
}
}
etag := user.Etag(*c.App.Config().PrivacySettings.ShowFullName, *c.App.Config().PrivacySettings.ShowEmailAddress)
if c.HandleEtag(etag, "Get User", w, r) {
return
}
if c.AppContext.Session().UserId == user.Id {
user.Sanitize(map[string]bool{})
} else {
c.App.SanitizeProfile(user, c.IsSystemAdmin())
}
c.App.Srv().Platform().UpdateLastActivityAtIfNeeded(*c.AppContext.Session())
w.Header().Set(model.HeaderEtagServer, etag)
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUserByUsername(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUsername()
if c.Err != nil {
return
}
user, err := c.App.GetUserByUsername(c.Params.Username)
if err != nil {
restrictions, err2 := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if err2 != nil {
c.Err = err2
return
}
if restrictions != nil {
c.SetPermissionError(model.PermissionViewMembers)
return
}
c.Err = err
return
}
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, user.Id)
if err != nil {
c.Err = err
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
if c.IsSystemAdmin() || c.AppContext.Session().UserId == user.Id {
userTermsOfService, err := c.App.GetUserTermsOfService(user.Id)
if err != nil && err.StatusCode != http.StatusNotFound {
c.Err = err
return
}
if userTermsOfService != nil {
user.TermsOfServiceId = userTermsOfService.TermsOfServiceId
user.TermsOfServiceCreateAt = userTermsOfService.CreateAt
}
}
etag := user.Etag(*c.App.Config().PrivacySettings.ShowFullName, *c.App.Config().PrivacySettings.ShowEmailAddress)
if c.HandleEtag(etag, "Get User", w, r) {
return
}
if c.AppContext.Session().UserId == user.Id {
user.Sanitize(map[string]bool{})
} else {
c.App.SanitizeProfile(user, c.IsSystemAdmin())
}
w.Header().Set(model.HeaderEtagServer, etag)
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUserByEmail(c *Context, w http.ResponseWriter, r *http.Request) {
c.SanitizeEmail()
if c.Err != nil {
return
}
sanitizeOptions := c.App.GetSanitizeOptions(c.IsSystemAdmin())
if !sanitizeOptions["email"] {
c.Err = model.NewAppError("getUserByEmail", "api.user.get_user_by_email.permissions.app_error", nil, "userId="+c.AppContext.Session().UserId, http.StatusForbidden)
return
}
user, err := c.App.GetUserByEmail(c.Params.Email)
if err != nil {
restrictions, err2 := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if err2 != nil {
c.Err = err2
return
}
if restrictions != nil {
c.SetPermissionError(model.PermissionViewMembers)
return
}
c.Err = err
return
}
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, user.Id)
if err != nil {
c.Err = err
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
etag := user.Etag(*c.App.Config().PrivacySettings.ShowFullName, *c.App.Config().PrivacySettings.ShowEmailAddress)
if c.HandleEtag(etag, "Get User", w, r) {
return
}
c.App.SanitizeProfile(user, c.IsSystemAdmin())
w.Header().Set(model.HeaderEtagServer, etag)
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getDefaultProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, c.Params.UserId)
if err != nil {
c.Err = err
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
img, err := c.App.GetDefaultProfileImage(user)
if err != nil {
c.Err = err
return
}
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%v, private", model.DayInSeconds)) // 24 hrs
w.Header().Set("Content-Type", "image/png")
w.Write(img)
}
func getProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
canSee, err := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, c.Params.UserId)
if err != nil {
c.Err = err
return
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
etag := strconv.FormatInt(user.LastPictureUpdate, 10)
if c.HandleEtag(etag, "Get Profile Image", w, r) {
return
}
img, readFailed, err := c.App.GetProfileImage(user)
if err != nil {
c.Err = err
return
}
if readFailed {
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%v, private", 5*60)) // 5 mins
} else {
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%v, private", model.DayInSeconds)) // 24 hrs
w.Header().Set(model.HeaderEtagServer, etag)
}
w.Header().Set("Content-Type", "image/png")
w.Write(img)
}
func setProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
defer io.Copy(io.Discard, r.Body)
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if *c.App.Config().FileSettings.DriverName == "" {
c.Err = model.NewAppError("uploadProfileImage", "api.user.upload_profile_user.storage.app_error", nil, "", http.StatusNotImplemented)
return
}
if r.ContentLength > *c.App.Config().FileSettings.MaxFileSize {
c.Err = model.NewAppError("uploadProfileImage", "api.user.upload_profile_user.too_large.app_error", nil, "", http.StatusRequestEntityTooLarge)
return
}
if err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize); err != nil {
c.Err = model.NewAppError("uploadProfileImage", "api.user.upload_profile_user.parse.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
m := r.MultipartForm
imageArray, ok := m.File["image"]
if !ok {
c.Err = model.NewAppError("uploadProfileImage", "api.user.upload_profile_user.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(imageArray) <= 0 {
c.Err = model.NewAppError("uploadProfileImage", "api.user.upload_profile_user.array.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("setProfileImage", audit.Fail)
defer c.LogAuditRec(auditRec)
if imageArray[0] != nil {
audit.AddEventParameter(auditRec, "filename", imageArray[0].Filename)
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.SetInvalidURLParam("user_id")
return
}
auditRec.AddEventResultState(user)
if (user.IsLDAPUser() || (user.IsSAMLUser() && *c.App.Config().SamlSettings.EnableSyncWithLdap)) &&
*c.App.Config().LdapSettings.PictureAttribute != "" {
c.Err = model.NewAppError(
"uploadProfileImage", "api.user.upload_profile_user.login_provider_attribute_set.app_error",
nil, "", http.StatusConflict)
return
}
imageData := imageArray[0]
if err := c.App.SetProfileImage(c.AppContext, c.Params.UserId, imageData); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
func setDefaultProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if *c.App.Config().FileSettings.DriverName == "" {
c.Err = model.NewAppError("setDefaultProfileImage", "api.user.upload_profile_user.storage.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec := c.MakeAuditRecord("setDefaultProfileImage", audit.Fail)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
defer c.LogAuditRec(auditRec)
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
audit.AddEventParameterAuditable(auditRec, "user", user)
if err := c.App.SetDefaultProfileImage(c.AppContext, user); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
func getTotalUsersStats(c *Context, w http.ResponseWriter, r *http.Request) {
if c.Err != nil {
return
}
restrictions, err := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
stats, err := c.App.GetTotalUsersStats(restrictions)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(stats); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getFilteredUsersStats(c *Context, w http.ResponseWriter, r *http.Request) {
teamID := r.URL.Query().Get("in_team")
channelID := r.URL.Query().Get("in_channel")
includeDeleted := r.URL.Query().Get("include_deleted")
includeBotAccounts := r.URL.Query().Get("include_bots")
rolesString := r.URL.Query().Get("roles")
channelRolesString := r.URL.Query().Get("channel_roles")
teamRolesString := r.URL.Query().Get("team_roles")
includeDeletedBool, _ := strconv.ParseBool(includeDeleted)
includeBotAccountsBool, _ := strconv.ParseBool(includeBotAccounts)
roles := []string{}
var rolesValid bool
if rolesString != "" {
roles, rolesValid = model.CleanRoleNames(strings.Split(rolesString, ","))
if !rolesValid {
c.SetInvalidParam("roles")
return
}
}
channelRoles := []string{}
if channelRolesString != "" && channelID != "" {
channelRoles, rolesValid = model.CleanRoleNames(strings.Split(channelRolesString, ","))
if !rolesValid {
c.SetInvalidParam("channelRoles")
return
}
}
teamRoles := []string{}
if teamRolesString != "" && teamID != "" {
teamRoles, rolesValid = model.CleanRoleNames(strings.Split(teamRolesString, ","))
if !rolesValid {
c.SetInvalidParam("teamRoles")
return
}
}
options := &model.UserCountOptions{
IncludeDeleted: includeDeletedBool,
IncludeBotAccounts: includeBotAccountsBool,
TeamId: teamID,
ChannelId: channelID,
Roles: roles,
ChannelRoles: channelRoles,
TeamRoles: teamRoles,
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementUsers) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementUsers)
return
}
stats, err := c.App.GetFilteredUsersStats(options)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(stats); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUsersByGroupChannelIds(c *Context, w http.ResponseWriter, r *http.Request) {
channelIds := model.ArrayFromJSON(r.Body)
if len(channelIds) == 0 {
c.SetInvalidParam("channel_ids")
return
}
usersByChannelId, appErr := c.App.GetUsersByGroupChannelIds(c.AppContext, channelIds, c.IsSystemAdmin())
if appErr != nil {
c.Err = appErr
return
}
err := json.NewEncoder(w).Encode(usersByChannelId)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func getUsers(c *Context, w http.ResponseWriter, r *http.Request) {
var (
query = r.URL.Query()
inTeamId = query.Get("in_team")
notInTeamId = query.Get("not_in_team")
inChannelId = query.Get("in_channel")
inGroupId = query.Get("in_group")
notInGroupId = query.Get("not_in_group")
notInChannelId = query.Get("not_in_channel")
groupConstrained = query.Get("group_constrained")
withoutTeam = query.Get("without_team")
inactive = query.Get("inactive")
active = query.Get("active")
role = query.Get("role")
sort = query.Get("sort")
rolesString = query.Get("roles")
channelRolesString = query.Get("channel_roles")
teamRolesString = query.Get("team_roles")
)
if notInChannelId != "" && inTeamId == "" {
c.SetInvalidURLParam("team_id")
return
}
if sort != "" && sort != "last_activity_at" && sort != "create_at" && sort != "status" && sort != "admin" && sort != "display_name" {
c.SetInvalidURLParam("sort")
return
}
// Currently only supports sorting on a team
// or sort="status" on inChannelId
// or sort="display_name" on inGroupId
if (sort == "last_activity_at" || sort == "create_at") && (inTeamId == "" || notInTeamId != "" || inChannelId != "" || notInChannelId != "" || withoutTeam != "" || inGroupId != "" || notInGroupId != "") {
c.SetInvalidURLParam("sort")
return
}
if sort == "status" && inChannelId == "" {
c.SetInvalidURLParam("sort")
return
}
if sort == "admin" && inChannelId == "" {
c.SetInvalidURLParam("sort")
return
}
if sort == "display_name" && (inGroupId == "" || notInGroupId != "" || inTeamId != "" || notInTeamId != "" || inChannelId != "" || notInChannelId != "" || withoutTeam != "") {
c.SetInvalidURLParam("sort")
return
}
var (
withoutTeamBool, _ = strconv.ParseBool(withoutTeam)
groupConstrainedBool, _ = strconv.ParseBool(groupConstrained)
inactiveBool, _ = strconv.ParseBool(inactive)
activeBool, _ = strconv.ParseBool(active)
)
if inactiveBool && activeBool {
c.SetInvalidURLParam("inactive")
}
roleNamesAll := []string{}
// MM-47378: validate 'role' related parameters
if role != "" || rolesString != "" || channelRolesString != "" || teamRolesString != "" {
// fetch all role names
rolesAll, err := c.App.GetAllRoles()
if err != nil {
c.Err = model.NewAppError("Api4.getUsers", "api.user.get_users.validation.app_error", nil, "Error fetching roles during validation.", http.StatusBadRequest)
return
}
for _, role := range rolesAll {
roleNamesAll = append(roleNamesAll, role.Name)
}
}
roles := []string{}
var rolesValid bool
if role != "" {
roles, rolesValid = model.CleanRoleNames([]string{role})
if !rolesValid {
c.SetInvalidParam("role")
return
}
roleValid := utils.StringInSlice(role, roleNamesAll)
if !roleValid {
c.SetInvalidParam("role")
return
}
}
if rolesString != "" {
roles, rolesValid = model.CleanRoleNames(strings.Split(rolesString, ","))
if !rolesValid {
c.SetInvalidParam("roles")
return
}
validRoleNames := utils.StringArrayIntersection(roleNamesAll, roles)
if len(validRoleNames) != len(roles) {
c.SetInvalidParam("roles")
return
}
}
channelRoles := []string{}
if channelRolesString != "" && inChannelId != "" {
channelRoles, rolesValid = model.CleanRoleNames(strings.Split(channelRolesString, ","))
if !rolesValid {
c.SetInvalidParam("channelRoles")
return
}
validRoleNames := utils.StringArrayIntersection(roleNamesAll, channelRoles)
if len(validRoleNames) != len(channelRoles) {
c.SetInvalidParam("channelRoles")
return
}
}
teamRoles := []string{}
if teamRolesString != "" && inTeamId != "" {
teamRoles, rolesValid = model.CleanRoleNames(strings.Split(teamRolesString, ","))
if !rolesValid {
c.SetInvalidParam("teamRoles")
return
}
validRoleNames := utils.StringArrayIntersection(roleNamesAll, teamRoles)
if len(validRoleNames) != len(teamRoles) {
c.SetInvalidParam("teamRoles")
return
}
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
userGetOptions := &model.UserGetOptions{
InTeamId: inTeamId,
InChannelId: inChannelId,
NotInTeamId: notInTeamId,
NotInChannelId: notInChannelId,
InGroupId: inGroupId,
NotInGroupId: notInGroupId,
GroupConstrained: groupConstrainedBool,
WithoutTeam: withoutTeamBool,
Inactive: inactiveBool,
Active: activeBool,
Role: role,
Roles: roles,
ChannelRoles: channelRoles,
TeamRoles: teamRoles,
Sort: sort,
Page: c.Params.Page,
PerPage: c.Params.PerPage,
ViewRestrictions: restrictions,
}
var (
profiles []*model.User
etag string
)
if inChannelId != "" {
if !*c.App.Config().TeamSettings.ExperimentalViewArchivedChannels {
channel, cErr := c.App.GetChannel(c.AppContext, inChannelId)
if cErr != nil {
c.Err = cErr
return
}
if channel.DeleteAt != 0 {
c.Err = model.NewAppError("Api4.getUsersInChannel", "api.user.view_archived_channels.get_users_in_channel.app_error", nil, "", http.StatusForbidden)
return
}
}
}
if withoutTeamBool, _ := strconv.ParseBool(withoutTeam); withoutTeamBool {
// Use a special permission for now
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionListUsersWithoutTeam) {
c.SetPermissionError(model.PermissionListUsersWithoutTeam)
return
}
profiles, appErr = c.App.GetUsersWithoutTeamPage(userGetOptions, c.IsSystemAdmin())
} else if notInChannelId != "" {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), notInChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
profiles, appErr = c.App.GetUsersNotInChannelPage(inTeamId, notInChannelId, groupConstrainedBool, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), restrictions)
} else if notInTeamId != "" {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), notInTeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
etag = c.App.GetUsersNotInTeamEtag(inTeamId, restrictions.Hash())
if c.HandleEtag(etag, "Get Users Not in Team", w, r) {
return
}
profiles, appErr = c.App.GetUsersNotInTeamPage(notInTeamId, groupConstrainedBool, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), restrictions)
} else if inTeamId != "" {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), inTeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
if sort == "last_activity_at" {
profiles, appErr = c.App.GetRecentlyActiveUsersForTeamPage(inTeamId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), restrictions)
} else if sort == "create_at" {
profiles, appErr = c.App.GetNewUsersForTeamPage(inTeamId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), restrictions)
} else {
etag = c.App.GetUsersInTeamEtag(inTeamId, restrictions.Hash())
if c.HandleEtag(etag, "Get Users in Team", w, r) {
return
}
profiles, appErr = c.App.GetUsersInTeamPage(userGetOptions, c.IsSystemAdmin())
}
} else if inChannelId != "" {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), inChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if sort == "status" {
profiles, appErr = c.App.GetUsersInChannelPageByStatus(userGetOptions, c.IsSystemAdmin())
} else if sort == "admin" {
profiles, appErr = c.App.GetUsersInChannelPageByAdmin(userGetOptions, c.IsSystemAdmin())
} else {
profiles, appErr = c.App.GetUsersInChannelPage(userGetOptions, c.IsSystemAdmin())
}
} else if inGroupId != "" {
if gErr := requireGroupAccess(c, inGroupId); gErr != nil {
gErr.Where = "Api.getUsers"
c.Err = gErr
return
}
if sort == "display_name" {
var user *model.User
user, appErr = c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
profiles, _, appErr = c.App.GetGroupMemberUsersSortedPage(inGroupId, c.Params.Page, c.Params.PerPage, userGetOptions.ViewRestrictions, c.App.GetNotificationNameFormat(user))
} else {
profiles, _, appErr = c.App.GetGroupMemberUsersPage(inGroupId, c.Params.Page, c.Params.PerPage, userGetOptions.ViewRestrictions)
}
} else if notInGroupId != "" {
appErr = requireGroupAccess(c, notInGroupId)
if appErr != nil {
appErr.Where = "Api.getUsers"
c.Err = appErr
return
}
profiles, appErr = c.App.GetUsersNotInGroupPage(notInGroupId, c.Params.Page, c.Params.PerPage, userGetOptions.ViewRestrictions)
if appErr != nil {
c.Err = appErr
return
}
} else {
userGetOptions, appErr = c.App.RestrictUsersGetByPermissions(c.AppContext.Session().UserId, userGetOptions)
if appErr != nil {
c.Err = appErr
return
}
profiles, appErr = c.App.GetUsersPage(userGetOptions, c.IsSystemAdmin())
}
if appErr != nil {
c.Err = appErr
return
}
if etag != "" {
w.Header().Set(model.HeaderEtagServer, etag)
}
c.App.Srv().Platform().UpdateLastActivityAtIfNeeded(*c.AppContext.Session())
js, err := json.Marshal(profiles)
if err != nil {
c.Err = model.NewAppError("getUsers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func requireGroupAccess(c *web.Context, groupID string) *model.AppError {
group, err := c.App.GetGroup(groupID, nil, nil)
if err != nil {
return err
}
if lcErr := licensedAndConfiguredForGroupBySource(c.App, group.Source); lcErr != nil {
return lcErr
}
if group.Source == model.GroupSourceLdap {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) {
return c.App.MakePermissionError(c.AppContext.Session(), []*model.Permission{model.PermissionSysconsoleReadUserManagementGroups})
}
}
return nil
}
func getUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
var userIDs []string
err := json.NewDecoder(r.Body).Decode(&userIDs)
if err != nil || len(userIDs) == 0 {
c.SetInvalidParamWithErr("user_ids", err)
return
}
sinceString := r.URL.Query().Get("since")
options := &store.UserGetByIdsOpts{
IsAdmin: c.IsSystemAdmin(),
}
if sinceString != "" {
since, sErr := strconv.ParseInt(sinceString, 10, 64)
if sErr != nil {
c.SetInvalidParamWithErr("since", sErr)
return
}
options.Since = since
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
options.ViewRestrictions = restrictions
users, appErr := c.App.GetUsersByIds(userIDs, options)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(users)
if err != nil {
c.Err = model.NewAppError("getUsersByIds", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getUsersByNames(c *Context, w http.ResponseWriter, r *http.Request) {
var usernames []string
err := json.NewDecoder(r.Body).Decode(&usernames)
if err != nil || len(usernames) == 0 {
c.SetInvalidParamWithErr("usernames", err)
return
}
restrictions, appErr := c.App.GetViewUsersRestrictions(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
users, appErr := c.App.GetUsersByUsernames(usernames, c.IsSystemAdmin(), restrictions)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(users)
if err != nil {
c.Err = model.NewAppError("getUsersByNames", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getKnownUsers(c *Context, w http.ResponseWriter, r *http.Request) {
userIDs, appErr := c.App.GetKnownUsers(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
err := json.NewEncoder(w).Encode(userIDs)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func searchUsers(c *Context, w http.ResponseWriter, r *http.Request) {
var props model.UserSearch
if err := json.NewDecoder(r.Body).Decode(&props); err != nil {
c.SetInvalidParamWithErr("props", err)
return
}
if props.Limit == 0 {
props.Limit = model.UserSearchDefaultLimit
}
if props.Term == "" {
c.SetInvalidParam("term")
return
}
if props.TeamId == "" && props.NotInChannelId != "" {
c.SetInvalidParam("team_id")
return
}
if props.InGroupId != "" {
if appErr := requireGroupAccess(c, props.InGroupId); appErr != nil {
appErr.Where = "Api.searchUsers"
c.Err = appErr
return
}
}
if props.NotInGroupId != "" {
if appErr := requireGroupAccess(c, props.NotInGroupId); appErr != nil {
appErr.Where = "Api.searchUsers"
c.Err = appErr
return
}
}
if props.InChannelId != "" && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), props.InChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if props.NotInChannelId != "" && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), props.NotInChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if props.TeamId != "" && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), props.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
if props.NotInTeamId != "" && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), props.NotInTeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
if props.Limit <= 0 || props.Limit > model.UserSearchMaxLimit {
c.SetInvalidParam("limit")
return
}
options := &model.UserSearchOptions{
IsAdmin: c.IsSystemAdmin(),
AllowInactive: props.AllowInactive,
GroupConstrained: props.GroupConstrained,
Limit: props.Limit,
Role: props.Role,
Roles: props.Roles,
ChannelRoles: props.ChannelRoles,
TeamRoles: props.TeamRoles,
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
options.AllowEmails = true
options.AllowFullNames = true
} else {
options.AllowEmails = *c.App.Config().PrivacySettings.ShowEmailAddress
options.AllowFullNames = *c.App.Config().PrivacySettings.ShowFullName
}
options, appErr := c.App.RestrictUsersSearchByPermissions(c.AppContext.Session().UserId, options)
if appErr != nil {
c.Err = appErr
return
}
profiles, appErr := c.App.SearchUsers(&props, options)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(profiles)
if err != nil {
c.Err = model.NewAppError("searchUsers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func autocompleteUsers(c *Context, w http.ResponseWriter, r *http.Request) {
channelId := r.URL.Query().Get("in_channel")
teamId := r.URL.Query().Get("in_team")
name := r.URL.Query().Get("name")
limitStr := r.URL.Query().Get("limit")
limit, _ := strconv.Atoi(limitStr)
if limitStr == "" {
limit = model.UserSearchDefaultLimit
} else if limit > model.UserSearchMaxLimit {
limit = model.UserSearchMaxLimit
}
options := &model.UserSearchOptions{
IsAdmin: c.IsSystemAdmin(),
// Never autocomplete on emails.
AllowEmails: false,
Limit: limit,
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
options.AllowFullNames = true
} else {
options.AllowFullNames = *c.App.Config().PrivacySettings.ShowFullName
}
if channelId != "" {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
}
if teamId != "" {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
}
var autocomplete model.UserAutocomplete
var err *model.AppError
options, err = c.App.RestrictUsersSearchByPermissions(c.AppContext.Session().UserId, options)
if err != nil {
c.Err = err
return
}
if channelId != "" {
// We're using the channelId to search for users inside that channel and the team
// to get the not in channel list. Also we want to include the DM and GM users for
// that team which could only be obtained having the team id.
if teamId == "" {
c.Err = model.NewAppError("autocompleteUser",
"api.user.autocomplete_users.missing_team_id.app_error",
nil,
"channelId="+channelId,
http.StatusInternalServerError,
)
return
}
result, err := c.App.AutocompleteUsersInChannel(teamId, channelId, name, options)
if err != nil {
c.Err = err
return
}
autocomplete.Users = result.InChannel
autocomplete.OutOfChannel = result.OutOfChannel
} else if teamId != "" {
result, err := c.App.AutocompleteUsersInTeam(teamId, name, options)
if err != nil {
c.Err = err
return
}
autocomplete.Users = result.InTeam
} else {
result, err := c.App.SearchUsersInTeam("", name, options)
if err != nil {
c.Err = err
return
}
autocomplete.Users = result
}
if err := json.NewEncoder(w).Encode(autocomplete); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("updateUser", audit.Fail)
defer c.LogAuditRec(auditRec)
var user model.User
if jsonErr := json.NewDecoder(r.Body).Decode(&user); jsonErr != nil {
c.SetInvalidParamWithErr("user", jsonErr)
return
}
audit.AddEventParameterAuditable(auditRec, "user", &user)
// The user being updated in the payload must be the same one as indicated in the URL.
if user.Id != c.Params.UserId {
c.SetInvalidParam("user_id")
return
}
// Cannot update a system admin unless user making request is a systemadmin also.
if user.IsSystemAdmin() && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), user.Id) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
ouser, err := c.App.GetUser(user.Id)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(ouser)
auditRec.AddEventObjectType("user")
if c.AppContext.Session().IsOAuth {
if ouser.Email != user.Email {
c.SetPermissionError(model.PermissionEditOtherUsers)
c.Err.DetailedError += ", attempted email update by oauth app"
return
}
}
// Check that the fields being updated are not set by the login provider
conflictField := c.App.CheckProviderAttributes(ouser, user.ToPatch())
if conflictField != "" {
c.Err = model.NewAppError(
"updateUser", "api.user.update_user.login_provider_attribute_set.app_error",
map[string]any{"Field": conflictField}, "", http.StatusConflict)
return
}
// If eMail update is attempted by the currently logged in user, check if correct password was provided
if user.Email != "" && ouser.Email != user.Email && c.AppContext.Session().UserId == c.Params.UserId {
err = c.App.DoubleCheckPassword(ouser, user.Password)
if err != nil {
c.SetInvalidParam("password")
return
}
}
ruser, err := c.App.UpdateUserAsUser(c.AppContext, &user, c.IsSystemAdmin())
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(ruser)
c.LogAudit("")
if err := json.NewEncoder(w).Encode(ruser); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func patchUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
var patch model.UserPatch
if jsonErr := json.NewDecoder(r.Body).Decode(&patch); jsonErr != nil {
c.SetInvalidParamWithErr("user", jsonErr)
return
}
auditRec := c.MakeAuditRecord("patchUser", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "user_patch", &patch)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
ouser, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.SetInvalidParam("user_id")
return
}
auditRec.AddEventPriorState(ouser)
auditRec.AddEventObjectType("user")
// Cannot update a system admin unless user making request is a systemadmin also
if ouser.IsSystemAdmin() && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if c.AppContext.Session().IsOAuth && patch.Email != nil {
if ouser.Email != *patch.Email {
c.SetPermissionError(model.PermissionEditOtherUsers)
c.Err.DetailedError += ", attempted email update by oauth app"
return
}
}
conflictField := c.App.CheckProviderAttributes(ouser, &patch)
if conflictField != "" {
c.Err = model.NewAppError(
"patchUser", "api.user.patch_user.login_provider_attribute_set.app_error",
map[string]any{"Field": conflictField}, "", http.StatusConflict)
return
}
// If eMail update is attempted by the currently logged in user, check if correct password was provided
if patch.Email != nil && ouser.Email != *patch.Email && c.AppContext.Session().UserId == c.Params.UserId {
if patch.Password == nil {
c.SetInvalidParam("password")
return
}
if err = c.App.DoubleCheckPassword(ouser, *patch.Password); err != nil {
c.Err = err
return
}
}
ruser, err := c.App.PatchUser(c.AppContext, c.Params.UserId, &patch, c.IsSystemAdmin())
if err != nil {
c.Err = err
return
}
c.App.SetAutoResponderStatus(ruser, ouser.NotifyProps)
auditRec.Success()
auditRec.AddEventResultState(ruser)
c.LogAudit("")
if err := json.NewEncoder(w).Encode(ruser); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
userId := c.Params.UserId
auditRec := c.MakeAuditRecord("deleteUser", audit.Fail)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), userId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
// if EnableUserDeactivation flag is disabled the user cannot deactivate himself.
if c.Params.UserId == c.AppContext.Session().UserId && !*c.App.Config().TeamSettings.EnableUserDeactivation && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.Err = model.NewAppError("deleteUser", "api.user.update_active.not_enable.app_error", nil, "userId="+c.Params.UserId, http.StatusUnauthorized)
return
}
user, err := c.App.GetUser(userId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(user)
auditRec.AddEventObjectType("user")
// Cannot update a system admin unless user making request is a systemadmin also
if user.IsSystemAdmin() && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if c.Params.Permanent {
if *c.App.Config().ServiceSettings.EnableAPIUserDeletion {
err = c.App.PermanentDeleteUser(c.AppContext, user)
} else {
err = model.NewAppError("deleteUser", "api.user.delete_user.not_enabled.app_error", nil, "userId="+c.Params.UserId, http.StatusUnauthorized)
}
} else {
_, err = c.App.UpdateActive(c.AppContext, user, false)
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func updateUserRoles(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
props := model.MapFromJSON(r.Body)
newRoles := props["roles"]
if !model.IsValidUserRoles(newRoles) {
c.SetInvalidParam("roles")
return
}
// require license feature to assign "new system roles"
for _, roleName := range strings.Fields(newRoles) {
for _, id := range model.NewSystemRoleIDs {
if roleName == id {
if license := c.App.Channels().License(); license == nil || !*license.Features.CustomPermissionsSchemes {
c.Err = model.NewAppError("updateUserRoles", "api.user.update_user_roles.license.app_error", nil, "", http.StatusBadRequest)
return
}
}
}
}
auditRec := c.MakeAuditRecord("updateUserRoles", audit.Fail)
audit.AddEventParameter(auditRec, "roles", newRoles)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageRoles) {
c.SetPermissionError(model.PermissionManageRoles)
return
}
user, err := c.App.UpdateUserRoles(c.AppContext, c.Params.UserId, newRoles, true)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(user)
auditRec.AddEventObjectType("user")
c.LogAudit(fmt.Sprintf("user=%s roles=%s", c.Params.UserId, newRoles))
ReturnStatusOK(w)
}
func updateUserActive(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
props := model.StringInterfaceFromJSON(r.Body)
active, ok := props["active"].(bool)
if !ok {
c.SetInvalidParam("active")
return
}
auditRec := c.MakeAuditRecord("updateUserActive", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "active", active)
// true when you're trying to de-activate yourself
isSelfDeactivate := !active && c.Params.UserId == c.AppContext.Session().UserId
if !isSelfDeactivate && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementUsers) {
c.Err = model.NewAppError("updateUserActive", "api.user.update_active.permissions.app_error", nil, "userId="+c.Params.UserId, http.StatusForbidden)
return
}
// if EnableUserDeactivation flag is disabled the user cannot deactivate himself.
if isSelfDeactivate && !*c.App.Config().TeamSettings.EnableUserDeactivation {
c.Err = model.NewAppError("updateUserActive", "api.user.update_active.not_enable.app_error", nil, "userId="+c.Params.UserId, http.StatusUnauthorized)
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(user)
auditRec.AddEventObjectType("user")
if user.IsSystemAdmin() && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if active && user.IsGuest() && !*c.App.Config().GuestAccountsSettings.Enable {
c.Err = model.NewAppError("updateUserActive", "api.user.update_active.cannot_enable_guest_when_guest_feature_is_disabled.app_error", nil, "userId="+c.Params.UserId, http.StatusUnauthorized)
return
}
if _, err = c.App.UpdateActive(c.AppContext, user, active); err != nil {
c.Err = err
}
auditRec.Success()
c.LogAudit(fmt.Sprintf("user_id=%s active=%v", user.Id, active))
if isSelfDeactivate {
c.App.Srv().Go(func() {
if err := c.App.Srv().EmailService.SendDeactivateAccountEmail(user.Email, user.Locale, c.App.GetSiteURL()); err != nil {
c.LogErrorByCode(model.NewAppError("SendDeactivateEmail", "api.user.send_deactivate_email_and_forget.failed.error", nil, err.Error(), http.StatusInternalServerError))
}
})
}
message := model.NewWebSocketEvent(model.WebsocketEventUserActivationStatusChange, "", "", "", nil, "")
c.App.Publish(message)
ReturnStatusOK(w)
}
func updateUserAuth(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("updateUserAuth", audit.Fail)
defer c.LogAuditRec(auditRec)
var userAuth model.UserAuth
if jsonErr := json.NewDecoder(r.Body).Decode(&userAuth); jsonErr != nil {
c.SetInvalidParamWithErr("user", jsonErr)
return
}
audit.AddEventParameterAuditable(auditRec, "user_auth", &userAuth)
if userAuth.AuthData == nil || *userAuth.AuthData == "" || userAuth.AuthService == "" {
c.Err = model.NewAppError("updateUserAuth", "api.user.update_user_auth.invalid_request", nil, "", http.StatusBadRequest)
return
}
if user, err := c.App.GetUser(c.Params.UserId); err == nil {
auditRec.AddEventPriorState(user)
}
user, err := c.App.UpdateUserAuth(c.Params.UserId, &userAuth)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(user)
auditRec.Success()
auditRec.AddMeta("auth_service", user.AuthService)
c.LogAudit(fmt.Sprintf("updated user %s auth to service=%v", c.Params.UserId, user.AuthService))
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateUserMfa(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("updateUserMfa", audit.Fail)
defer c.LogAuditRec(auditRec)
if c.AppContext.Session().IsOAuth {
c.SetPermissionError(model.PermissionEditOtherUsers)
c.Err.DetailedError += ", attempted access by oauth app"
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if user, err := c.App.GetUser(c.Params.UserId); err == nil {
audit.AddEventParameterAuditable(auditRec, "user", user)
}
props := model.StringInterfaceFromJSON(r.Body)
activate, ok := props["activate"].(bool)
if !ok {
c.SetInvalidParam("activate")
return
}
code := ""
if activate {
code, ok = props["code"].(string)
if !ok || code == "" {
c.SetInvalidParam("code")
return
}
}
c.LogAudit("attempt")
if err := c.App.UpdateMfa(c.AppContext, activate, c.Params.UserId, code); err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddMeta("activate", activate)
c.LogAudit("success - mfa updated")
ReturnStatusOK(w)
}
func generateMfaSecret(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if c.AppContext.Session().IsOAuth {
c.SetPermissionError(model.PermissionEditOtherUsers)
c.Err.DetailedError += ", attempted access by oauth app"
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
secret, err := c.App.GenerateMfaSecret(c.Params.UserId)
if err != nil {
c.Err = err
return
}
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
if err := json.NewEncoder(w).Encode(secret); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
props := model.MapFromJSON(r.Body)
newPassword := props["new_password"]
auditRec := c.MakeAuditRecord("updatePassword", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempted")
var canUpdatePassword bool
if user, err := c.App.GetUser(c.Params.UserId); err == nil {
audit.AddEventParameterAuditable(auditRec, "user", user)
if user.IsSystemAdmin() {
canUpdatePassword = c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem)
} else {
canUpdatePassword = c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementUsers)
}
}
var err *model.AppError
// There are two main update flows depending on whether the provided password
// is already hashed or not.
if props["already_hashed"] == "true" {
if canUpdatePassword {
err = c.App.UpdateHashedPasswordByUserId(c.Params.UserId, newPassword)
} else if c.Params.UserId == c.AppContext.Session().UserId {
err = model.NewAppError("updatePassword", "api.user.update_password.user_and_hashed.app_error", nil, "", http.StatusUnauthorized)
} else {
err = model.NewAppError("updatePassword", "api.user.update_password.context.app_error", nil, "", http.StatusForbidden)
}
} else {
if c.Params.UserId == c.AppContext.Session().UserId {
currentPassword := props["current_password"]
if currentPassword == "" {
c.SetInvalidParam("current_password")
return
}
err = c.App.UpdatePasswordAsUser(c.AppContext, c.Params.UserId, currentPassword, newPassword)
} else if canUpdatePassword {
err = c.App.UpdatePasswordByUserIdSendEmail(c.AppContext, c.Params.UserId, newPassword, c.AppContext.T("api.user.reset_password.method"))
} else {
err = model.NewAppError("updatePassword", "api.user.update_password.context.app_error", nil, "", http.StatusForbidden)
}
}
if err != nil {
c.LogAudit("failed")
c.Err = err
return
}
auditRec.Success()
c.LogAudit("completed")
ReturnStatusOK(w)
}
func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
token := props["token"]
if len(token) != model.TokenSize {
c.SetInvalidParam("token")
return
}
newPassword := props["new_password"]
auditRec := c.MakeAuditRecord("resetPassword", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt - token=" + token)
if err := c.App.ResetPasswordFromToken(c.AppContext, token, newPassword); err != nil {
c.LogAudit("fail - token=" + token)
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success - token=" + token)
ReturnStatusOK(w)
}
func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
email := props["email"]
email = strings.ToLower(email)
if email == "" {
c.SetInvalidParam("email")
return
}
auditRec := c.MakeAuditRecord("sendPasswordReset", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "email", email)
sent, err := c.App.SendPasswordReset(email, c.App.GetSiteURL())
if err != nil {
if *c.App.Config().ServiceSettings.ExperimentalEnableHardenedMode {
ReturnStatusOK(w)
} else {
c.Err = err
}
return
}
if sent {
auditRec.Success()
c.LogAudit("sent=" + email)
}
ReturnStatusOK(w)
}
func login(c *Context, w http.ResponseWriter, r *http.Request) {
// Mask all sensitive errors, with the exception of the following
defer func() {
if c.Err == nil {
return
}
unmaskedErrors := []string{
"mfa.validate_token.authenticate.app_error",
"api.user.check_user_mfa.bad_code.app_error",
"api.user.login.blank_pwd.app_error",
"api.user.login.bot_login_forbidden.app_error",
"api.user.login.client_side_cert.certificate.app_error",
"api.user.login.inactive.app_error",
"api.user.login.not_verified.app_error",
"api.user.check_user_login_attempts.too_many.app_error",
"app.team.join_user_to_team.max_accounts.app_error",
"store.sql_user.save.max_accounts.app_error",
}
maskError := true
for _, unmaskedError := range unmaskedErrors {
if c.Err.Id == unmaskedError {
maskError = false
}
}
if !maskError {
return
}
config := c.App.Config()
enableUsername := *config.EmailSettings.EnableSignInWithUsername
enableEmail := *config.EmailSettings.EnableSignInWithEmail
samlEnabled := *config.SamlSettings.Enable
gitlabEnabled := *config.GitLabSettings.Enable
openidEnabled := *config.OpenIdSettings.Enable
googleEnabled := *config.GoogleSettings.Enable
office365Enabled := *config.Office365Settings.Enable
if samlEnabled || gitlabEnabled || googleEnabled || office365Enabled || openidEnabled {
c.Err = model.NewAppError("login", "api.user.login.invalid_credentials_sso", nil, "", http.StatusUnauthorized)
return
}
if enableUsername && !enableEmail {
c.Err = model.NewAppError("login", "api.user.login.invalid_credentials_username", nil, "", http.StatusUnauthorized)
return
}
if !enableUsername && enableEmail {
c.Err = model.NewAppError("login", "api.user.login.invalid_credentials_email", nil, "", http.StatusUnauthorized)
return
}
c.Err = model.NewAppError("login", "api.user.login.invalid_credentials_email_username", nil, "", http.StatusUnauthorized)
}()
props := model.MapFromJSON(r.Body)
id := props["id"]
loginId := props["login_id"]
password := props["password"]
mfaToken := props["token"]
deviceId := props["device_id"]
ldapOnly := props["ldap_only"] == "true"
if *c.App.Config().ExperimentalSettings.ClientSideCertEnable {
if license := c.App.Channels().License(); license == nil || !*license.Features.FutureFeatures {
c.Err = model.NewAppError("ClientSideCertNotAllowed", "api.user.login.client_side_cert.license.app_error", nil, "", http.StatusBadRequest)
return
}
certPem, certSubject, certEmail := c.App.CheckForClientSideCert(r)
c.Logger.Debug("Client Cert", mlog.String("cert_subject", certSubject), mlog.String("cert_email", certEmail))
if certPem == "" || certEmail == "" {
c.Err = model.NewAppError("ClientSideCertMissing", "api.user.login.client_side_cert.certificate.app_error", nil, "", http.StatusBadRequest)
return
}
if *c.App.Config().ExperimentalSettings.ClientSideCertCheck == model.ClientSideCertCheckPrimaryAuth {
loginId = certEmail
password = "certificate"
}
}
auditRec := c.MakeAuditRecord("login", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "login_id", loginId)
audit.AddEventParameter(auditRec, "device_id", deviceId)
c.LogAuditWithUserId(id, "attempt - login_id="+loginId)
user, err := c.App.AuthenticateUserForLogin(c.AppContext, id, loginId, password, mfaToken, "", ldapOnly)
if err != nil {
c.LogAuditWithUserId(id, "failure - login_id="+loginId)
c.Err = err
return
}
auditRec.AddEventResultState(user)
if user.IsGuest() {
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("login", "api.user.login.guest_accounts.license.error", nil, "", http.StatusUnauthorized)
return
}
if !*c.App.Config().GuestAccountsSettings.Enable {
c.Err = model.NewAppError("login", "api.user.login.guest_accounts.disabled.error", nil, "", http.StatusUnauthorized)
return
}
}
c.LogAuditWithUserId(user.Id, "authenticated")
err = c.App.DoLogin(c.AppContext, w, r, user, deviceId, false, false, false)
if err != nil {
c.Err = err
return
}
c.LogAuditWithUserId(user.Id, "success")
if r.Header.Get(model.HeaderRequestedWith) == model.HeaderRequestedWithXML {
c.App.AttachSessionCookies(c.AppContext, w, r)
}
userTermsOfService, err := c.App.GetUserTermsOfService(user.Id)
if err != nil && err.StatusCode != http.StatusNotFound {
c.Err = err
return
}
if userTermsOfService != nil {
user.TermsOfServiceId = userTermsOfService.TermsOfServiceId
user.TermsOfServiceCreateAt = userTermsOfService.CreateAt
}
user.Sanitize(map[string]bool{})
auditRec.Success()
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func loginCWS(c *Context, w http.ResponseWriter, r *http.Request) {
campaignToURL := map[string]string{
"focalboard": "/boards",
}
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("loginCWS", "api.user.login_cws.license.error", nil, "", http.StatusUnauthorized)
return
}
r.ParseForm()
var loginID string
var token string
var campaign string
if len(r.Form) > 0 {
for key, value := range r.Form {
if key == "login_id" {
loginID = value[0]
}
if key == "cws_token" {
token = value[0]
}
if key == "utm_campaign" {
campaign = value[0]
}
}
}
auditRec := c.MakeAuditRecord("login", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "login_id", loginID)
user, err := c.App.AuthenticateUserForLogin(c.AppContext, "", loginID, "", "", token, false)
if err != nil {
c.LogAuditWithUserId("", "failure - login_id="+loginID)
c.LogErrorByCode(err)
http.Redirect(w, r, *c.App.Config().ServiceSettings.SiteURL, http.StatusFound)
return
}
audit.AddEventParameterAuditable(auditRec, "user", user)
c.LogAuditWithUserId(user.Id, "authenticated")
err = c.App.DoLogin(c.AppContext, w, r, user, "", false, false, false)
if err != nil {
c.LogErrorByCode(err)
http.Redirect(w, r, *c.App.Config().ServiceSettings.SiteURL, http.StatusFound)
return
}
c.LogAuditWithUserId(user.Id, "success")
c.App.AttachSessionCookies(c.AppContext, w, r)
redirectURL := *c.App.Config().ServiceSettings.SiteURL
if campaign != "" {
if url, ok := campaignToURL[campaign]; ok {
properties := map[string]any{
"category": "acquisition",
"redirect_to": strings.TrimSuffix(url, "/"),
}
c.App.Srv().GetTelemetryService().SendTelemetry("product_start_redirect", properties)
redirectURL += url
}
}
http.Redirect(w, r, redirectURL, http.StatusFound)
}
func logout(c *Context, w http.ResponseWriter, r *http.Request) {
Logout(c, w, r)
}
func Logout(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("Logout", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("")
c.RemoveSessionCookie(w, r)
if c.AppContext.Session().Id != "" {
if err := c.App.RevokeSessionById(c.AppContext.Session().Id); err != nil {
c.Err = err
return
}
}
auditRec.Success()
ReturnStatusOK(w)
}
func getSessions(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
sessions, appErr := c.App.GetSessions(c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
for _, session := range sessions {
session.Sanitize()
}
js, err := json.Marshal(sessions)
if err != nil {
c.Err = model.NewAppError("getSessions", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func revokeSession(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("revokeSession", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
props := model.MapFromJSON(r.Body)
sessionId := props["session_id"]
if sessionId == "" {
c.SetInvalidParam("session_id")
return
}
audit.AddEventParameter(auditRec, "session_id", sessionId)
session, err := c.App.GetSessionById(sessionId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(session)
auditRec.AddEventObjectType("session")
if session.UserId != c.Params.UserId {
c.SetInvalidURLParam("user_id")
return
}
if err := c.App.RevokeSession(session); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
func revokeAllSessionsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("revokeAllSessionsForUser", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if err := c.App.RevokeAllSessions(c.Params.UserId); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
func revokeAllSessionsAllUsers(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
auditRec := c.MakeAuditRecord("revokeAllSessionsAllUsers", audit.Fail)
defer c.LogAuditRec(auditRec)
if err := c.App.RevokeSessionsFromAllUsers(); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
func attachDeviceId(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
deviceId := props["device_id"]
if deviceId == "" {
c.SetInvalidParam("device_id")
return
}
auditRec := c.MakeAuditRecord("attachDeviceId", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "device_id", deviceId)
// A special case where we logout of all other sessions with the same device id
if err := c.App.RevokeSessionsForDeviceId(c.AppContext.Session().UserId, deviceId, c.AppContext.Session().Id); err != nil {
c.Err = err
return
}
c.App.ClearSessionCacheForUser(c.AppContext.Session().UserId)
c.App.SetSessionExpireInHours(c.AppContext.Session(), *c.App.Config().ServiceSettings.SessionLengthMobileInHours)
maxAgeSeconds := *c.App.Config().ServiceSettings.SessionLengthMobileInHours * 60 * 60
secure := false
if app.GetProtocol(r) == "https" {
secure = true
}
subpath, _ := utils.GetSubpathFromConfig(c.App.Config())
expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAgeSeconds), 0)
sessionCookie := &http.Cookie{
Name: model.SessionCookieToken,
Value: c.AppContext.Session().Token,
Path: subpath,
MaxAge: maxAgeSeconds,
Expires: expiresAt,
HttpOnly: true,
Domain: c.App.GetCookieDomain(),
Secure: secure,
}
http.SetCookie(w, sessionCookie)
if err := c.App.AttachDeviceId(c.AppContext.Session().Id, deviceId, c.AppContext.Session().ExpiresAt); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}
func getUserAudits(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("getUserAudits", audit.Fail)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
defer c.LogAuditRec(auditRec)
if user, err := c.App.GetUser(c.Params.UserId); err == nil {
audit.AddEventParameterAuditable(auditRec, "user", user)
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
audits, err := c.App.GetAuditsPage(c.Params.UserId, c.Params.Page, c.Params.PerPage)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddMeta("page", c.Params.Page)
auditRec.AddMeta("audits_per_page", c.Params.LogsPerPage)
if err := json.NewEncoder(w).Encode(audits); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func verifyUserEmail(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
token := props["token"]
if len(token) != model.TokenSize {
c.SetInvalidParam("token")
return
}
auditRec := c.MakeAuditRecord("verifyUserEmail", audit.Fail)
defer c.LogAuditRec(auditRec)
if err := c.App.VerifyEmailFromToken(c.AppContext, token); err != nil {
c.Err = model.NewAppError("verifyUserEmail", "api.user.verify_email.bad_link.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
auditRec.Success()
c.LogAudit("Email Verified")
ReturnStatusOK(w)
}
func sendVerificationEmail(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
email := props["email"]
email = strings.ToLower(email)
if email == "" {
c.SetInvalidParam("email")
return
}
redirect := r.URL.Query().Get("r")
auditRec := c.MakeAuditRecord("sendVerificationEmail", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "email", email)
audit.AddEventParameter(auditRec, "redirect", redirect)
user, err := c.App.GetUserForLogin("", email)
if err != nil {
// Don't want to leak whether the email is valid or not
ReturnStatusOK(w)
return
}
auditRec.AddEventResultState(user)
if err = c.App.SendEmailVerification(user, user.Email, redirect); err != nil {
// Don't want to leak whether the email is valid or not
c.LogErrorByCode(err)
ReturnStatusOK(w)
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func switchAccountType(c *Context, w http.ResponseWriter, r *http.Request) {
var switchRequest model.SwitchRequest
if jsonErr := json.NewDecoder(r.Body).Decode(&switchRequest); jsonErr != nil {
c.SetInvalidParamWithErr("switch_request", jsonErr)
return
}
auditRec := c.MakeAuditRecord("switchAccountType", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "switch_request", &switchRequest)
link := ""
var err *model.AppError
if switchRequest.EmailToOAuth() {
link, err = c.App.SwitchEmailToOAuth(w, r, switchRequest.Email, switchRequest.Password, switchRequest.MfaCode, switchRequest.NewService)
} else if switchRequest.OAuthToEmail() {
c.SessionRequired()
if c.Err != nil {
return
}
link, err = c.App.SwitchOAuthToEmail(switchRequest.Email, switchRequest.NewPassword, c.AppContext.Session().UserId)
} else if switchRequest.EmailToLdap() {
link, err = c.App.SwitchEmailToLdap(switchRequest.Email, switchRequest.Password, switchRequest.MfaCode, switchRequest.LdapLoginId, switchRequest.NewPassword)
} else if switchRequest.LdapToEmail() {
link, err = c.App.SwitchLdapToEmail(switchRequest.Password, switchRequest.MfaCode, switchRequest.Email, switchRequest.NewPassword)
} else {
c.SetInvalidParam("switch_request")
return
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
w.Write([]byte(model.MapToJSON(map[string]string{"follow_link": link})))
}
func createUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("createUserAccessToken", audit.Fail)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
defer c.LogAuditRec(auditRec)
if user, err := c.App.GetUser(c.Params.UserId); err == nil {
audit.AddEventParameterAuditable(auditRec, "user", user)
}
if c.AppContext.Session().IsOAuth {
c.SetPermissionError(model.PermissionCreateUserAccessToken)
c.Err.DetailedError += ", attempted access by oauth app"
return
}
var accessToken model.UserAccessToken
if jsonErr := json.NewDecoder(r.Body).Decode(&accessToken); jsonErr != nil {
c.SetInvalidParamWithErr("user_access_token", jsonErr)
return
}
if accessToken.Description == "" {
c.SetInvalidParam("description")
return
}
c.LogAudit("")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateUserAccessToken) {
c.SetPermissionError(model.PermissionCreateUserAccessToken)
return
}
if !c.App.SessionHasPermissionToUserOrBot(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
accessToken.UserId = c.Params.UserId
accessToken.Token = ""
token, err := c.App.CreateUserAccessToken(&accessToken)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddMeta("token_id", token.Id)
c.LogAudit("success - token_id=" + token.Id)
if err := json.NewEncoder(w).Encode(token); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func searchUserAccessTokens(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
var props model.UserAccessTokenSearch
if err := json.NewDecoder(r.Body).Decode(&props); err != nil {
c.SetInvalidParamWithErr("user_access_token_search", err)
return
}
if props.Term == "" {
c.SetInvalidParam("term")
return
}
accessTokens, appErr := c.App.SearchUserAccessTokens(props.Term)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(accessTokens)
if err != nil {
c.Err = model.NewAppError("searchUserAccessTokens", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getUserAccessTokens(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
accessTokens, appErr := c.App.GetUserAccessTokens(c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(accessTokens)
if err != nil {
c.Err = model.NewAppError("searchUserAccessTokens", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getUserAccessTokensForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadUserAccessToken) {
c.SetPermissionError(model.PermissionReadUserAccessToken)
return
}
if !c.App.SessionHasPermissionToUserOrBot(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
accessTokens, appErr := c.App.GetUserAccessTokensForUser(c.Params.UserId, c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(accessTokens)
if err != nil {
c.Err = model.NewAppError("searchUserAccessTokens", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTokenId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadUserAccessToken) {
c.SetPermissionError(model.PermissionReadUserAccessToken)
return
}
accessToken, appErr := c.App.GetUserAccessToken(c.Params.TokenId, true)
if appErr != nil {
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToUserOrBot(*c.AppContext.Session(), accessToken.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if err := json.NewEncoder(w).Encode(accessToken); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func revokeUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
tokenId := props["token_id"]
if tokenId == "" {
c.SetInvalidParam("token_id")
}
auditRec := c.MakeAuditRecord("revokeUserAccessToken", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "token_id", tokenId)
c.LogAudit("")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRevokeUserAccessToken) {
c.SetPermissionError(model.PermissionRevokeUserAccessToken)
return
}
accessToken, err := c.App.GetUserAccessToken(tokenId, false)
if err != nil {
c.Err = err
return
}
if user, errGet := c.App.GetUser(accessToken.UserId); errGet == nil {
audit.AddEventParameterAuditable(auditRec, "user", user)
}
if !c.App.SessionHasPermissionToUserOrBot(*c.AppContext.Session(), accessToken.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if err = c.App.RevokeUserAccessToken(accessToken); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success - token_id=" + accessToken.Id)
ReturnStatusOK(w)
}
func disableUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
tokenId := props["token_id"]
if tokenId == "" {
c.SetInvalidParam("token_id")
}
auditRec := c.MakeAuditRecord("disableUserAccessToken", audit.Fail)
audit.AddEventParameter(auditRec, "token_id", tokenId)
defer c.LogAuditRec(auditRec)
c.LogAudit("")
// No separate permission for this action for now
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRevokeUserAccessToken) {
c.SetPermissionError(model.PermissionRevokeUserAccessToken)
return
}
accessToken, err := c.App.GetUserAccessToken(tokenId, false)
if err != nil {
c.Err = err
return
}
if user, errGet := c.App.GetUser(accessToken.UserId); errGet == nil {
audit.AddEventParameterAuditable(auditRec, "user", user)
}
if !c.App.SessionHasPermissionToUserOrBot(*c.AppContext.Session(), accessToken.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if err = c.App.DisableUserAccessToken(accessToken); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success - token_id=" + accessToken.Id)
ReturnStatusOK(w)
}
func enableUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJSON(r.Body)
tokenId := props["token_id"]
if tokenId == "" {
c.SetInvalidParam("token_id")
}
auditRec := c.MakeAuditRecord("enableUserAccessToken", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "token_id", tokenId)
c.LogAudit("")
// No separate permission for this action for now
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateUserAccessToken) {
c.SetPermissionError(model.PermissionCreateUserAccessToken)
return
}
accessToken, err := c.App.GetUserAccessToken(tokenId, false)
if err != nil {
c.Err = err
return
}
if user, errGet := c.App.GetUser(accessToken.UserId); errGet == nil {
audit.AddEventParameterAuditable(auditRec, "user", user)
}
if !c.App.SessionHasPermissionToUserOrBot(*c.AppContext.Session(), accessToken.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if err = c.App.EnableUserAccessToken(accessToken); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success - token_id=" + accessToken.Id)
ReturnStatusOK(w)
}
func saveUserTermsOfService(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.StringInterfaceFromJSON(r.Body)
auditRec := c.MakeAuditRecord("saveUserTermsOfService", audit.Fail)
defer c.LogAuditRec(auditRec)
userId := c.AppContext.Session().UserId
termsOfServiceId, ok := props["termsOfServiceId"].(string)
if !ok {
c.SetInvalidParam("termsOfServiceId")
return
}
audit.AddEventParameter(auditRec, "terms_of_service_id", termsOfServiceId)
accepted, ok := props["accepted"].(bool)
if !ok {
c.SetInvalidParam("accepted")
return
}
audit.AddEventParameter(auditRec, "accepted", accepted)
if user, err := c.App.GetUser(userId); err == nil {
audit.AddEventParameterAuditable(auditRec, "user", user)
}
if _, err := c.App.GetTermsOfService(termsOfServiceId); err != nil {
c.Err = err
return
}
if err := c.App.SaveUserTermsOfService(userId, termsOfServiceId, accepted); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("TermsOfServiceId=" + termsOfServiceId + ", accepted=" + strconv.FormatBool(accepted))
ReturnStatusOK(w)
}
func getUserTermsOfService(c *Context, w http.ResponseWriter, r *http.Request) {
userId := c.AppContext.Session().UserId
result, err := c.App.GetUserTermsOfService(userId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(result); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func promoteGuestToUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("promoteGuestToUser", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionPromoteGuest) {
c.SetPermissionError(model.PermissionPromoteGuest)
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(user)
if !user.IsGuest() {
c.Err = model.NewAppError("Api4.promoteGuestToUser", "api.user.promote_guest_to_user.no_guest.app_error", nil, "", http.StatusNotImplemented)
return
}
if err := c.App.PromoteGuestToUser(c.AppContext, user, c.AppContext.Session().UserId); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func demoteUserToGuest(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("Api4.demoteUserToGuest", "api.team.demote_user_to_guest.license.error", nil, "", http.StatusNotImplemented)
return
}
if !*c.App.Config().GuestAccountsSettings.Enable {
c.Err = model.NewAppError("Api4.demoteUserToGuest", "api.team.demote_user_to_guest.disabled.error", nil, "", http.StatusNotImplemented)
return
}
guestEnabled := c.App.Channels().License() != nil && *c.App.Channels().License().Features.GuestAccounts
if !guestEnabled {
c.Err = model.NewAppError("Api4.demoteUserToGuest", "api.team.invite_guests_to_channels.disabled.error", nil, "", http.StatusForbidden)
return
}
auditRec := c.MakeAuditRecord("demoteUserToGuest", audit.Fail)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionDemoteToGuest) {
c.SetPermissionError(model.PermissionDemoteToGuest)
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
if user.IsSystemAdmin() && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
auditRec.AddEventResultState(user)
if user.IsGuest() {
c.Err = model.NewAppError("Api4.demoteUserToGuest", "api.user.demote_user_to_guest.already_guest.app_error", nil, "", http.StatusNotImplemented)
return
}
if err := c.App.DemoteUserToGuest(c.AppContext, user); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func publishUserTyping(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
var typingRequest model.TypingRequest
if jsonErr := json.NewDecoder(r.Body).Decode(&typingRequest); jsonErr != nil {
c.SetInvalidParamWithErr("typing_request", jsonErr)
return
}
if c.Params.UserId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if !c.App.HasPermissionToChannel(c.AppContext, c.Params.UserId, typingRequest.ChannelId, model.PermissionCreatePost) {
c.SetPermissionError(model.PermissionCreatePost)
return
}
if err := c.App.PublishUserTyping(c.Params.UserId, typingRequest.ChannelId, typingRequest.ParentId); err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
func verifyUserEmailWithoutToken(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("verifyUserEmailWithoutToken", audit.Fail)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
defer c.LogAuditRec(auditRec)
auditRec.AddMeta("user_id", user.Id)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if err := c.App.VerifyUserEmail(user.Id, user.Email); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("user verified")
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func convertUserToBot(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
user, appErr := c.App.GetUser(c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
auditRec := c.MakeAuditRecord("convertUserToBot", audit.Fail)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "user", user)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
bot, appErr := c.App.ConvertUserToBot(user)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventPriorState(user)
auditRec.AddEventResultState(bot)
auditRec.AddEventObjectType("bot")
js, err := json.Marshal(bot)
if err != nil {
c.Err = model.NewAppError("convertUserToBot", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(js)
}
func getUploadsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if c.Params.UserId != c.AppContext.Session().UserId {
c.Err = model.NewAppError("getUploadsForUser", "api.user.get_uploads_for_user.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
uss, appErr := c.App.GetUploadSessionsForUser(c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(uss)
if err != nil {
c.Err = model.NewAppError("getUploadsForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getChannelMembersForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
members, err := c.App.GetChannelMembersWithTeamDataForUserWithPagination(c.AppContext, c.Params.UserId, c.Params.Page, c.Params.PerPage)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(members); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func migrateAuthToLDAP(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.StringInterfaceFromJSON(r.Body)
from, ok := props["from"].(string)
if !ok {
c.SetInvalidParam("from")
return
}
if from == "" || (from != "email" && from != "gitlab" && from != "saml" && from != "google" && from != "office365") {
c.SetInvalidParam("from")
return
}
force, ok := props["force"].(bool)
if !ok {
c.SetInvalidParam("force")
return
}
matchField, ok := props["match_field"].(string)
if !ok {
c.SetInvalidParam("match_field")
return
}
auditRec := c.MakeAuditRecord("migrateAuthToLdap", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "from", from)
audit.AddEventParameter(auditRec, "force", force)
audit.AddEventParameter(auditRec, "match_field", matchField)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAP {
c.Err = model.NewAppError("api.migrateAuthToLDAP", "api.admin.ldap.not_available.app_error", nil, "", http.StatusNotImplemented)
return
}
// Email auth in Mattermost system is represented by ""
if from == "email" {
from = ""
}
if migrate := c.App.AccountMigration(); migrate != nil {
if err := migrate.MigrateToLdap(from, matchField, force, false); err != nil {
c.Err = model.NewAppError("api.migrateAuthToLdap", "api.migrate_to_saml.error", nil, err.Error(), http.StatusInternalServerError)
return
}
} else {
c.Err = model.NewAppError("api.migrateAuthToLdap", "api.admin.ldap.not_available.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func migrateAuthToSaml(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.StringInterfaceFromJSON(r.Body)
from, ok := props["from"].(string)
if !ok {
c.SetInvalidParam("from")
return
}
if from == "" || (from != "email" && from != "gitlab" && from != "ldap" && from != "google" && from != "office365") {
c.SetInvalidParam("from")
return
}
auto, ok := props["auto"].(bool)
if !ok {
c.SetInvalidParam("auto")
return
}
matches, ok := props["matches"].(map[string]any)
if !ok {
c.SetInvalidParam("matches")
return
}
usersMap := model.MapFromJSON(strings.NewReader(model.StringInterfaceToJSON(matches)))
auditRec := c.MakeAuditRecord("migrateAuthToSaml", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "from", from)
audit.AddEventParameter(auditRec, "auto", auto)
audit.AddEventParameter(auditRec, "users_map", usersMap)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.SAML {
c.Err = model.NewAppError("api.migrateAuthToSaml", "api.admin.saml.not_available.app_error", nil, "", http.StatusNotImplemented)
return
}
// Email auth in Mattermost system is represented by ""
if from == "email" {
from = ""
}
if migrate := c.App.AccountMigration(); migrate != nil {
if err := migrate.MigrateToSaml(from, usersMap, auto, false); err != nil {
c.Err = model.NewAppError("api.migrateAuthToSaml", "api.migrate_to_saml.error", nil, err.Error(), http.StatusInternalServerError)
return
}
} else {
c.Err = model.NewAppError("api.migrateAuthToSaml", "api.admin.saml.not_available.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getThreadForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId().RequireThreadId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
extendedStr := r.URL.Query().Get("extended")
extended, _ := strconv.ParseBool(extendedStr)
threadMembership, err := c.App.GetThreadMembershipForUser(c.Params.UserId, c.Params.ThreadId)
if err != nil {
c.Err = err
return
}
thread, err := c.App.GetThreadForUser(threadMembership, extended)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(thread); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getThreadsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
options := model.GetUserThreadsOpts{
Since: 0,
Before: "",
After: "",
PageSize: uint64(c.Params.PerPage),
Unread: false,
Extended: false,
Deleted: false,
TotalsOnly: false,
ThreadsOnly: false,
}
sinceString := r.URL.Query().Get("since")
if sinceString != "" {
since, parseError := strconv.ParseUint(sinceString, 10, 64)
if parseError != nil {
c.SetInvalidParam("since")
return
}
options.Since = since
}
options.Before = r.URL.Query().Get("before")
options.After = r.URL.Query().Get("after")
totalsOnlyStr := r.URL.Query().Get("totalsOnly")
threadsOnlyStr := r.URL.Query().Get("threadsOnly")
options.TotalsOnly, _ = strconv.ParseBool(totalsOnlyStr)
options.ThreadsOnly, _ = strconv.ParseBool(threadsOnlyStr)
// parameters are mutually exclusive
if options.Before != "" && options.After != "" {
c.Err = model.NewAppError("api.getThreadsForUser", "api.getThreadsForUser.bad_params", nil, "", http.StatusBadRequest)
return
}
// parameters are mutually exclusive
if options.TotalsOnly && options.ThreadsOnly {
c.Err = model.NewAppError("api.getThreadsForUser", "api.getThreadsForUser.bad_only_params", nil, "", http.StatusBadRequest)
return
}
deletedStr := r.URL.Query().Get("deleted")
unreadStr := r.URL.Query().Get("unread")
extendedStr := r.URL.Query().Get("extended")
options.Deleted, _ = strconv.ParseBool(deletedStr)
options.Unread, _ = strconv.ParseBool(unreadStr)
options.Extended, _ = strconv.ParseBool(extendedStr)
threads, err := c.App.GetThreadsForUser(c.Params.UserId, c.Params.TeamId, options)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(threads); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateReadStateThreadByUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireThreadId().RequireTimestamp().RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("updateReadStateThreadByUser", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
audit.AddEventParameter(auditRec, "thread_id", c.Params.ThreadId)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
audit.AddEventParameter(auditRec, "timestamp", c.Params.Timestamp)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
thread, err := c.App.UpdateThreadReadForUser(c.AppContext, c.AppContext.Session().Id, c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, c.Params.Timestamp)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(thread); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
auditRec.Success()
}
func setUnreadThreadByPostId(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireThreadId().RequirePostId().RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("setUnreadThreadByPostId", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
audit.AddEventParameter(auditRec, "thread_id", c.Params.ThreadId)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
audit.AddEventParameter(auditRec, "post_id", c.Params.PostId)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.ThreadId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
thread, err := c.App.UpdateThreadReadForUserByPost(c.AppContext, c.AppContext.Session().Id, c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, c.Params.PostId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(thread); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
auditRec.Success()
}
func unfollowThreadByUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireThreadId().RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("unfollowThreadByUser", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
audit.AddEventParameter(auditRec, "thread_id", c.Params.ThreadId)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
err := c.App.UpdateThreadFollowForUser(c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, false)
if err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
auditRec.Success()
}
func followThreadByUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireThreadId().RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("followThreadByUser", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
audit.AddEventParameter(auditRec, "thread_id", c.Params.ThreadId)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.ThreadId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
err := c.App.UpdateThreadFollowForUser(c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, true)
if err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
auditRec.Success()
}
func updateReadStateAllThreadsByUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("updateReadStateAllThreadsByUser", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
audit.AddEventParameter(auditRec, "team_id", c.Params.TeamId)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
err := c.App.UpdateThreadsReadForUser(c.Params.UserId, c.Params.TeamId)
if err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
auditRec.Success()
}
func getUsersWithInvalidEmails(c *Context, w http.ResponseWriter, r *http.Request) {
if *c.App.Config().TeamSettings.EnableOpenServer {
c.Err = model.NewAppError("GetUsersWithInvalidEmails", model.NoTranslation, nil, "TeamSettings.EnableOpenServer is enabled", http.StatusBadRequest)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementUsers) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementUsers)
return
}
users, appErr := c.App.GetUsersWithInvalidEmails(c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
err := json.NewEncoder(w).Encode(users)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func getRecentSearches(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
searchParams, err := c.App.GetRecentSearchesForUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(searchParams); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitUserLocal() {
api.BaseRoutes.Users.Handle("", api.APILocal(localGetUsers)).Methods("GET")
api.BaseRoutes.Users.Handle("", api.APILocal(localPermanentDeleteAllUsers)).Methods("DELETE")
api.BaseRoutes.Users.Handle("", api.APILocal(createUser)).Methods("POST")
api.BaseRoutes.Users.Handle("/password/reset/send", api.APILocal(sendPasswordReset)).Methods("POST")
api.BaseRoutes.Users.Handle("/ids", api.APILocal(localGetUsersByIds)).Methods("POST")
api.BaseRoutes.User.Handle("", api.APILocal(localGetUser)).Methods("GET")
api.BaseRoutes.User.Handle("", api.APILocal(updateUser)).Methods("PUT")
api.BaseRoutes.User.Handle("", api.APILocal(localDeleteUser)).Methods("DELETE")
api.BaseRoutes.User.Handle("/roles", api.APILocal(updateUserRoles)).Methods("PUT")
api.BaseRoutes.User.Handle("/mfa", api.APILocal(updateUserMfa)).Methods("PUT")
api.BaseRoutes.User.Handle("/active", api.APILocal(updateUserActive)).Methods("PUT")
api.BaseRoutes.User.Handle("/password", api.APILocal(updatePassword)).Methods("PUT")
api.BaseRoutes.User.Handle("/convert_to_bot", api.APILocal(convertUserToBot)).Methods("POST")
api.BaseRoutes.User.Handle("/email/verify/member", api.APILocal(verifyUserEmailWithoutToken)).Methods("POST")
api.BaseRoutes.User.Handle("/promote", api.APILocal(promoteGuestToUser)).Methods("POST")
api.BaseRoutes.User.Handle("/demote", api.APILocal(demoteUserToGuest)).Methods("POST")
api.BaseRoutes.UserByUsername.Handle("", api.APILocal(localGetUserByUsername)).Methods("GET")
api.BaseRoutes.UserByEmail.Handle("", api.APILocal(localGetUserByEmail)).Methods("GET")
api.BaseRoutes.Users.Handle("/tokens/revoke", api.APILocal(revokeUserAccessToken)).Methods("POST")
api.BaseRoutes.User.Handle("/tokens", api.APILocal(getUserAccessTokensForUser)).Methods("GET")
api.BaseRoutes.User.Handle("/tokens", api.APILocal(createUserAccessToken)).Methods("POST")
api.BaseRoutes.Users.Handle("/migrate_auth/ldap", api.APILocal(migrateAuthToLDAP)).Methods("POST")
api.BaseRoutes.Users.Handle("/migrate_auth/saml", api.APILocal(migrateAuthToSaml)).Methods("POST")
api.BaseRoutes.User.Handle("/uploads", api.APILocal(localGetUploadsForUser)).Methods("GET")
}
func localGetUsers(c *Context, w http.ResponseWriter, r *http.Request) {
inTeamId := r.URL.Query().Get("in_team")
notInTeamId := r.URL.Query().Get("not_in_team")
inChannelId := r.URL.Query().Get("in_channel")
notInChannelId := r.URL.Query().Get("not_in_channel")
groupConstrained := r.URL.Query().Get("group_constrained")
withoutTeam := r.URL.Query().Get("without_team")
active := r.URL.Query().Get("active")
inactive := r.URL.Query().Get("inactive")
role := r.URL.Query().Get("role")
rolesString := r.URL.Query().Get("roles")
channelRolesString := r.URL.Query().Get("channel_roles")
teamRolesString := r.URL.Query().Get("team_roles")
sort := r.URL.Query().Get("sort")
roleNamesAll := []string{}
// MM-47378: validate 'role' related parameters
if role != "" || rolesString != "" || channelRolesString != "" || teamRolesString != "" {
// fetch all role names
rolesAll, err := c.App.GetAllRoles()
if err != nil {
c.Err = model.NewAppError("Api4.getUsers", "api.user.get_users.validation.app_error", nil, "Error fetching roles during validation.", http.StatusBadRequest)
return
}
for _, role := range rolesAll {
roleNamesAll = append(roleNamesAll, role.Name)
}
}
var roles []string
var rolesValid bool
if role != "" {
_, rolesValid = model.CleanRoleNames([]string{role})
if !rolesValid {
c.SetInvalidParam("role")
return
}
roleValid := utils.StringInSlice(role, roleNamesAll)
if !roleValid {
c.SetInvalidParam("role")
return
}
}
if rolesString != "" {
roles, rolesValid = model.CleanRoleNames(strings.Split(rolesString, ","))
if !rolesValid {
c.SetInvalidParam("roles")
return
}
validRoleNames := utils.StringArrayIntersection(roleNamesAll, roles)
if len(validRoleNames) != len(roles) {
c.SetInvalidParam("roles")
return
}
}
var channelRoles []string
if channelRolesString != "" && inChannelId != "" {
channelRoles, rolesValid = model.CleanRoleNames(strings.Split(channelRolesString, ","))
if !rolesValid {
c.SetInvalidParam("channelRoles")
return
}
validRoleNames := utils.StringArrayIntersection(roleNamesAll, channelRoles)
if len(validRoleNames) != len(channelRoles) {
c.SetInvalidParam("channelRoles")
return
}
}
var teamRoles []string
if teamRolesString != "" && inTeamId != "" {
teamRoles, rolesValid = model.CleanRoleNames(strings.Split(teamRolesString, ","))
if !rolesValid {
c.SetInvalidParam("teamRoles")
return
}
validRoleNames := utils.StringArrayIntersection(roleNamesAll, teamRoles)
if len(validRoleNames) != len(teamRoles) {
c.SetInvalidParam("teamRoles")
return
}
}
if notInChannelId != "" && inTeamId == "" {
c.SetInvalidURLParam("team_id")
return
}
if sort != "" && sort != "last_activity_at" && sort != "create_at" && sort != "status" {
c.SetInvalidURLParam("sort")
return
}
// Currently only supports sorting on a team
// or sort="status" on inChannelId
if (sort == "last_activity_at" || sort == "create_at") && (inTeamId == "" || notInTeamId != "" || inChannelId != "" || notInChannelId != "" || withoutTeam != "") {
c.SetInvalidURLParam("sort")
return
}
if sort == "status" && inChannelId == "" {
c.SetInvalidURLParam("sort")
return
}
withoutTeamBool, _ := strconv.ParseBool(withoutTeam)
groupConstrainedBool, _ := strconv.ParseBool(groupConstrained)
activeBool, _ := strconv.ParseBool(active)
inactiveBool, _ := strconv.ParseBool(inactive)
userGetOptions := &model.UserGetOptions{
InTeamId: inTeamId,
InChannelId: inChannelId,
NotInTeamId: notInTeamId,
NotInChannelId: notInChannelId,
GroupConstrained: groupConstrainedBool,
WithoutTeam: withoutTeamBool,
Active: activeBool,
Inactive: inactiveBool,
Role: role,
Sort: sort,
Page: c.Params.Page,
PerPage: c.Params.PerPage,
ViewRestrictions: nil,
}
var (
appErr *model.AppError
profiles []*model.User
etag string
)
if withoutTeamBool, _ := strconv.ParseBool(withoutTeam); withoutTeamBool {
profiles, appErr = c.App.GetUsersWithoutTeamPage(userGetOptions, c.IsSystemAdmin())
} else if notInChannelId != "" {
profiles, appErr = c.App.GetUsersNotInChannelPage(inTeamId, notInChannelId, groupConstrainedBool, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), nil)
} else if notInTeamId != "" {
etag = c.App.GetUsersNotInTeamEtag(inTeamId, "")
if c.HandleEtag(etag, "Get Users Not in Team", w, r) {
return
}
profiles, appErr = c.App.GetUsersNotInTeamPage(notInTeamId, groupConstrainedBool, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), nil)
} else if inTeamId != "" {
if sort == "last_activity_at" {
profiles, appErr = c.App.GetRecentlyActiveUsersForTeamPage(inTeamId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), nil)
} else if sort == "create_at" {
profiles, appErr = c.App.GetNewUsersForTeamPage(inTeamId, c.Params.Page, c.Params.PerPage, c.IsSystemAdmin(), nil)
} else {
etag = c.App.GetUsersInTeamEtag(inTeamId, "")
if c.HandleEtag(etag, "Get Users in Team", w, r) {
return
}
profiles, appErr = c.App.GetUsersInTeamPage(userGetOptions, c.IsSystemAdmin())
}
} else if inChannelId != "" {
if sort == "status" {
profiles, appErr = c.App.GetUsersInChannelPageByStatus(userGetOptions, c.IsSystemAdmin())
} else {
profiles, appErr = c.App.GetUsersInChannelPage(userGetOptions, c.IsSystemAdmin())
}
} else {
profiles, appErr = c.App.GetUsersPage(userGetOptions, c.IsSystemAdmin())
}
if appErr != nil {
c.Err = appErr
return
}
if etag != "" {
w.Header().Set(model.HeaderEtagServer, etag)
}
js, err := json.Marshal(profiles)
if err != nil {
c.Err = model.NewAppError("localGetUsers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func localGetUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
userIds := model.ArrayFromJSON(r.Body)
if len(userIds) == 0 {
c.SetInvalidParam("user_ids")
return
}
sinceString := r.URL.Query().Get("since")
options := &store.UserGetByIdsOpts{
IsAdmin: c.IsSystemAdmin(),
}
if sinceString != "" {
since, err := strconv.ParseInt(sinceString, 10, 64)
if err != nil {
c.SetInvalidParamWithErr("since", err)
return
}
options.Since = since
}
users, appErr := c.App.GetUsersByIds(userIds, options)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(users)
if err != nil {
c.Err = model.NewAppError("localGetUsersByIds", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func localGetUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
userTermsOfService, err := c.App.GetUserTermsOfService(user.Id)
if err != nil && err.StatusCode != http.StatusNotFound {
c.Err = err
return
}
if userTermsOfService != nil {
user.TermsOfServiceId = userTermsOfService.TermsOfServiceId
user.TermsOfServiceCreateAt = userTermsOfService.CreateAt
}
etag := user.Etag(*c.App.Config().PrivacySettings.ShowFullName, *c.App.Config().PrivacySettings.ShowEmailAddress)
if c.HandleEtag(etag, "Get User", w, r) {
return
}
c.App.SanitizeProfile(user, c.IsSystemAdmin())
w.Header().Set(model.HeaderEtagServer, etag)
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localDeleteUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
userId := c.Params.UserId
auditRec := c.MakeAuditRecord("localDeleteUser", audit.Fail)
defer c.LogAuditRec(auditRec)
user, err := c.App.GetUser(userId)
if err != nil {
c.Err = err
return
}
audit.AddEventParameter(auditRec, "user_id", c.Params.UserId)
auditRec.AddEventPriorState(user)
auditRec.AddEventObjectType("user")
if c.Params.Permanent {
err = c.App.PermanentDeleteUser(c.AppContext, user)
} else {
_, err = c.App.UpdateActive(c.AppContext, user, false)
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func localPermanentDeleteAllUsers(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("localPermanentDeleteAllUsers", audit.Fail)
defer c.LogAuditRec(auditRec)
if err := c.App.PermanentDeleteAllUsers(c.AppContext); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func localGetUserByUsername(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUsername()
if c.Err != nil {
return
}
user, err := c.App.GetUserByUsername(c.Params.Username)
if err != nil {
c.Err = err
return
}
userTermsOfService, err := c.App.GetUserTermsOfService(user.Id)
if err != nil && err.StatusCode != http.StatusNotFound {
c.Err = err
return
}
if userTermsOfService != nil {
user.TermsOfServiceId = userTermsOfService.TermsOfServiceId
user.TermsOfServiceCreateAt = userTermsOfService.CreateAt
}
etag := user.Etag(*c.App.Config().PrivacySettings.ShowFullName, *c.App.Config().PrivacySettings.ShowEmailAddress)
if c.HandleEtag(etag, "Get User", w, r) {
return
}
c.App.SanitizeProfile(user, c.IsSystemAdmin())
w.Header().Set(model.HeaderEtagServer, etag)
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localGetUserByEmail(c *Context, w http.ResponseWriter, r *http.Request) {
c.SanitizeEmail()
if c.Err != nil {
return
}
sanitizeOptions := c.App.GetSanitizeOptions(c.IsSystemAdmin())
if !sanitizeOptions["email"] {
c.Err = model.NewAppError("getUserByEmail", "api.user.get_user_by_email.permissions.app_error", nil, "userId="+c.AppContext.Session().UserId, http.StatusForbidden)
return
}
user, err := c.App.GetUserByEmail(c.Params.Email)
if err != nil {
c.Err = err
return
}
etag := user.Etag(*c.App.Config().PrivacySettings.ShowFullName, *c.App.Config().PrivacySettings.ShowEmailAddress)
if c.HandleEtag(etag, "Get User", w, r) {
return
}
c.App.SanitizeProfile(user, c.IsSystemAdmin())
w.Header().Set(model.HeaderEtagServer, etag)
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localGetUploadsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
uss, appErr := c.App.GetUploadSessionsForUser(c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(uss)
if err != nil {
c.Err = model.NewAppError("localGetUploadsForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitWebhook() {
api.BaseRoutes.IncomingHooks.Handle("", api.APISessionRequired(createIncomingHook)).Methods("POST")
api.BaseRoutes.IncomingHooks.Handle("", api.APISessionRequired(getIncomingHooks)).Methods("GET")
api.BaseRoutes.IncomingHook.Handle("", api.APISessionRequired(getIncomingHook)).Methods("GET")
api.BaseRoutes.IncomingHook.Handle("", api.APISessionRequired(updateIncomingHook)).Methods("PUT")
api.BaseRoutes.IncomingHook.Handle("", api.APISessionRequired(deleteIncomingHook)).Methods("DELETE")
api.BaseRoutes.OutgoingHooks.Handle("", api.APISessionRequired(createOutgoingHook)).Methods("POST")
api.BaseRoutes.OutgoingHooks.Handle("", api.APISessionRequired(getOutgoingHooks)).Methods("GET")
api.BaseRoutes.OutgoingHook.Handle("", api.APISessionRequired(getOutgoingHook)).Methods("GET")
api.BaseRoutes.OutgoingHook.Handle("", api.APISessionRequired(updateOutgoingHook)).Methods("PUT")
api.BaseRoutes.OutgoingHook.Handle("", api.APISessionRequired(deleteOutgoingHook)).Methods("DELETE")
api.BaseRoutes.OutgoingHook.Handle("/regen_token", api.APISessionRequired(regenOutgoingHookToken)).Methods("POST")
}
func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
var hook model.IncomingWebhook
if jsonErr := json.NewDecoder(r.Body).Decode(&hook); jsonErr != nil {
c.SetInvalidParamWithErr("incoming_webhook", jsonErr)
return
}
channel, err := c.App.GetChannel(c.AppContext, hook.ChannelId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("createIncomingHook", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "incoming_webhook", &hook)
audit.AddEventParameterAuditable(auditRec, "channel", channel)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionManageIncomingWebhooks) {
c.SetPermissionError(model.PermissionManageIncomingWebhooks)
return
}
if channel.Type != model.ChannelTypeOpen && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionReadChannel) {
c.LogAudit("fail - bad channel permissions")
c.SetPermissionError(model.PermissionReadChannel)
return
}
userId := c.AppContext.Session().UserId
if hook.UserId != "" && hook.UserId != userId {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionManageOthersIncomingWebhooks) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersIncomingWebhooks)
return
}
if _, err = c.App.GetUser(hook.UserId); err != nil {
c.Err = err
return
}
userId = hook.UserId
}
incomingHook, err := c.App.CreateIncomingWebhookForChannel(userId, channel, &hook)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(incomingHook)
auditRec.AddEventObjectType("hook")
c.LogAudit("success")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(incomingHook); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireHookId()
if c.Err != nil {
return
}
var updatedHook model.IncomingWebhook
if jsonErr := json.NewDecoder(r.Body).Decode(&updatedHook); jsonErr != nil {
c.SetInvalidParamWithErr("incoming_webhook", jsonErr)
return
}
// The hook being updated in the payload must be the same one as indicated in the URL.
if updatedHook.Id != c.Params.HookId {
c.SetInvalidParam("hook_id")
return
}
auditRec := c.MakeAuditRecord("updateIncomingHook", audit.Fail)
audit.AddEventParameter(auditRec, "hook_id", c.Params.HookId)
audit.AddEventParameterAuditable(auditRec, "updated_hook", &updatedHook)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
oldHook, err := c.App.GetIncomingWebhook(c.Params.HookId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(oldHook)
auditRec.AddEventObjectType("incoming_webhook")
if updatedHook.TeamId == "" {
updatedHook.TeamId = oldHook.TeamId
}
if updatedHook.TeamId != oldHook.TeamId {
c.Err = model.NewAppError("updateIncomingHook", "api.webhook.team_mismatch.app_error", nil, "user_id="+c.AppContext.Session().UserId, http.StatusBadRequest)
return
}
channel, err := c.App.GetChannel(c.AppContext, updatedHook.ChannelId)
if err != nil {
c.Err = err
return
}
auditRec.AddMeta("channel_id", channel.Id)
auditRec.AddMeta("channel_name", channel.Name)
if channel.TeamId != updatedHook.TeamId {
c.SetInvalidParam("channel_id")
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionManageIncomingWebhooks) {
c.SetPermissionError(model.PermissionManageIncomingWebhooks)
return
}
if c.AppContext.Session().UserId != oldHook.UserId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionManageOthersIncomingWebhooks) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersIncomingWebhooks)
return
}
if channel.Type != model.ChannelTypeOpen && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionReadChannel) {
c.LogAudit("fail - bad channel permissions")
c.SetPermissionError(model.PermissionReadChannel)
return
}
incomingHook, err := c.App.UpdateIncomingWebhook(oldHook, &updatedHook)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(incomingHook)
auditRec.Success()
c.LogAudit("success")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(incomingHook); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
var (
teamID = r.URL.Query().Get("team_id")
userID = c.AppContext.Session().UserId
hooks []*model.IncomingWebhook
appErr *model.AppError
)
if teamID != "" {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionManageIncomingWebhooks) {
c.SetPermissionError(model.PermissionManageIncomingWebhooks)
return
}
// Remove userId as a filter if they have permission to manage others.
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionManageOthersIncomingWebhooks) {
userID = ""
}
hooks, appErr = c.App.GetIncomingWebhooksForTeamPageByUser(teamID, userID, c.Params.Page, c.Params.PerPage)
} else {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageIncomingWebhooks) {
c.SetPermissionError(model.PermissionManageIncomingWebhooks)
return
}
// Remove userId as a filter if they have permission to manage others.
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOthersIncomingWebhooks) {
userID = ""
}
hooks, appErr = c.App.GetIncomingWebhooksPageByUser(userID, c.Params.Page, c.Params.PerPage)
}
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(hooks)
if err != nil {
c.Err = model.NewAppError("getIncomingHooks", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireHookId()
if c.Err != nil {
return
}
hookId := c.Params.HookId
var err *model.AppError
var hook *model.IncomingWebhook
var channel *model.Channel
hook, err = c.App.GetIncomingWebhook(hookId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("getIncomingHook", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "hook_id", c.Params.HookId)
auditRec.AddMeta("hook_id", hook.Id)
auditRec.AddMeta("hook_display", hook.DisplayName)
auditRec.AddMeta("channel_id", hook.ChannelId)
auditRec.AddMeta("team_id", hook.TeamId)
c.LogAudit("attempt")
channel, err = c.App.GetChannel(c.AppContext, hook.ChannelId)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageIncomingWebhooks) ||
(channel.Type != model.ChannelTypeOpen && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), hook.ChannelId, model.PermissionReadChannel)) {
c.LogAudit("fail - bad permissions")
c.SetPermissionError(model.PermissionManageIncomingWebhooks)
return
}
if c.AppContext.Session().UserId != hook.UserId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOthersIncomingWebhooks) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersIncomingWebhooks)
return
}
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(hook); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireHookId()
if c.Err != nil {
return
}
hookId := c.Params.HookId
var err *model.AppError
var hook *model.IncomingWebhook
var channel *model.Channel
hook, err = c.App.GetIncomingWebhook(hookId)
if err != nil {
c.Err = err
return
}
channel, err = c.App.GetChannel(c.AppContext, hook.ChannelId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("deleteIncomingHook", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "hook_id", c.Params.HookId)
auditRec.AddMeta("hook_id", hook.Id)
auditRec.AddMeta("hook_display", hook.DisplayName)
auditRec.AddMeta("channel_id", channel.Id)
auditRec.AddMeta("channel_name", channel.Name)
auditRec.AddMeta("team_id", hook.TeamId)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageIncomingWebhooks) ||
(channel.Type != model.ChannelTypeOpen && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), hook.ChannelId, model.PermissionReadChannel)) {
c.LogAudit("fail - bad permissions")
c.SetPermissionError(model.PermissionManageIncomingWebhooks)
return
}
if c.AppContext.Session().UserId != hook.UserId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOthersIncomingWebhooks) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersIncomingWebhooks)
return
}
if err = c.App.DeleteIncomingWebhook(hookId); err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(hook)
auditRec.AddEventObjectType("incoming_webhook")
auditRec.Success()
ReturnStatusOK(w)
}
func updateOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireHookId()
if c.Err != nil {
return
}
var updatedHook model.OutgoingWebhook
if jsonErr := json.NewDecoder(r.Body).Decode(&updatedHook); jsonErr != nil {
c.SetInvalidParamWithErr("outgoing_webhook", jsonErr)
return
}
// The hook being updated in the payload must be the same one as indicated in the URL.
if updatedHook.Id != c.Params.HookId {
c.SetInvalidParam("hook_id")
return
}
auditRec := c.MakeAuditRecord("updateOutgoingHook", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "updated_hook", &updatedHook)
c.LogAudit("attempt")
oldHook, err := c.App.GetOutgoingWebhook(c.Params.HookId)
if err != nil {
c.Err = err
return
}
if updatedHook.TeamId == "" {
updatedHook.TeamId = oldHook.TeamId
}
if updatedHook.TeamId != oldHook.TeamId {
c.Err = model.NewAppError("updateOutgoingHook", "api.webhook.team_mismatch.app_error", nil, "user_id="+c.AppContext.Session().UserId, http.StatusBadRequest)
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), updatedHook.TeamId, model.PermissionManageOutgoingWebhooks) {
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
return
}
if c.AppContext.Session().UserId != oldHook.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), updatedHook.TeamId, model.PermissionManageOthersOutgoingWebhooks) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersOutgoingWebhooks)
return
}
updatedHook.CreatorId = c.AppContext.Session().UserId
rhook, err := c.App.UpdateOutgoingWebhook(c.AppContext, oldHook, &updatedHook)
if err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(rhook); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
var hook model.OutgoingWebhook
if jsonErr := json.NewDecoder(r.Body).Decode(&hook); jsonErr != nil {
c.SetInvalidParamWithErr("outgoing_webhook", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createOutgoingHook", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "hook", &hook)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOutgoingWebhooks) {
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
return
}
if hook.CreatorId == "" {
hook.CreatorId = c.AppContext.Session().UserId
} else {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOthersOutgoingWebhooks) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersOutgoingWebhooks)
return
}
_, err := c.App.GetUser(hook.CreatorId)
if err != nil {
c.Err = err
return
}
}
rhook, err := c.App.CreateOutgoingWebhook(&hook)
if err != nil {
c.LogAudit("fail")
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(rhook)
auditRec.AddEventObjectType("outgoing_webhook")
c.LogAudit("success")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rhook); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getOutgoingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
var (
query = r.URL.Query()
channelID = query.Get("channel_id")
teamID = query.Get("team_id")
userID = c.AppContext.Session().UserId
hooks []*model.OutgoingWebhook
appErr *model.AppError
)
if channelID != "" {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelID, model.PermissionManageOutgoingWebhooks) {
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
return
}
// Remove userId as a filter if they have permission to manage others.
if c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelID, model.PermissionManageOthersOutgoingWebhooks) {
userID = ""
}
hooks, appErr = c.App.GetOutgoingWebhooksForChannelPageByUser(channelID, userID, c.Params.Page, c.Params.PerPage)
} else if teamID != "" {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionManageOutgoingWebhooks) {
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
return
}
// Remove userId as a filter if they have permission to manage others.
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionManageOthersOutgoingWebhooks) {
userID = ""
}
hooks, appErr = c.App.GetOutgoingWebhooksForTeamPageByUser(teamID, userID, c.Params.Page, c.Params.PerPage)
} else {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOutgoingWebhooks) {
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
return
}
// Remove userId as a filter if they have permission to manage others.
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOthersOutgoingWebhooks) {
userID = ""
}
hooks, appErr = c.App.GetOutgoingWebhooksPageByUser(userID, c.Params.Page, c.Params.PerPage)
}
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(hooks)
if err != nil {
c.Err = model.NewAppError("getOutgoingHooks", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireHookId()
if c.Err != nil {
return
}
hook, err := c.App.GetOutgoingWebhook(c.Params.HookId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("getOutgoingHook", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "hook_id", c.Params.HookId)
auditRec.AddMeta("hook_id", hook.Id)
auditRec.AddMeta("hook_display", hook.DisplayName)
auditRec.AddMeta("channel_id", hook.ChannelId)
auditRec.AddMeta("team_id", hook.TeamId)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOutgoingWebhooks) {
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
return
}
if c.AppContext.Session().UserId != hook.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOthersOutgoingWebhooks) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersOutgoingWebhooks)
return
}
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(hook); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireHookId()
if c.Err != nil {
return
}
hook, err := c.App.GetOutgoingWebhook(c.Params.HookId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("regenOutgoingHookToken", audit.Fail)
defer c.LogAuditRec(auditRec)
auditRec.AddMeta("hook_id", hook.Id)
auditRec.AddMeta("hook_display", hook.DisplayName)
auditRec.AddMeta("channel_id", hook.ChannelId)
auditRec.AddMeta("team_id", hook.TeamId)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOutgoingWebhooks) {
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
return
}
if c.AppContext.Session().UserId != hook.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOthersOutgoingWebhooks) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersOutgoingWebhooks)
return
}
rhook, err := c.App.RegenOutgoingWebhookToken(hook)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(rhook)
auditRec.AddEventObjectType("outgoing_webhook")
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(rhook); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireHookId()
if c.Err != nil {
return
}
hook, err := c.App.GetOutgoingWebhook(c.Params.HookId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("deleteOutgoingHook", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "hook_id", c.Params.HookId)
auditRec.AddMeta("hook_id", hook.Id)
auditRec.AddMeta("hook_display", hook.DisplayName)
auditRec.AddMeta("channel_id", hook.ChannelId)
auditRec.AddMeta("team_id", hook.TeamId)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOutgoingWebhooks) {
c.SetPermissionError(model.PermissionManageOutgoingWebhooks)
return
}
if c.AppContext.Session().UserId != hook.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), hook.TeamId, model.PermissionManageOthersOutgoingWebhooks) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersOutgoingWebhooks)
return
}
if err := c.App.DeleteOutgoingWebhook(hook.Id); err != nil {
c.LogAudit("fail")
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitWebhookLocal() {
api.BaseRoutes.IncomingHooks.Handle("", api.APILocal(localCreateIncomingHook)).Methods("POST")
api.BaseRoutes.IncomingHooks.Handle("", api.APILocal(getIncomingHooks)).Methods("GET")
api.BaseRoutes.IncomingHook.Handle("", api.APILocal(getIncomingHook)).Methods("GET")
api.BaseRoutes.IncomingHook.Handle("", api.APILocal(updateIncomingHook)).Methods("PUT")
api.BaseRoutes.IncomingHook.Handle("", api.APILocal(deleteIncomingHook)).Methods("DELETE")
api.BaseRoutes.OutgoingHooks.Handle("", api.APILocal(localCreateOutgoingHook)).Methods("POST")
api.BaseRoutes.OutgoingHooks.Handle("", api.APILocal(getOutgoingHooks)).Methods("GET")
api.BaseRoutes.OutgoingHook.Handle("", api.APILocal(getOutgoingHook)).Methods("GET")
api.BaseRoutes.OutgoingHook.Handle("", api.APILocal(updateOutgoingHook)).Methods("PUT")
api.BaseRoutes.OutgoingHook.Handle("", api.APILocal(deleteOutgoingHook)).Methods("DELETE")
}
func localCreateIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
var hook model.IncomingWebhook
if jsonErr := json.NewDecoder(r.Body).Decode(&hook); jsonErr != nil {
c.SetInvalidParamWithErr("incoming_webhook", jsonErr)
return
}
if hook.UserId == "" {
c.SetInvalidParam("user_id")
return
}
channel, err := c.App.GetChannel(c.AppContext, hook.ChannelId)
if err != nil {
c.Err = err
return
}
if _, err = c.App.GetUser(hook.UserId); err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("localCreateIncomingHook", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "hook", &hook)
audit.AddEventParameterAuditable(auditRec, "channel", channel)
c.LogAudit("attempt")
incomingHook, err := c.App.CreateIncomingWebhookForChannel(hook.UserId, channel, &hook)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(incomingHook)
auditRec.AddEventObjectType("incoming_webhook")
c.LogAudit("success")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(incomingHook); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localCreateOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
var hook model.OutgoingWebhook
if jsonErr := json.NewDecoder(r.Body).Decode(&hook); jsonErr != nil {
c.SetInvalidParamWithErr("outgoing_webhook", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createOutgoingHook", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "hook", &hook)
c.LogAudit("attempt")
if hook.CreatorId == "" {
c.SetInvalidParam("creator_id")
return
}
_, err := c.App.GetUser(hook.CreatorId)
if err != nil {
c.Err = err
return
}
rhook, err := c.App.CreateOutgoingWebhook(&hook)
if err != nil {
c.LogAudit("fail")
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(rhook)
auditRec.AddEventObjectType("outgoing_webhook")
c.LogAudit("success")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rhook); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"github.com/gorilla/websocket"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
connectionIDParam = "connection_id"
sequenceNumberParam = "sequence_number"
)
func (api *API) InitWebSocket() {
// Optionally supports a trailing slash
api.BaseRoutes.APIRoot.Handle("/{websocket:websocket(?:\\/)?}", api.APIHandlerTrustRequester(connectWebSocket)).Methods("GET")
}
func connectWebSocket(c *Context, w http.ResponseWriter, r *http.Request) {
upgrader := websocket.Upgrader{
ReadBufferSize: model.SocketMaxMessageSizeKb,
WriteBufferSize: model.SocketMaxMessageSizeKb,
CheckOrigin: c.App.OriginChecker(),
}
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
c.Err = model.NewAppError("connect", "api.web_socket.connect.upgrade.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
// We initialize webconn with all the necessary data.
// If the queues are empty, they are initialized in the constructor.
cfg := &platform.WebConnConfig{
WebSocket: ws,
Session: *c.AppContext.Session(),
TFunc: c.AppContext.T,
Locale: "",
Active: true,
}
cfg.ConnectionID = r.URL.Query().Get(connectionIDParam)
if cfg.ConnectionID == "" || c.AppContext.Session().UserId == "" {
// If not present, we assume client is not capable yet, or it's a fresh connection.
// We just create a new ID.
cfg.ConnectionID = model.NewId()
// In case of fresh connection id, sequence number is already zero.
} else {
cfg, err = c.App.Srv().Platform().PopulateWebConnConfig(c.AppContext.Session(), cfg, r.URL.Query().Get(sequenceNumberParam))
if err != nil {
mlog.Warn("Error while populating webconn config", mlog.String("id", r.URL.Query().Get(connectionIDParam)), mlog.Err(err))
ws.Close()
return
}
}
wc := c.App.Srv().Platform().NewWebConn(cfg, c.App, c.App.Srv().Channels())
if c.AppContext.Session().UserId != "" {
c.App.Srv().Platform().HubRegister(wc)
}
wc.Pump()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/worktemplates"
)
func (api *API) InitWorkTemplate() {
api.BaseRoutes.WorkTemplates.Handle("/categories", api.APISessionRequired(getWorkTemplateCategories)).Methods("GET")
api.BaseRoutes.WorkTemplates.Handle("/categories/{category}/templates", api.APISessionRequired(getWorkTemplates)).Methods("GET")
api.BaseRoutes.WorkTemplates.Handle("/execute", api.APIHandler(executeWorkTemplate)).Methods("POST")
}
func areWorkTemplatesEnabled(c *Context) *model.AppError {
if !c.App.Config().FeatureFlags.WorkTemplate {
return model.NewAppError("areWorkTemplatesEnabled", "api.work_templates.disabled", nil, "feature flag is off", http.StatusNotFound)
}
// we have to make sure that playbooks plugin is enabled and board is a product
pbActive, err := c.App.IsPluginActive(model.PluginIdPlaybooks)
if err != nil {
return model.NewAppError("areWorkTemplatesEnabled", "api.work_templates.disabled", nil, "", http.StatusInternalServerError).Wrap(err)
}
if !pbActive {
return model.NewAppError("areWorkTemplatesEnabled", "api.work_templates.disabled", nil, "playbook plugin not active", http.StatusNotFound)
}
hasBoard, err := c.App.HasBoardProduct()
if err != nil {
return model.NewAppError("areWorkTemplatesEnabled", "api.work_templates.disabled", nil, "", http.StatusInternalServerError).Wrap(err)
}
if !hasBoard {
return model.NewAppError("areWorkTemplatesEnabled", "api.work_templates.disabled", nil, "board product not found", http.StatusNotFound)
}
return nil
}
func getWorkTemplateCategories(c *Context, w http.ResponseWriter, r *http.Request) {
appErr := areWorkTemplatesEnabled(c)
if appErr != nil {
c.Err = appErr
return
}
t := c.AppContext.GetT()
categories, appErr := c.App.GetWorkTemplateCategories(t)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(categories)
if err != nil {
c.Err = model.NewAppError("getWorkTemplateCategories", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func getWorkTemplates(c *Context, w http.ResponseWriter, r *http.Request) {
appErr := areWorkTemplatesEnabled(c)
if appErr != nil {
c.Err = appErr
return
}
c.RequireCategory()
if c.Err != nil {
return
}
t := c.AppContext.GetT()
workTemplates, appErr := c.App.GetWorkTemplates(c.Params.Category, c.App.Config().FeatureFlags.ToMap(), t)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(workTemplates)
if err != nil {
c.Err = model.NewAppError("getWorkTemplates", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func executeWorkTemplate(c *Context, w http.ResponseWriter, r *http.Request) {
appErr := areWorkTemplatesEnabled(c)
if appErr != nil {
c.Err = appErr
return
}
wtcr := &worktemplates.ExecutionRequest{}
err := json.NewDecoder(r.Body).Decode(wtcr)
if err != nil {
c.Err = model.NewAppError("executeWorkTemplate", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
canCreatePublicChannel := c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), wtcr.TeamID, model.PermissionCreatePublicChannel)
canCreatePrivateChannel := c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), wtcr.TeamID, model.PermissionCreatePrivateChannel)
// focalboard uses channel permissions for board creation
canCreatePublicBoard := canCreatePublicChannel
canCreatePrivateBoard := canCreatePrivateChannel
canCreatePublicPlaybook := c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), wtcr.TeamID, model.PermissionPublicPlaybookCreate)
canCreatePrivatePlaybook := c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), wtcr.TeamID, model.PermissionPrivatePlaybookCreate)
appErr = wtcr.CanBeExecuted(worktemplates.PermissionSet{
License: c.App.License(),
CanCreatePublicChannel: canCreatePublicChannel,
CanCreatePrivateChannel: canCreatePrivateChannel,
CanCreatePublicBoard: canCreatePublicBoard,
CanCreatePrivateBoard: canCreatePrivateBoard,
CanCreatePublicPlaybook: canCreatePublicPlaybook,
CanCreatePrivatePlaybook: canCreatePrivatePlaybook,
})
if appErr != nil {
c.Err = appErr
return
}
canInstallPlugin := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins)
if !*c.App.Config().PluginSettings.Enable || !*c.App.Config().PluginSettings.EnableMarketplace || *c.App.Config().PluginSettings.MarketplaceURL != model.PluginSettingsDefaultMarketplaceURL {
canInstallPlugin = false
}
res, appErr := c.App.ExecuteWorkTemplate(c.AppContext, wtcr, canInstallPlugin)
if appErr != nil {
c.Err = appErr
return
}
err = json.NewEncoder(w).Encode(res)
if err != nil {
c.Err = model.NewAppError("executeWorkTemplate", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/cache"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mail"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var latestVersionCache = cache.NewLRU(cache.LRUOptions{
Size: 1,
})
func (s *Server) GetLogs(page, perPage int) ([]string, *model.AppError) {
var lines []string
license := s.License()
if license != nil && *license.Features.Cluster && s.platform.Cluster() != nil && *s.platform.Config().ClusterSettings.Enable {
if info := s.platform.Cluster().GetMyClusterInfo(); info != nil {
lines = append(lines, "-----------------------------------------------------------------------------------------------------------")
lines = append(lines, "-----------------------------------------------------------------------------------------------------------")
lines = append(lines, info.Hostname)
lines = append(lines, "-----------------------------------------------------------------------------------------------------------")
lines = append(lines, "-----------------------------------------------------------------------------------------------------------")
} else {
mlog.Error("Could not get cluster info")
}
}
melines, err := s.GetLogsSkipSend(page, perPage, &model.LogFilter{})
if err != nil {
return nil, err
}
lines = append(lines, melines...)
if s.platform.Cluster() != nil && *s.platform.Config().ClusterSettings.Enable {
clines, err := s.platform.Cluster().GetLogs(page, perPage)
if err != nil {
return nil, err
}
lines = append(lines, clines...)
}
return lines, nil
}
func (s *Server) QueryLogs(page, perPage int, logFilter *model.LogFilter) (map[string][]string, *model.AppError) {
logData := make(map[string][]string)
serverName := "default"
license := s.License()
if license != nil && *license.Features.Cluster && s.platform.Cluster() != nil && *s.platform.Config().ClusterSettings.Enable {
if info := s.platform.Cluster().GetMyClusterInfo(); info != nil {
serverName = info.Hostname
} else {
mlog.Error("Could not get cluster info")
}
}
serverNames := logFilter.ServerNames
if len(serverNames) > 0 {
for _, nodeName := range serverNames {
if nodeName == "default" {
AddLocalLogs(logData, s, page, perPage, nodeName, logFilter)
}
}
} else {
AddLocalLogs(logData, s, page, perPage, serverName, logFilter)
}
if s.platform.Cluster() != nil && *s.Config().ClusterSettings.Enable {
clusterLogs, err := s.platform.Cluster().QueryLogs(page, perPage)
if err != nil {
return nil, err
}
if clusterLogs != nil && len(serverNames) > 0 {
for _, filteredNodeName := range serverNames {
logData[filteredNodeName] = clusterLogs[filteredNodeName]
}
} else {
for nodeName, logs := range clusterLogs {
logData[nodeName] = logs
}
}
}
return logData, nil
}
func AddLocalLogs(logData map[string][]string, s *Server, page, perPage int, serverName string, logFilter *model.LogFilter) *model.AppError {
currentServerLogs, err := s.GetLogsSkipSend(page, perPage, logFilter)
if err != nil {
return err
}
logData[serverName] = currentServerLogs
return nil
}
func (a *App) QueryLogs(page, perPage int, logFilter *model.LogFilter) (map[string][]string, *model.AppError) {
return a.Srv().QueryLogs(page, perPage, logFilter)
}
func (a *App) GetLogs(page, perPage int) ([]string, *model.AppError) {
return a.Srv().GetLogs(page, perPage)
}
func (s *Server) GetLogsSkipSend(page, perPage int, logFilter *model.LogFilter) ([]string, *model.AppError) {
return s.platform.GetLogsSkipSend(page, perPage, logFilter)
}
func (a *App) GetLogsSkipSend(page, perPage int, logFilter *model.LogFilter) ([]string, *model.AppError) {
return a.Srv().GetLogsSkipSend(page, perPage, logFilter)
}
func (a *App) GetClusterStatus() []*model.ClusterInfo {
infos := make([]*model.ClusterInfo, 0)
if a.Cluster() != nil {
infos = a.Cluster().GetClusterInfos()
}
return infos
}
func (s *Server) InvalidateAllCaches() *model.AppError {
return s.platform.InvalidateAllCaches()
}
func (s *Server) InvalidateAllCachesSkipSend() {
s.platform.InvalidateAllCachesSkipSend()
}
func (a *App) RecycleDatabaseConnection() {
mlog.Info("Attempting to recycle database connections.")
// This works by setting 10 seconds as the max conn lifetime for all DB connections.
// This allows in gradually closing connections as they expire. In future, we can think
// of exposing this as a param from the REST api.
a.Srv().Store().RecycleDBConnections(10 * time.Second)
mlog.Info("Finished recycling database connections.")
}
func (a *App) TestSiteURL(siteURL string) *model.AppError {
url := fmt.Sprintf("%s/api/v4/system/ping", siteURL)
res, err := http.Get(url)
if err != nil || res.StatusCode != 200 {
return model.NewAppError("testSiteURL", "app.admin.test_site_url.failure", nil, "", http.StatusBadRequest)
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
return nil
}
func (a *App) TestEmail(userID string, cfg *model.Config) *model.AppError {
if *cfg.EmailSettings.SMTPServer == "" {
return model.NewAppError("testEmail", "api.admin.test_email.missing_server", nil, i18n.T("api.context.invalid_param.app_error", map[string]any{"Name": "SMTPServer"}), http.StatusBadRequest)
}
// if the user hasn't changed their email settings, fill in the actual SMTP password so that
// the user can verify an existing SMTP connection
if *cfg.EmailSettings.SMTPPassword == model.FakeSetting {
if *cfg.EmailSettings.SMTPServer == *a.Config().EmailSettings.SMTPServer &&
*cfg.EmailSettings.SMTPPort == *a.Config().EmailSettings.SMTPPort &&
*cfg.EmailSettings.SMTPUsername == *a.Config().EmailSettings.SMTPUsername {
*cfg.EmailSettings.SMTPPassword = *a.Config().EmailSettings.SMTPPassword
} else {
return model.NewAppError("testEmail", "api.admin.test_email.reenter_password", nil, "", http.StatusBadRequest)
}
}
user, err := a.GetUser(userID)
if err != nil {
return err
}
T := i18n.GetUserTranslations(user.Locale)
license := a.Srv().License()
mailConfig := a.Srv().MailServiceConfig()
if err := mail.SendMailUsingConfig(user.Email, T("api.admin.test_email.subject"), T("api.admin.test_email.body"), mailConfig, license != nil && *license.Features.Compliance, "", "", "", "", ""); err != nil {
return model.NewAppError("testEmail", "app.admin.test_email.failure", map[string]any{"Error": err.Error()}, "", http.StatusInternalServerError)
}
return nil
}
func (a *App) GetLatestVersion(latestVersionUrl string) (*model.GithubReleaseInfo, *model.AppError) {
var cachedLatestVersion *model.GithubReleaseInfo
if cacheErr := latestVersionCache.Get("latest_version_cache", &cachedLatestVersion); cacheErr == nil {
return cachedLatestVersion, nil
}
res, err := http.Get(latestVersionUrl)
if err != nil {
return nil, model.NewAppError("GetLatestVersion", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
defer res.Body.Close()
responseData, err := io.ReadAll(res.Body)
if err != nil {
return nil, model.NewAppError("GetLatestVersion", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
var releaseInfoResponse *model.GithubReleaseInfo
err = json.Unmarshal(responseData, &releaseInfoResponse)
if err != nil {
return nil, model.NewAppError("GetLatestVersion", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
if validErr := releaseInfoResponse.IsValid(); validErr != nil {
return nil, model.NewAppError("GetLatestVersion", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(validErr)
}
err = latestVersionCache.Set("latest_version_cache", releaseInfoResponse)
if err != nil {
return nil, model.NewAppError("GetLatestVersion", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return releaseInfoResponse, nil
}
func (a *App) ClearLatestVersionCache() {
latestVersionCache.Remove("latest_version_cache")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mail"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) GetWarnMetricsStatus() (map[string]*model.WarnMetricStatus, *model.AppError) {
systemDataList, nErr := a.Srv().Store().System().Get()
if nErr != nil {
return nil, model.NewAppError("GetWarnMetricsStatus", "app.system.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
isE0Edition := model.BuildEnterpriseReady == "true" // license == nil was already validated upstream
result := map[string]*model.WarnMetricStatus{}
for key, value := range systemDataList {
if strings.HasPrefix(key, model.WarnMetricStatusStorePrefix) {
if warnMetric, ok := model.WarnMetricsTable[key]; ok {
if !warnMetric.IsBotOnly && (value == model.WarnMetricStatusRunonce || value == model.WarnMetricStatusLimitReached) {
result[key], _ = a.getWarnMetricStatusAndDisplayTextsForId(key, nil, isE0Edition)
}
}
}
}
return result, nil
}
func (a *App) getWarnMetricStatusAndDisplayTextsForId(warnMetricId string, T i18n.TranslateFunc, isE0Edition bool) (*model.WarnMetricStatus, *model.WarnMetricDisplayTexts) {
var warnMetricStatus *model.WarnMetricStatus
var warnMetricDisplayTexts = &model.WarnMetricDisplayTexts{}
if warnMetric, ok := model.WarnMetricsTable[warnMetricId]; ok {
warnMetricStatus = &model.WarnMetricStatus{
Id: warnMetric.Id,
Limit: warnMetric.Limit,
Acked: false,
}
if T == nil {
mlog.Debug("No translation function")
return warnMetricStatus, nil
}
warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.bot_response.notification_success.message")
switch warnMetricId {
case model.SystemWarnMetricNumberOfTeams5:
warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_teams_5.notification_title")
if isE0Edition {
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_teams_5.start_trial.notification_body")
warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.number_of_teams_5.start_trial_notification_success.message")
} else {
warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.number_of_teams_5.contact_us.email_body")
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_teams_5.notification_body")
}
case model.SystemWarnMetricMfa:
warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.mfa.notification_title")
if isE0Edition {
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.mfa.start_trial.notification_body")
warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.mfa.start_trial_notification_success.message")
} else {
warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.mfa.contact_us.email_body")
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.mfa.notification_body")
}
case model.SystemWarnMetricEmailDomain:
warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.email_domain.notification_title")
if isE0Edition {
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.email_domain.start_trial.notification_body")
warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.email_domain.start_trial_notification_success.message")
} else {
warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.email_domain.contact_us.email_body")
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.email_domain.notification_body")
}
case model.SystemWarnMetricNumberOfChannels50:
warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_channels_50.notification_title")
if isE0Edition {
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_channels_50.start_trial.notification_body")
warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.number_of_channels_50.start_trial.notification_success.message")
} else {
warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.number_of_channels_50.contact_us.email_body")
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_channels_50.notification_body")
}
case model.SystemWarnMetricNumberOfActiveUsers100:
warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_active_users_100.notification_title")
if isE0Edition {
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_100.start_trial.notification_body")
warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.number_of_active_users_100.start_trial.notification_success.message")
} else {
warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.number_of_active_users_100.contact_us.email_body")
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_100.notification_body")
}
case model.SystemWarnMetricNumberOfActiveUsers200:
warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_active_users_200.notification_title")
if isE0Edition {
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_200.start_trial.notification_body")
warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.number_of_active_users_200.start_trial.notification_success.message")
} else {
warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.number_of_active_users_200.contact_us.email_body")
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_200.notification_body")
}
case model.SystemWarnMetricNumberOfActiveUsers300:
warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_active_users_300.start_trial.notification_title")
if isE0Edition {
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_300.start_trial.notification_body")
warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.number_of_active_users_300.start_trial.notification_success.message")
} else {
warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.number_of_active_users_300.contact_us.email_body")
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_300.notification_body")
}
case model.SystemWarnMetricNumberOfActiveUsers500:
warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_active_users_500.notification_title")
if isE0Edition {
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_500.start_trial.notification_body")
warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.number_of_active_users_500.start_trial.notification_success.message")
} else {
warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.number_of_active_users_500.contact_us.email_body")
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_500.notification_body")
}
case model.SystemWarnMetricNumberOfPosts2m:
warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_posts_2M.notification_title")
if isE0Edition {
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_posts_2M.start_trial.notification_body")
warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.number_of_posts_2M.start_trial.notification_success.message")
} else {
warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.number_of_posts_2M.contact_us.email_body")
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_posts_2M.notification_body")
}
default:
mlog.Debug("Invalid metric id", mlog.String("id", warnMetricId))
return nil, nil
}
return warnMetricStatus, warnMetricDisplayTexts
}
return nil, nil
}
func (a *App) NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, forceAck bool, isBot bool) *model.AppError {
if warnMetric, ok := model.WarnMetricsTable[warnMetricId]; ok {
data, nErr := a.Srv().Store().System().GetByName(warnMetric.Id)
if nErr == nil && data != nil && data.Value == model.WarnMetricStatusAck {
mlog.Debug("This metric warning has already been acknowledged", mlog.String("id", warnMetric.Id))
return nil
}
if !forceAck {
if *a.Config().EmailSettings.SMTPServer == "" {
return model.NewAppError("NotifyAndSetWarnMetricAck", "api.email.send_warn_metric_ack.missing_server.app_error", nil, i18n.T("api.context.invalid_param.app_error", map[string]any{"Name": "SMTPServer"}), http.StatusInternalServerError)
}
T := i18n.GetUserTranslations(sender.Locale)
data := a.Srv().EmailService.NewEmailTemplateData(sender.Locale)
data.Props["ContactNameHeader"] = T("api.templates.warn_metric_ack.body.contact_name_header")
data.Props["ContactNameValue"] = sender.GetFullName()
data.Props["ContactEmailHeader"] = T("api.templates.warn_metric_ack.body.contact_email_header")
data.Props["ContactEmailValue"] = sender.Email
//same definition as the active users count metric displayed in the SystemConsole Analytics section
registeredUsersCount, cerr := a.Srv().Store().User().Count(model.UserCountOptions{})
if cerr != nil {
mlog.Warn("Error retrieving the number of registered users", mlog.Err(cerr))
} else {
data.Props["RegisteredUsersHeader"] = T("api.templates.warn_metric_ack.body.registered_users_header")
data.Props["RegisteredUsersValue"] = registeredUsersCount
}
data.Props["SiteURLHeader"] = T("api.templates.warn_metric_ack.body.site_url_header")
data.Props["SiteURL"] = a.GetSiteURL()
data.Props["TelemetryIdHeader"] = T("api.templates.warn_metric_ack.body.diagnostic_id_header")
data.Props["TelemetryIdValue"] = a.TelemetryId()
data.Props["Footer"] = T("api.templates.warn_metric_ack.footer")
warnMetricStatus, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, T, false)
if warnMetricStatus == nil {
return model.NewAppError("NotifyAndSetWarnMetricAck", "api.email.send_warn_metric_ack.invalid_warn_metric.app_error", nil, "", http.StatusInternalServerError)
}
subject := T("api.templates.warn_metric_ack.subject")
data.Props["Title"] = warnMetricDisplayTexts.EmailBody
mailConfig := a.Srv().MailServiceConfig()
body, err := a.Srv().TemplatesContainer().RenderToString("warn_metric_ack", data)
if err != nil {
return model.NewAppError("NotifyAndSetWarnMetricAck", "api.email.send_warn_metric_ack.failure.app_error", map[string]any{"Error": err.Error()}, "", http.StatusInternalServerError)
}
if err := mail.SendMailUsingConfig(model.MmSupportAdvisorAddress, subject, body, mailConfig, false, "", "", "", sender.Email, "NotifyAndSetWarnMetricAck"); err != nil {
return model.NewAppError("NotifyAndSetWarnMetricAck", "api.email.send_warn_metric_ack.failure.app_error", map[string]any{"Error": err.Error()}, "", http.StatusInternalServerError)
}
}
if err := a.setWarnMetricsStatusAndNotify(warnMetric.Id); err != nil {
return err
}
}
return nil
}
func (a *App) setWarnMetricsStatusAndNotify(warnMetricId string) *model.AppError {
// Ack all metric warnings on the server
if err := a.setWarnMetricsStatus(model.WarnMetricStatusAck); err != nil {
return err
}
// Inform client that this metric warning has been acked
message := model.NewWebSocketEvent(model.WebsocketWarnMetricStatusRemoved, "", "", "", nil, "")
message.Add("warnMetricId", warnMetricId)
a.Publish(message)
return nil
}
func (a *App) setWarnMetricsStatus(status string) *model.AppError {
mlog.Debug("Set monitoring status for all warn metrics", mlog.String("status", status))
for _, warnMetric := range model.WarnMetricsTable {
if err := a.setWarnMetricsStatusForId(warnMetric.Id, status); err != nil {
return err
}
}
return nil
}
func (a *App) setWarnMetricsStatusForId(warnMetricId string, status string) *model.AppError {
mlog.Debug("Store status for warn metric", mlog.String("warnMetricId", warnMetricId), mlog.String("status", status))
if err := a.Srv().Store().System().SaveOrUpdateWithWarnMetricHandling(&model.System{
Name: warnMetricId,
Value: status,
}); err != nil {
return model.NewAppError("setWarnMetricsStatusForId", "app.system.warn_metric.store.app_error", map[string]any{"WarnMetricName": warnMetricId}, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) RequestLicenseAndAckWarnMetric(c *request.Context, warnMetricId string, isBot bool) *model.AppError {
if *a.Config().ExperimentalSettings.RestrictSystemAdmin {
return model.NewAppError("RequestLicenseAndAckWarnMetric", "api.restricted_system_admin", nil, "", http.StatusForbidden)
}
currentUser, appErr := a.GetUser(c.Session().UserId)
if appErr != nil {
return appErr
}
registeredUsersCount, err := a.Srv().Store().User().Count(model.UserCountOptions{})
if err != nil {
return model.NewAppError("RequestLicenseAndAckWarnMetric", "api.license.request_trial_license.fail_get_user_count.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if err := a.Channels().RequestTrialLicense(c.Session().UserId, int(registeredUsersCount), true, true); err != nil {
// turn off warn metric warning even in case of StartTrial failure
if nerr := a.setWarnMetricsStatusAndNotify(warnMetricId); nerr != nil {
return nerr
}
return err
}
if appErr = a.NotifyAndSetWarnMetricAck(warnMetricId, currentUser, true, isBot); appErr != nil {
return appErr
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"golang.org/x/sync/errgroup"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
DayMilliseconds = 24 * 60 * 60 * 1000
MonthMilliseconds = 31 * DayMilliseconds
)
func (a *App) GetAnalytics(name string, teamID string) (model.AnalyticsRows, *model.AppError) {
skipIntensiveQueries := false
var systemUserCount int64
systemUserCount, err := a.Srv().Store().User().Count(model.UserCountOptions{})
if err != nil {
return nil, model.NewAppError("GetAnalytics", "app.user.get_total_users_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if systemUserCount > int64(*a.Config().AnalyticsSettings.MaxUsersForStatistics) {
mlog.Debug("More than limit users are on the system, intensive queries skipped", mlog.Int("limit", *a.Config().AnalyticsSettings.MaxUsersForStatistics))
skipIntensiveQueries = true
}
if name == "standard" {
var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 11)
rows[0] = &model.AnalyticsRow{Name: "channel_open_count", Value: 0}
rows[1] = &model.AnalyticsRow{Name: "channel_private_count", Value: 0}
rows[2] = &model.AnalyticsRow{Name: "post_count", Value: 0}
rows[3] = &model.AnalyticsRow{Name: "unique_user_count", Value: 0}
rows[4] = &model.AnalyticsRow{Name: "team_count", Value: 0}
rows[5] = &model.AnalyticsRow{Name: "total_websocket_connections", Value: 0}
rows[6] = &model.AnalyticsRow{Name: "total_master_db_connections", Value: 0}
rows[7] = &model.AnalyticsRow{Name: "total_read_db_connections", Value: 0}
rows[8] = &model.AnalyticsRow{Name: "daily_active_users", Value: 0}
rows[9] = &model.AnalyticsRow{Name: "monthly_active_users", Value: 0}
rows[10] = &model.AnalyticsRow{Name: "inactive_user_count", Value: 0}
var g errgroup.Group
var openChannelsCount int64
g.Go(func() error {
var err error
if openChannelsCount, err = a.Srv().Store().Channel().AnalyticsTypeCount(teamID, model.ChannelTypeOpen); err != nil {
return model.NewAppError("GetAnalytics", "app.channel.analytics_type_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
var privateChannelsCount int64
g.Go(func() error {
var err error
if privateChannelsCount, err = a.Srv().Store().Channel().AnalyticsTypeCount(teamID, model.ChannelTypePrivate); err != nil {
return model.NewAppError("GetAnalytics", "app.channel.analytics_type_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
var usersCount int64
var inactiveUsersCount int64
if teamID == "" {
g.Go(func() error {
var err error
if inactiveUsersCount, err = a.Srv().Store().User().AnalyticsGetInactiveUsersCount(); err != nil {
return model.NewAppError("GetAnalytics", "app.user.analytics_get_inactive_users_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
} else {
g.Go(func() error {
var err error
if usersCount, err = a.Srv().Store().User().Count(model.UserCountOptions{TeamId: teamID}); err != nil {
return model.NewAppError("GetAnalytics", "app.user.get_total_users_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
}
var postsCount int64
if !skipIntensiveQueries {
g.Go(func() error {
var err error
if postsCount, err = a.Srv().Store().Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: teamID}); err != nil {
return model.NewAppError("GetAnalytics", "app.post.analytics_posts_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
}
var teamsCount int64
g.Go(func() error {
var err error
if teamsCount, err = a.Srv().Store().Team().AnalyticsTeamCount(nil); err != nil {
return model.NewAppError("GetAnalytics", "app.team.analytics_team_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
var dailyActiveUsersCount int64
g.Go(func() error {
var err error
if dailyActiveUsersCount, err = a.Srv().Store().User().AnalyticsActiveCount(DayMilliseconds, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: false}); err != nil {
return model.NewAppError("GetAnalytics", "app.user.analytics_daily_active_users.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
var monthlyActiveUsersCount int64
g.Go(func() error {
var err error
if monthlyActiveUsersCount, err = a.Srv().Store().User().AnalyticsActiveCount(MonthMilliseconds, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: false}); err != nil {
return model.NewAppError("GetAnalytics", "app.user.analytics_daily_active_users.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
if err := g.Wait(); err != nil {
return nil, err.(*model.AppError)
}
rows[0].Value = float64(openChannelsCount)
rows[1].Value = float64(privateChannelsCount)
if skipIntensiveQueries {
rows[2].Value = -1
} else {
rows[2].Value = float64(postsCount)
}
if teamID == "" {
rows[3].Value = float64(systemUserCount)
rows[10].Value = float64(inactiveUsersCount)
} else {
rows[10].Value = -1
rows[3].Value = float64(usersCount)
}
rows[4].Value = float64(teamsCount)
// If in HA mode then aggregate all the stats
if a.Cluster() != nil && *a.Config().ClusterSettings.Enable {
stats, err2 := a.Cluster().GetClusterStats()
if err2 != nil {
return nil, err2
}
totalSockets := a.TotalWebsocketConnections()
totalMasterDb := a.Srv().Store().TotalMasterDbConnections()
totalReadDb := a.Srv().Store().TotalReadDbConnections()
for _, stat := range stats {
totalSockets = totalSockets + stat.TotalWebsocketConnections
totalMasterDb = totalMasterDb + stat.TotalMasterDbConnections
totalReadDb = totalReadDb + stat.TotalReadDbConnections
}
rows[5].Value = float64(totalSockets)
rows[6].Value = float64(totalMasterDb)
rows[7].Value = float64(totalReadDb)
} else {
rows[5].Value = float64(a.TotalWebsocketConnections())
rows[6].Value = float64(a.Srv().Store().TotalMasterDbConnections())
rows[7].Value = float64(a.Srv().Store().TotalReadDbConnections())
}
rows[8].Value = float64(dailyActiveUsersCount)
rows[9].Value = float64(monthlyActiveUsersCount)
return rows, nil
} else if name == "bot_post_counts_day" {
if skipIntensiveQueries {
rows := model.AnalyticsRows{&model.AnalyticsRow{Name: "", Value: -1}}
return rows, nil
}
analyticsRows, nErr := a.Srv().Store().Post().AnalyticsPostCountsByDay(&model.AnalyticsPostCountsOptions{
TeamId: teamID,
BotsOnly: true,
YesterdayOnly: false,
})
if nErr != nil {
return nil, model.NewAppError("GetAnalytics", "app.post.analytics_posts_count_by_day.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return analyticsRows, nil
} else if name == "post_counts_day" {
if skipIntensiveQueries {
rows := model.AnalyticsRows{&model.AnalyticsRow{Name: "", Value: -1}}
return rows, nil
}
analyticsRows, nErr := a.Srv().Store().Post().AnalyticsPostCountsByDay(&model.AnalyticsPostCountsOptions{
TeamId: teamID,
BotsOnly: false,
YesterdayOnly: false,
})
if nErr != nil {
return nil, model.NewAppError("GetAnalytics", "app.post.analytics_posts_count_by_day.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return analyticsRows, nil
} else if name == "user_counts_with_posts_day" {
if skipIntensiveQueries {
rows := model.AnalyticsRows{&model.AnalyticsRow{Name: "", Value: -1}}
return rows, nil
}
analyticsRows, nErr := a.Srv().Store().Post().AnalyticsUserCountsWithPostsByDay(teamID)
if nErr != nil {
return nil, model.NewAppError("GetAnalytics", "app.post.analytics_user_counts_posts_by_day.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return analyticsRows, nil
} else if name == "extra_counts" {
var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 6)
rows[0] = &model.AnalyticsRow{Name: "file_post_count", Value: 0}
rows[1] = &model.AnalyticsRow{Name: "hashtag_post_count", Value: 0}
rows[2] = &model.AnalyticsRow{Name: "incoming_webhook_count", Value: 0}
rows[3] = &model.AnalyticsRow{Name: "outgoing_webhook_count", Value: 0}
rows[4] = &model.AnalyticsRow{Name: "command_count", Value: 0}
rows[5] = &model.AnalyticsRow{Name: "session_count", Value: 0}
var g2 errgroup.Group
var incomingWebhookCount int64
g2.Go(func() error {
var err error
if incomingWebhookCount, err = a.Srv().Store().Webhook().AnalyticsIncomingCount(teamID); err != nil {
return model.NewAppError("GetAnalytics", "app.webhooks.analytics_incoming_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
var outgoingWebhookCount int64
g2.Go(func() error {
var err error
if outgoingWebhookCount, err = a.Srv().Store().Webhook().AnalyticsOutgoingCount(teamID); err != nil {
return model.NewAppError("GetAnalytics", "app.webhooks.analytics_outgoing_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
var commandsCount int64
g2.Go(func() error {
var err error
if commandsCount, err = a.Srv().Store().Command().AnalyticsCommandCount(teamID); err != nil {
return model.NewAppError("GetAnalytics", "app.analytics.getanalytics.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
var sessionsCount int64
g2.Go(func() error {
var err error
if sessionsCount, err = a.Srv().Store().Session().AnalyticsSessionCount(); err != nil {
return model.NewAppError("GetAnalytics", "app.session.analytics_session_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
var filesCount int64
var hashtagsCount int64
if !skipIntensiveQueries {
g2.Go(func() error {
var err error
if filesCount, err = a.Srv().Store().Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: teamID, MustHaveFile: true}); err != nil {
return model.NewAppError("GetAnalytics", "app.post.analytics_posts_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
g2.Go(func() error {
var err error
if hashtagsCount, err = a.Srv().Store().Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: teamID, MustHaveHashtag: true}); err != nil {
return model.NewAppError("GetAnalytics", "app.post.analytics_posts_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
})
}
if err := g2.Wait(); err != nil {
return nil, err.(*model.AppError)
}
if skipIntensiveQueries {
rows[0].Value = -1
rows[1].Value = -1
} else {
rows[0].Value = float64(filesCount)
rows[1].Value = float64(hashtagsCount)
}
rows[2].Value = float64(incomingWebhookCount)
rows[3].Value = float64(outgoingWebhookCount)
rows[4].Value = float64(commandsCount)
rows[5].Value = float64(sessionsCount)
return rows, nil
}
return nil, nil
}
func (a *App) GetRecentlyActiveUsersForTeam(teamID string) (map[string]*model.User, *model.AppError) {
users, err := a.Srv().Store().User().GetRecentlyActiveUsersForTeam(teamID, 0, 100, nil)
if err != nil {
return nil, model.NewAppError("GetRecentlyActiveUsersForTeam", "app.user.get_recently_active_users.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
userMap := make(map[string]*model.User)
for _, user := range users {
userMap[user.Id] = user
}
return userMap, nil
}
func (a *App) GetRecentlyActiveUsersForTeamPage(teamID string, page, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store().User().GetRecentlyActiveUsersForTeam(teamID, page*perPage, perPage, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetRecentlyActiveUsersForTeamPage", "app.user.get_recently_active_users.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetNewUsersForTeamPage(teamID string, page, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store().User().GetNewUsersForTeam(teamID, page*perPage, perPage, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetNewUsersForTeamPage", "app.user.get_new_users.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return a.sanitizeProfiles(users, asAdmin), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/services/httpservice"
"github.com/mattermost/mattermost-server/v6/server/platform/services/imageproxy"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
"github.com/mattermost/mattermost-server/v6/server/platform/services/timezones"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/templates"
)
// App is a pure functional component that does not have any fields, except Server.
// It is a request-scoped struct constructed every time a request hits the server,
// and its only purpose is to provide business logic to Server via its methods.
type App struct {
ch *Channels
}
func New(options ...AppOption) *App {
app := &App{}
for _, option := range options {
option(app)
}
return app
}
func (a *App) TelemetryId() string {
return a.Srv().TelemetryId()
}
func (s *Server) TemplatesContainer() *templates.Container {
return s.htmlTemplateWatcher
}
func (a *App) Handle404(w http.ResponseWriter, r *http.Request) {
ipAddress := utils.GetIPAddress(r, a.Config().ServiceSettings.TrustedProxyIPHeader)
mlog.Debug("not found handler triggered", mlog.String("path", r.URL.Path), mlog.Int("code", 404), mlog.String("ip", ipAddress))
if *a.Config().ServiceSettings.WebserverMode == "disabled" {
http.NotFound(w, r)
return
}
utils.RenderWebAppError(a.Config(), w, r, model.NewAppError("Handle404", "api.context.404.app_error", nil, "", http.StatusNotFound), a.AsymmetricSigningKey())
}
func (s *Server) getFirstServerRunTimestamp() (int64, *model.AppError) {
systemData, err := s.Store().System().GetByName(model.SystemFirstServerRunTimestampKey)
if err != nil {
return 0, model.NewAppError("getFirstServerRunTimestamp", "app.system.get_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
value, err := strconv.ParseInt(systemData.Value, 10, 64)
if err != nil {
return 0, model.NewAppError("getFirstServerRunTimestamp", "app.system_install_date.parse_int.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return value, nil
}
func (a *App) Channels() *Channels {
return a.ch
}
func (a *App) Srv() *Server {
return a.ch.srv
}
func (a *App) Log() *mlog.Logger {
return a.ch.srv.Log()
}
func (a *App) NotificationsLog() *mlog.Logger {
return a.ch.srv.NotificationsLog()
}
func (a *App) AccountMigration() einterfaces.AccountMigrationInterface {
return a.ch.AccountMigration
}
func (a *App) Cluster() einterfaces.ClusterInterface {
return a.ch.srv.platform.Cluster()
}
func (a *App) Compliance() einterfaces.ComplianceInterface {
return a.ch.Compliance
}
func (a *App) DataRetention() einterfaces.DataRetentionInterface {
return a.ch.DataRetention
}
func (a *App) SearchEngine() *searchengine.Broker {
return a.ch.srv.platform.SearchEngine
}
func (a *App) Ldap() einterfaces.LdapInterface {
return a.ch.Ldap
}
func (a *App) MessageExport() einterfaces.MessageExportInterface {
return a.ch.MessageExport
}
func (a *App) Metrics() einterfaces.MetricsInterface {
return a.ch.srv.GetMetrics()
}
func (a *App) Notification() einterfaces.NotificationInterface {
return a.ch.Notification
}
func (a *App) Saml() einterfaces.SamlInterface {
return a.ch.Saml
}
func (a *App) Cloud() einterfaces.CloudInterface {
return a.ch.srv.Cloud
}
func (a *App) HTTPService() httpservice.HTTPService {
return a.ch.srv.httpService
}
func (a *App) ImageProxy() *imageproxy.ImageProxy {
return a.ch.imageProxy
}
func (a *App) Timezones() *timezones.Timezones {
return a.ch.srv.timezones
}
func (a *App) License() *model.License {
return a.Srv().License()
}
func (a *App) DBHealthCheckWrite() error {
currentTime := strconv.FormatInt(time.Now().Unix(), 10)
return a.Srv().Store().System().SaveOrUpdate(&model.System{
Name: a.dbHealthCheckKey(),
Value: currentTime,
})
}
func (a *App) DBHealthCheckDelete() error {
_, err := a.Srv().Store().System().PermanentDeleteByName(a.dbHealthCheckKey())
return err
}
func (a *App) dbHealthCheckKey() string {
return fmt.Sprintf("health_check_%s", a.GetClusterId())
}
func (a *App) CheckIntegrity() <-chan model.IntegrityCheckResult {
return a.Srv().Store().CheckIntegrity()
}
func (a *App) SetChannels(ch *Channels) {
a.ch = ch
}
func (a *App) SetServer(srv *Server) {
a.ch.srv = srv
}
func (a *App) UpdateExpiredDNDStatuses() ([]*model.Status, error) {
return a.Srv().Store().Status().UpdateExpiredDNDStatuses()
}
// Ensure system service adapter implements `product.SystemService`
var _ product.SystemService = (*systemServiceAdapter)(nil)
// systemServiceAdapter provides a collection of system APIs for use by products.
type systemServiceAdapter struct {
server *Server
}
func (ssa *systemServiceAdapter) GetDiagnosticId() string {
return ssa.server.TelemetryId()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"fmt"
"net/http"
"os/user"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var (
LevelAPI = mlog.LvlAuditAPI
LevelContent = mlog.LvlAuditContent
LevelPerms = mlog.LvlAuditPerms
LevelCLI = mlog.LvlAuditCLI
)
func (a *App) GetAudits(userID string, limit int) (model.Audits, *model.AppError) {
audits, err := a.Srv().Store().Audit().Get(userID, 0, limit)
if err != nil {
var outErr *store.ErrOutOfBounds
switch {
case errors.As(err, &outErr):
return nil, model.NewAppError("GetAudits", "app.audit.get.limit.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("GetAudits", "app.audit.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return audits, nil
}
func (a *App) GetAuditsPage(userID string, page int, perPage int) (model.Audits, *model.AppError) {
audits, err := a.Srv().Store().Audit().Get(userID, page*perPage, perPage)
if err != nil {
var outErr *store.ErrOutOfBounds
switch {
case errors.As(err, &outErr):
return nil, model.NewAppError("GetAuditsPage", "app.audit.get.limit.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("GetAuditsPage", "app.audit.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return audits, nil
}
// LogAuditRec logs an audit record using default LvlAuditCLI.
func (a *App) LogAuditRec(rec *audit.Record, err error) {
a.LogAuditRecWithLevel(rec, mlog.LvlAuditCLI, err)
}
// LogAuditRecWithLevel logs an audit record using specified Level.
func (a *App) LogAuditRecWithLevel(rec *audit.Record, level mlog.Level, err error) {
if rec == nil {
return
}
if err != nil {
appErr, ok := err.(*model.AppError)
if ok {
rec.AddErrorCode(appErr.StatusCode)
}
rec.AddErrorDesc(appErr.Error())
rec.Fail()
}
a.Srv().Audit.LogRecord(level, *rec)
}
// MakeAuditRecord creates a audit record pre-populated with defaults.
func (a *App) MakeAuditRecord(event string, initialStatus string) *audit.Record {
var userID string
user, err := user.Current()
if err == nil {
userID = fmt.Sprintf("%s:%s", user.Uid, user.Username)
}
rec := &audit.Record{
EventName: event,
Status: initialStatus,
Meta: map[string]interface{}{
audit.KeyAPIPath: "",
audit.KeyClusterID: a.GetClusterId(),
},
Actor: audit.EventActor{
UserId: userID,
SessionId: "",
Client: fmt.Sprintf("server %s-%s", model.BuildNumber, model.BuildHash),
IpAddress: "",
},
EventData: audit.EventData{
Parameters: map[string]interface{}{},
PriorState: map[string]interface{}{},
ResultState: map[string]interface{}{},
ObjectType: "",
},
}
return rec
}
func (s *Server) configureAudit(adt *audit.Audit, bAllowAdvancedLogging bool) error {
adt.OnQueueFull = s.onAuditTargetQueueFull
adt.OnError = s.onAuditError
var logConfigSrc config.LogConfigSrc
dsn := *s.platform.Config().ExperimentalAuditSettings.AdvancedLoggingConfig
if bAllowAdvancedLogging && dsn != "" {
var err error
logConfigSrc, err = config.NewLogConfigSrc(dsn, s.platform.GetConfigStore())
if err != nil {
return fmt.Errorf("invalid config source for audit, %w", err)
}
mlog.Debug("Loaded audit configuration", mlog.String("source", dsn))
}
// ExperimentalAuditSettings provides basic file audit (E0, E10); logConfigSrc provides advanced config (E20).
cfg, err := config.MloggerConfigFromAuditConfig(s.platform.Config().ExperimentalAuditSettings, logConfigSrc)
if err != nil {
return fmt.Errorf("invalid config for audit, %w", err)
}
return adt.Configure(cfg)
}
func (s *Server) onAuditTargetQueueFull(qname string, maxQSize int) bool {
mlog.Error("Audit queue full, dropping record.", mlog.String("qname", qname), mlog.Int("queueSize", maxQSize))
return true // drop it
}
func (s *Server) onAuditError(err error) {
mlog.Error("Audit Error", mlog.Err(err))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/app/users"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mfa"
)
type TokenLocation int
const (
TokenLocationNotFound TokenLocation = iota
TokenLocationHeader
TokenLocationCookie
TokenLocationQueryString
TokenLocationCloudHeader
TokenLocationRemoteClusterHeader
)
func (tl TokenLocation) String() string {
switch tl {
case TokenLocationNotFound:
return "Not Found"
case TokenLocationHeader:
return "Header"
case TokenLocationCookie:
return "Cookie"
case TokenLocationQueryString:
return "QueryString"
case TokenLocationCloudHeader:
return "CloudHeader"
case TokenLocationRemoteClusterHeader:
return "RemoteClusterHeader"
default:
return "Unknown"
}
}
func (a *App) IsPasswordValid(password string) *model.AppError {
if err := users.IsPasswordValidWithSettings(password, &a.Config().PasswordSettings); err != nil {
var invErr *users.ErrInvalidPassword
switch {
case errors.As(err, &invErr):
return model.NewAppError("User.IsValid", invErr.Id(), map[string]any{"Min": *a.Config().PasswordSettings.MinimumLength}, "", http.StatusBadRequest).Wrap(err)
default:
return model.NewAppError("User.IsValid", "app.valid_password_generic.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return nil
}
func (a *App) CheckPasswordAndAllCriteria(user *model.User, password string, mfaToken string) *model.AppError {
if err := a.CheckUserPreflightAuthenticationCriteria(user, mfaToken); err != nil {
return err
}
if err := users.CheckUserPassword(user, password); err != nil {
if passErr := a.Srv().Store().User().UpdateFailedPasswordAttempts(user.Id, user.FailedAttempts+1); passErr != nil {
return model.NewAppError("CheckPasswordAndAllCriteria", "app.user.update_failed_pwd_attempts.app_error", nil, "", http.StatusInternalServerError).Wrap(passErr)
}
a.InvalidateCacheForUser(user.Id)
var invErr *users.ErrInvalidPassword
switch {
case errors.As(err, &invErr):
return model.NewAppError("checkUserPassword", "api.user.check_user_password.invalid.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized).Wrap(err)
default:
return model.NewAppError("checkUserPassword", "app.valid_password_generic.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if err := a.CheckUserMfa(user, mfaToken); err != nil {
// If the mfaToken is not set, we assume the client used this as a pre-flight request to query the server
// about the MFA state of the user in question
if mfaToken != "" {
if passErr := a.Srv().Store().User().UpdateFailedPasswordAttempts(user.Id, user.FailedAttempts+1); passErr != nil {
return model.NewAppError("CheckPasswordAndAllCriteria", "app.user.update_failed_pwd_attempts.app_error", nil, "", http.StatusInternalServerError).Wrap(passErr)
}
}
a.InvalidateCacheForUser(user.Id)
return err
}
if passErr := a.Srv().Store().User().UpdateFailedPasswordAttempts(user.Id, 0); passErr != nil {
return model.NewAppError("CheckPasswordAndAllCriteria", "app.user.update_failed_pwd_attempts.app_error", nil, "", http.StatusInternalServerError).Wrap(passErr)
}
a.InvalidateCacheForUser(user.Id)
if err := a.CheckUserPostflightAuthenticationCriteria(user); err != nil {
return err
}
return nil
}
// This to be used for places we check the users password when they are already logged in
func (a *App) DoubleCheckPassword(user *model.User, password string) *model.AppError {
if err := checkUserLoginAttempts(user, *a.Config().ServiceSettings.MaximumLoginAttempts); err != nil {
return err
}
if err := users.CheckUserPassword(user, password); err != nil {
if passErr := a.Srv().Store().User().UpdateFailedPasswordAttempts(user.Id, user.FailedAttempts+1); passErr != nil {
return model.NewAppError("DoubleCheckPassword", "app.user.update_failed_pwd_attempts.app_error", nil, "", http.StatusInternalServerError).Wrap(passErr)
}
a.InvalidateCacheForUser(user.Id)
var invErr *users.ErrInvalidPassword
switch {
case errors.As(err, &invErr):
return model.NewAppError("DoubleCheckPassword", "api.user.check_user_password.invalid.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized).Wrap(err)
default:
return model.NewAppError("DoubleCheckPassword", "app.valid_password_generic.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if passErr := a.Srv().Store().User().UpdateFailedPasswordAttempts(user.Id, 0); passErr != nil {
return model.NewAppError("DoubleCheckPassword", "app.user.update_failed_pwd_attempts.app_error", nil, "", http.StatusInternalServerError).Wrap(passErr)
}
a.InvalidateCacheForUser(user.Id)
return nil
}
func (a *App) checkLdapUserPasswordAndAllCriteria(c *request.Context, ldapId *string, password string, mfaToken string) (*model.User, *model.AppError) {
if a.Ldap() == nil || ldapId == nil {
err := model.NewAppError("doLdapAuthentication", "api.user.login_ldap.not_available.app_error", nil, "", http.StatusNotImplemented)
return nil, err
}
ldapUser, err := a.Ldap().DoLogin(c, *ldapId, password)
if err != nil {
err.StatusCode = http.StatusUnauthorized
return nil, err
}
if err := a.CheckUserMfa(ldapUser, mfaToken); err != nil {
return nil, err
}
if err := checkUserNotDisabled(ldapUser); err != nil {
return nil, err
}
// user successfully authenticated
return ldapUser, nil
}
func (a *App) CheckUserAllAuthenticationCriteria(user *model.User, mfaToken string) *model.AppError {
if err := a.CheckUserPreflightAuthenticationCriteria(user, mfaToken); err != nil {
return err
}
if err := a.CheckUserPostflightAuthenticationCriteria(user); err != nil {
return err
}
return nil
}
func (a *App) CheckUserPreflightAuthenticationCriteria(user *model.User, mfaToken string) *model.AppError {
if err := checkUserNotDisabled(user); err != nil {
return err
}
if err := checkUserNotBot(user); err != nil {
return err
}
if err := checkUserLoginAttempts(user, *a.Config().ServiceSettings.MaximumLoginAttempts); err != nil {
return err
}
return nil
}
func (a *App) CheckUserPostflightAuthenticationCriteria(user *model.User) *model.AppError {
if !user.EmailVerified && *a.Config().EmailSettings.RequireEmailVerification {
return model.NewAppError("Login", "api.user.login.not_verified.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized)
}
return nil
}
func (a *App) CheckUserMfa(user *model.User, token string) *model.AppError {
if !user.MfaActive || !*a.Config().ServiceSettings.EnableMultifactorAuthentication {
return nil
}
if !*a.Config().ServiceSettings.EnableMultifactorAuthentication {
return model.NewAppError("CheckUserMfa", "mfa.mfa_disabled.app_error", nil, "", http.StatusNotImplemented)
}
ok, err := mfa.New(a.Srv().Store().User()).ValidateToken(user.MfaSecret, token)
if err != nil {
return model.NewAppError("CheckUserMfa", "mfa.validate_token.authenticate.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if !ok {
return model.NewAppError("checkUserMfa", "api.user.check_user_mfa.bad_code.app_error", nil, "", http.StatusUnauthorized)
}
return nil
}
func checkUserLoginAttempts(user *model.User, max int) *model.AppError {
if user.FailedAttempts >= max {
return model.NewAppError("checkUserLoginAttempts", "api.user.check_user_login_attempts.too_many.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized)
}
return nil
}
func checkUserNotDisabled(user *model.User) *model.AppError {
if user.DeleteAt > 0 {
return model.NewAppError("Login", "api.user.login.inactive.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized)
}
return nil
}
func checkUserNotBot(user *model.User) *model.AppError {
if user.IsBot {
return model.NewAppError("Login", "api.user.login.bot_login_forbidden.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized)
}
return nil
}
func (a *App) authenticateUser(c *request.Context, user *model.User, password, mfaToken string) (*model.User, *model.AppError) {
license := a.Srv().License()
ldapAvailable := *a.Config().LdapSettings.Enable && a.Ldap() != nil && license != nil && *license.Features.LDAP
if user.AuthService == model.UserAuthServiceLdap {
if !ldapAvailable {
err := model.NewAppError("login", "api.user.login_ldap.not_available.app_error", nil, "", http.StatusNotImplemented)
return user, err
}
ldapUser, err := a.checkLdapUserPasswordAndAllCriteria(c, user.AuthData, password, mfaToken)
if err != nil {
err.StatusCode = http.StatusUnauthorized
return user, err
}
// slightly redundant to get the user again, but we need to get it from the LDAP server
return ldapUser, nil
}
if user.AuthService != "" {
authService := user.AuthService
if authService == model.UserAuthServiceSaml {
authService = strings.ToUpper(authService)
}
err := model.NewAppError("login", "api.user.login.use_auth_service.app_error", map[string]any{"AuthService": authService}, "", http.StatusBadRequest)
return user, err
}
if err := a.CheckPasswordAndAllCriteria(user, password, mfaToken); err != nil {
err.StatusCode = http.StatusUnauthorized
return user, err
}
return user, nil
}
func ParseAuthTokenFromRequest(r *http.Request) (token string, loc TokenLocation) {
defer func() {
// Stripping off tokens of large sizes
// to prevent logging a large string.
if len(token) > 50 {
token = token[:50]
}
}()
authHeader := r.Header.Get(model.HeaderAuth)
// Attempt to parse the token from the cookie
if cookie, err := r.Cookie(model.SessionCookieToken); err == nil {
return cookie.Value, TokenLocationCookie
}
// Parse the token from the header
if len(authHeader) > 6 && strings.ToUpper(authHeader[0:6]) == model.HeaderBearer {
// Default session token
return authHeader[7:], TokenLocationHeader
}
if len(authHeader) > 5 && strings.ToLower(authHeader[0:5]) == model.HeaderToken {
// OAuth token
return authHeader[6:], TokenLocationHeader
}
// Attempt to parse token out of the query string
if token := r.URL.Query().Get("access_token"); token != "" {
return token, TokenLocationQueryString
}
if token := r.Header.Get(model.HeaderCloudToken); token != "" {
return token, TokenLocationCloudHeader
}
if token := r.Header.Get(model.HeaderRemoteclusterToken); token != "" {
return token, TokenLocationRemoteClusterHeader
}
return "", TokenLocationNotFound
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"database/sql"
"errors"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) MakePermissionError(s *model.Session, permissions []*model.Permission) *model.AppError {
permissionsStr := "permission="
for _, permission := range permissions {
permissionsStr += permission.Id
permissionsStr += ","
}
return model.NewAppError("Permissions", "api.context.permissions.app_error", nil, "userId="+s.UserId+", "+permissionsStr, http.StatusForbidden)
}
func (a *App) SessionHasPermissionTo(session model.Session, permission *model.Permission) bool {
if session.IsUnrestricted() {
return true
}
return a.RolesGrantPermission(session.GetUserRoles(), permission.Id)
}
func (a *App) SessionHasPermissionToAny(session model.Session, permissions []*model.Permission) bool {
for _, perm := range permissions {
if a.SessionHasPermissionTo(session, perm) {
return true
}
}
return false
}
func (a *App) SessionHasPermissionToTeam(session model.Session, teamID string, permission *model.Permission) bool {
if teamID == "" {
return false
}
if session.IsUnrestricted() {
return true
}
teamMember := session.GetTeamByTeamId(teamID)
if teamMember != nil {
if a.RolesGrantPermission(teamMember.GetRoles(), permission.Id) {
return true
}
}
return a.RolesGrantPermission(session.GetUserRoles(), permission.Id)
}
// SessionHasPermissionToTeams returns true only if user has access to all teams.
func (a *App) SessionHasPermissionToTeams(c request.CTX, session model.Session, teamIDs []string, permission *model.Permission) bool {
if len(teamIDs) == 0 {
return true
}
for _, teamID := range teamIDs {
if teamID == "" {
return false
}
}
if session.IsUnrestricted() {
return true
}
// Getting the list of unique roles from all teams.
var roles []string
uniqueRoles := make(map[string]bool)
for _, teamID := range teamIDs {
tm := session.GetTeamByTeamId(teamID)
if tm != nil {
for _, role := range tm.GetRoles() {
uniqueRoles[role] = true
}
}
}
for role := range uniqueRoles {
roles = append(roles, role)
}
if a.RolesGrantPermission(roles, permission.Id) {
return true
}
return a.RolesGrantPermission(session.GetUserRoles(), permission.Id)
}
func (a *App) SessionHasPermissionToChannel(c request.CTX, session model.Session, channelID string, permission *model.Permission) bool {
if channelID == "" {
return false
}
ids, err := a.Srv().Store().Channel().GetAllChannelMembersForUser(session.UserId, true, true)
var channelRoles []string
if err == nil {
if roles, ok := ids[channelID]; ok {
channelRoles = strings.Fields(roles)
if a.RolesGrantPermission(channelRoles, permission.Id) {
return true
}
}
}
channel, appErr := a.GetChannel(c, channelID)
if appErr != nil && appErr.StatusCode == http.StatusNotFound {
return false
}
if session.IsUnrestricted() {
return true
}
if appErr == nil && channel.TeamId != "" {
return a.SessionHasPermissionToTeam(session, channel.TeamId, permission)
}
return a.SessionHasPermissionTo(session, permission)
}
// SessionHasPermissionToChannels returns true only if user has access to all channels.
func (a *App) SessionHasPermissionToChannels(c request.CTX, session model.Session, channelIDs []string, permission *model.Permission) bool {
if len(channelIDs) == 0 {
return true
}
for _, channelID := range channelIDs {
if channelID == "" {
return false
}
}
if session.IsUnrestricted() {
return true
}
ids, err := a.Srv().Store().Channel().GetAllChannelMembersForUser(session.UserId, true, true)
var channelRoles []string
uniqueRoles := make(map[string]bool)
if err == nil {
for _, channelID := range channelIDs {
if roles, ok := ids[channelID]; ok {
for _, role := range strings.Fields(roles) {
uniqueRoles[role] = true
}
}
}
}
for role := range uniqueRoles {
channelRoles = append(channelRoles, role)
}
if a.RolesGrantPermission(channelRoles, permission.Id) {
return true
}
channels, appErr := a.GetChannels(c, channelIDs)
if appErr != nil && appErr.StatusCode == http.StatusNotFound {
return false
}
// Get TeamIDs from channels
uniqueTeamIDs := make(map[string]bool)
for _, ch := range channels {
if ch.TeamId != "" {
uniqueTeamIDs[ch.TeamId] = true
}
}
var teamIDs []string
for teamID := range uniqueTeamIDs {
teamIDs = append(teamIDs, teamID)
}
if appErr == nil && len(teamIDs) > 0 {
return a.SessionHasPermissionToTeams(c, session, teamIDs, permission)
}
return a.SessionHasPermissionTo(session, permission)
}
func (a *App) SessionHasPermissionToGroup(session model.Session, groupID string, permission *model.Permission) bool {
groupMember, err := a.Srv().Store().Group().GetMember(groupID, session.UserId)
// don't reject immediately on ErrNoRows error because there's further authz logic below for non-groupmembers
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return false
}
// each member of a group is implicitly considered to have the 'custom_group_user' role in that group, so if the user is a member of the
// group and custom_group_user on their system has the requested permission then return true
if groupMember != nil && a.RolesGrantPermission([]string{model.CustomGroupUserRoleId}, permission.Id) {
return true
}
// Not implemented: group-override schemes.
// ...otherwise check their system roles to see if they have the requested permission system-wide
return a.SessionHasPermissionTo(session, permission)
}
func (a *App) SessionHasPermissionToChannelByPost(session model.Session, postID string, permission *model.Permission) bool {
if channelMember, err := a.Srv().Store().Channel().GetMemberForPost(postID, session.UserId); err == nil {
if a.RolesGrantPermission(channelMember.GetRoles(), permission.Id) {
return true
}
}
if channel, err := a.Srv().Store().Channel().GetForPost(postID); err == nil {
if channel.TeamId != "" {
return a.SessionHasPermissionToTeam(session, channel.TeamId, permission)
}
}
return a.SessionHasPermissionTo(session, permission)
}
func (a *App) SessionHasPermissionToCategory(c request.CTX, session model.Session, userID, teamID, categoryId string) bool {
if a.SessionHasPermissionTo(session, model.PermissionEditOtherUsers) {
return true
}
category, err := a.GetSidebarCategory(c, categoryId)
return err == nil && category != nil && category.UserId == session.UserId && category.UserId == userID && category.TeamId == teamID
}
func (a *App) SessionHasPermissionToUser(session model.Session, userID string) bool {
if userID == "" {
return false
}
if session.IsUnrestricted() {
return true
}
if session.UserId == userID {
return true
}
if a.SessionHasPermissionTo(session, model.PermissionEditOtherUsers) {
return true
}
return false
}
func (a *App) SessionHasPermissionToUserOrBot(session model.Session, userID string) bool {
if session.IsUnrestricted() {
return true
}
if a.SessionHasPermissionToUser(session, userID) {
return true
}
if err := a.SessionHasPermissionToManageBot(session, userID); err == nil {
return true
}
return false
}
func (a *App) HasPermissionTo(askingUserId string, permission *model.Permission) bool {
user, err := a.GetUser(askingUserId)
if err != nil {
return false
}
roles := user.GetRoles()
return a.RolesGrantPermission(roles, permission.Id)
}
func (a *App) HasPermissionToTeam(askingUserId string, teamID string, permission *model.Permission) bool {
if teamID == "" || askingUserId == "" {
return false
}
teamMember, _ := a.GetTeamMember(teamID, askingUserId)
if teamMember != nil && teamMember.DeleteAt == 0 {
if a.RolesGrantPermission(teamMember.GetRoles(), permission.Id) {
return true
}
}
return a.HasPermissionTo(askingUserId, permission)
}
func (a *App) HasPermissionToChannel(c request.CTX, askingUserId string, channelID string, permission *model.Permission) bool {
if channelID == "" || askingUserId == "" {
return false
}
channelMember, err := a.GetChannelMember(c, channelID, askingUserId)
if err == nil {
roles := channelMember.GetRoles()
if a.RolesGrantPermission(roles, permission.Id) {
return true
}
}
var channel *model.Channel
channel, err = a.GetChannel(c, channelID)
if err == nil {
return a.HasPermissionToTeam(askingUserId, channel.TeamId, permission)
}
return a.HasPermissionTo(askingUserId, permission)
}
func (a *App) HasPermissionToChannelByPost(askingUserId string, postID string, permission *model.Permission) bool {
if channelMember, err := a.Srv().Store().Channel().GetMemberForPost(postID, askingUserId); err == nil {
if a.RolesGrantPermission(channelMember.GetRoles(), permission.Id) {
return true
}
}
if channel, err := a.Srv().Store().Channel().GetForPost(postID); err == nil {
return a.HasPermissionToTeam(askingUserId, channel.TeamId, permission)
}
return a.HasPermissionTo(askingUserId, permission)
}
func (a *App) HasPermissionToUser(askingUserId string, userID string) bool {
if askingUserId == userID {
return true
}
if a.HasPermissionTo(askingUserId, model.PermissionEditOtherUsers) {
return true
}
return false
}
func (a *App) RolesGrantPermission(roleNames []string, permissionId string) bool {
roles, err := a.GetRolesByNames(roleNames)
if err != nil {
// This should only happen if something is very broken. We can't realistically
// recover the situation, so deny permission and log an error.
mlog.Error("Failed to get roles from database with role names: "+strings.Join(roleNames, ",")+" ", mlog.Err(err))
return false
}
for _, role := range roles {
if role.DeleteAt != 0 {
continue
}
permissions := role.Permissions
for _, permission := range permissions {
if permission == permissionId {
return true
}
}
}
return false
}
// SessionHasPermissionToManageBot returns nil if the session has access to manage the given bot.
// This function deviates from other authorization checks in returning an error instead of just
// a boolean, allowing the permission failure to be exposed with more granularity.
func (a *App) SessionHasPermissionToManageBot(session model.Session, botUserId string) *model.AppError {
existingBot, err := a.GetBot(botUserId, true)
if err != nil {
return err
}
if session.IsUnrestricted() {
return nil
}
if existingBot.OwnerId == session.UserId {
if !a.SessionHasPermissionTo(session, model.PermissionManageBots) {
if !a.SessionHasPermissionTo(session, model.PermissionReadBots) {
// If the user doesn't have permission to read bots, pretend as if
// the bot doesn't exist at all.
return model.MakeBotNotFoundError(botUserId)
}
return a.MakePermissionError(&session, []*model.Permission{model.PermissionManageBots})
}
} else {
if !a.SessionHasPermissionTo(session, model.PermissionManageOthersBots) {
if !a.SessionHasPermissionTo(session, model.PermissionReadOthersBots) {
// If the user doesn't have permission to read others' bots,
// pretend as if the bot doesn't exist at all.
return model.MakeBotNotFoundError(botUserId)
}
return a.MakePermissionError(&session, []*model.Permission{model.PermissionManageOthersBots})
}
}
return nil
}
func (a *App) HasPermissionToReadChannel(c request.CTX, userID string, channel *model.Channel) bool {
return a.HasPermissionToChannel(c, userID, channel.Id, model.PermissionReadChannel) || (channel.Type == model.ChannelTypeOpen && a.HasPermissionToTeam(userID, channel.TeamId, model.PermissionReadPublicChannel))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
)
// check if there is any auto_response type post in channel by the user in a calender day
func (a *App) checkIfRespondedToday(createdAt int64, channelId, userId string) (bool, error) {
y, m, d := model.GetTimeForMillis(createdAt).Date()
since := model.GetMillisForTime(time.Date(y, m, d, 0, 0, 0, 0, time.UTC))
return a.Srv().Store().Post().HasAutoResponsePostByUserSince(
model.GetPostsSinceOptions{ChannelId: channelId, Time: since},
userId,
)
}
func (a *App) SendAutoResponseIfNecessary(c request.CTX, channel *model.Channel, sender *model.User, post *model.Post) (bool, *model.AppError) {
if channel.Type != model.ChannelTypeDirect {
return false, nil
}
if sender.IsBot {
return false, nil
}
receiverId := channel.GetOtherUserIdForDM(sender.Id)
if receiverId == "" {
// User direct messaged themself, let them test their auto-responder.
receiverId = sender.Id
}
receiver, aErr := a.GetUser(receiverId)
if aErr != nil {
return false, aErr
}
autoResponded, err := a.checkIfRespondedToday(post.CreateAt, post.ChannelId, receiverId)
if err != nil {
return false, model.NewAppError("SendAutoResponseIfNecessary", "app.user.send_auto_response.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if autoResponded {
return false, nil
}
return a.SendAutoResponse(c, channel, receiver, post)
}
func (a *App) SendAutoResponse(c request.CTX, channel *model.Channel, receiver *model.User, post *model.Post) (bool, *model.AppError) {
if receiver == nil || receiver.NotifyProps == nil {
return false, nil
}
active := receiver.NotifyProps[model.AutoResponderActiveNotifyProp] == "true"
message := receiver.NotifyProps[model.AutoResponderMessageNotifyProp]
if !active || message == "" {
return false, nil
}
rootID := post.Id
if post.RootId != "" {
rootID = post.RootId
}
autoResponderPost := &model.Post{
ChannelId: channel.Id,
Message: message,
RootId: rootID,
Type: model.PostTypeAutoResponder,
UserId: receiver.Id,
}
if _, err := a.CreatePost(c, autoResponderPost, channel, false, false); err != nil {
return false, err
}
return true, nil
}
func (a *App) SetAutoResponderStatus(user *model.User, oldNotifyProps model.StringMap) {
active := user.NotifyProps[model.AutoResponderActiveNotifyProp] == "true"
oldActive := oldNotifyProps[model.AutoResponderActiveNotifyProp] == "true"
autoResponderEnabled := !oldActive && active
autoResponderDisabled := oldActive && !active
if autoResponderEnabled {
a.SetStatusOutOfOffice(user.Id)
} else if autoResponderDisabled {
a.SetStatusOnline(user.Id, true)
}
}
func (a *App) DisableAutoResponder(c request.CTX, userID string, asAdmin bool) *model.AppError {
user, err := a.GetUser(userID)
if err != nil {
return err
}
active := user.NotifyProps[model.AutoResponderActiveNotifyProp] == "true"
if active {
patch := &model.UserPatch{}
patch.NotifyProps = user.NotifyProps
patch.NotifyProps[model.AutoResponderActiveNotifyProp] = "false"
_, err := a.PatchUser(c, userID, patch, asAdmin)
if err != nil {
return err
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
internalKeyPrefix = "mmi_"
botUserKey = internalKeyPrefix + "botid"
)
// Ensure bot service wrapper implements `product.BotService`
var _ product.BotService = (*botServiceWrapper)(nil)
// botServiceWrapper provides an implementation of `product.BotService` for use by products.
type botServiceWrapper struct {
app AppIface
}
func (w *botServiceWrapper) EnsureBot(c *request.Context, productID string, bot *model.Bot) (string, error) {
return w.app.EnsureBot(c, productID, bot)
}
// EnsureBot provides similar functionality with the plugin-api BotService. It doesn't accept
// any ensureBotOptions hence it is not required for now.
// TODO: Once the focalboard migration completed, we should add this logic to the app and
// let plugin-api use the same code
func (a *App) EnsureBot(c request.CTX, productID string, bot *model.Bot) (string, error) {
if bot == nil {
return "", errors.New("passed a nil bot")
}
if bot.Username == "" {
return "", errors.New("passed a bot with no username")
}
botIDBytes, err := a.GetPluginKey(productID, botUserKey)
if err != nil {
return "", err
}
// If the bot has already been created, use it
if botIDBytes != nil {
botID := string(botIDBytes)
// ensure existing bot is synced with what is being created
botPatch := &model.BotPatch{
Username: &bot.Username,
DisplayName: &bot.DisplayName,
Description: &bot.Description,
}
if _, err = a.PatchBot(botID, botPatch); err != nil {
return "", fmt.Errorf("failed to patch bot: %w", err)
}
return botID, nil
}
// Check for an existing bot user with that username. If one exists, then use that.
if user, appErr := a.GetUserByUsername(bot.Username); appErr == nil && user != nil {
if user.IsBot {
if appErr := a.SetPluginKey(productID, botUserKey, []byte(user.Id)); appErr != nil {
return "", fmt.Errorf("failed to set plugin key: %w", err)
}
} else {
c.Logger().Error("Product attempted to use an account that already exists. Convert user to a bot "+
"account in the CLI by running 'mattermost user convert <username> --bot'. If the user is an "+
"existing user account you want to preserve, change its username and restart the Mattermost server, "+
"after which the plugin will create a bot account with that name. For more information about bot "+
"accounts, see https://mattermost.com/pl/default-bot-accounts", mlog.String("username",
bot.Username),
mlog.String("user_id",
user.Id),
)
}
return user.Id, nil
}
createdBot, err := a.CreateBot(c, bot)
if err != nil {
return "", fmt.Errorf("failed to create bot: %w", err)
}
if appErr := a.SetPluginKey(productID, botUserKey, []byte(createdBot.UserId)); appErr != nil {
return "", fmt.Errorf("failed to set plugin key: %w", err)
}
return createdBot.UserId, nil
}
// CreateBot creates the given bot and corresponding user.
func (a *App) CreateBot(c request.CTX, bot *model.Bot) (*model.Bot, *model.AppError) {
vErr := bot.IsValidCreate()
if vErr != nil {
return nil, vErr
}
user, nErr := a.Srv().Store().User().Save(model.UserFromBot(bot))
if nErr != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(nErr, &appErr):
return nil, appErr
case errors.As(nErr, &invErr):
code := ""
switch invErr.Field {
case "email":
code = "app.user.save.email_exists.app_error"
case "username":
code = "app.user.save.username_exists.app_error"
default:
code = "app.user.save.existing.app_error"
}
return nil, model.NewAppError("CreateBot", code, nil, "", http.StatusBadRequest).Wrap(nErr)
default:
return nil, model.NewAppError("CreateBot", "app.user.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
bot.UserId = user.Id
savedBot, nErr := a.Srv().Store().Bot().Save(bot)
if nErr != nil {
a.Srv().Store().User().PermanentDelete(bot.UserId)
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
return nil, appErr
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("CreateBot", "app.bot.createbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
// Get the owner of the bot, if one exists. If not, don't send a message
ownerUser, err := a.Srv().Store().User().Get(context.Background(), bot.OwnerId)
var nfErr *store.ErrNotFound
if err != nil && !errors.As(err, &nfErr) {
return nil, model.NewAppError("CreateBot", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
} else if ownerUser != nil {
// Send a message to the bot's creator to inform them that the bot needs to be added
// to a team and channel after it's created
botOwner, err := a.GetUser(bot.OwnerId)
if err != nil {
return nil, err
}
channel, err := a.getOrCreateDirectChannelWithUser(c, user, botOwner)
if err != nil {
return nil, err
}
T := i18n.GetUserTranslations(ownerUser.Locale)
botAddPost := &model.Post{
Type: model.PostTypeAddBotTeamsChannels,
UserId: savedBot.UserId,
ChannelId: channel.Id,
Message: T("api.bot.teams_channels.add_message_mobile"),
}
if _, err := a.CreatePostAsUser(c, botAddPost, c.Session().Id, true); err != nil {
return nil, err
}
}
return savedBot, nil
}
func (a *App) GetWarnMetricsBot() (*model.Bot, *model.AppError) {
perPage := 1
userOptions := &model.UserGetOptions{
Page: 0,
PerPage: perPage,
Role: model.SystemAdminRoleId,
Inactive: false,
}
sysAdminList, err := a.GetUsersFromProfiles(userOptions)
if err != nil {
return nil, err
}
if len(sysAdminList) == 0 {
return nil, model.NewAppError("GetWarnMetricsBot", "app.bot.get_warn_metrics_bot.empty_admin_list.app_error", nil, "", http.StatusInternalServerError)
}
T := i18n.GetUserTranslations(sysAdminList[0].Locale)
warnMetricsBot := &model.Bot{
Username: model.BotWarnMetricBotUsername,
DisplayName: T("app.system.warn_metric.bot_displayname"),
Description: "",
OwnerId: sysAdminList[0].Id,
}
return a.getOrCreateBot(warnMetricsBot)
}
func (a *App) GetSystemBot() (*model.Bot, *model.AppError) {
perPage := 1
userOptions := &model.UserGetOptions{
Page: 0,
PerPage: perPage,
Role: model.SystemAdminRoleId,
Inactive: false,
}
sysAdminList, err := a.GetUsersFromProfiles(userOptions)
if err != nil {
return nil, err
}
if len(sysAdminList) == 0 {
return nil, model.NewAppError("GetSystemBot", "app.bot.get_system_bot.empty_admin_list.app_error", nil, "", http.StatusInternalServerError)
}
T := i18n.GetUserTranslations(sysAdminList[0].Locale)
systemBot := &model.Bot{
Username: model.BotSystemBotUsername,
DisplayName: T("app.system.system_bot.bot_displayname"),
Description: "",
OwnerId: sysAdminList[0].Id,
}
return a.getOrCreateBot(systemBot)
}
func (a *App) getOrCreateBot(botDef *model.Bot) (*model.Bot, *model.AppError) {
botUser, appErr := a.GetUserByUsername(botDef.Username)
if appErr != nil {
if appErr.StatusCode != http.StatusNotFound {
return nil, appErr
}
// cannot find this bot user, save the user
user, nErr := a.Srv().Store().User().Save(model.UserFromBot(botDef))
if nErr != nil {
var appError *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(nErr, &appError):
return nil, appError
case errors.As(nErr, &invErr):
code := ""
switch invErr.Field {
case "email":
code = "app.user.save.email_exists.app_error"
case "username":
code = "app.user.save.username_exists.app_error"
default:
code = "app.user.save.existing.app_error"
}
return nil, model.NewAppError("getOrCreateBot", code, nil, "", http.StatusBadRequest).Wrap(nErr)
default:
return nil, model.NewAppError("getOrCreateBot", "app.user.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
botDef.UserId = user.Id
//save the bot
savedBot, nErr := a.Srv().Store().Bot().Save(botDef)
if nErr != nil {
a.Srv().Store().User().PermanentDelete(savedBot.UserId)
var nAppErr *model.AppError
switch {
case errors.As(nErr, &nAppErr): // in case we haven't converted to plain error.
return nil, nAppErr
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("getOrCreateBot", "app.bot.createbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return savedBot, nil
}
if botUser == nil {
return nil, model.NewAppError("getOrCreateBot", "app.bot.createbot.internal_error", nil, "", http.StatusInternalServerError)
}
//return the bot for this user
savedBot, appErr := a.GetBot(botUser.Id, false)
if appErr != nil {
return nil, appErr
}
return savedBot, nil
}
// PatchBot applies the given patch to the bot and corresponding user.
func (a *App) PatchBot(botUserId string, botPatch *model.BotPatch) (*model.Bot, *model.AppError) {
bot, err := a.GetBot(botUserId, true)
if err != nil {
return nil, err
}
if !bot.WouldPatch(botPatch) {
return bot, nil
}
bot.Patch(botPatch)
user, nErr := a.Srv().Store().User().Get(context.Background(), botUserId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("PatchBot", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("PatchBot", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
patchedUser := model.UserFromBot(bot)
user.Id = patchedUser.Id
user.Username = patchedUser.Username
user.Email = patchedUser.Email
user.FirstName = patchedUser.FirstName
userUpdate, nErr := a.Srv().Store().User().Update(user, true)
if nErr != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
var conErr *store.ErrConflict
switch {
case errors.As(nErr, &appErr):
return nil, appErr
case errors.As(nErr, &invErr):
return nil, model.NewAppError("PatchBot", "app.user.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &conErr):
if conErr.Resource == "Username" {
return nil, model.NewAppError("PatchBot", "app.user.save.username_exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
}
return nil, model.NewAppError("PatchBot", "app.user.save.email_exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
default:
return nil, model.NewAppError("PatchBot", "app.user.update.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
a.InvalidateCacheForUser(user.Id)
ruser := userUpdate.New
a.sendUpdatedUserEvent(*ruser)
bot, nErr = a.Srv().Store().Bot().Update(bot)
if nErr != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
switch {
case errors.As(nErr, &nfErr):
return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(nErr)
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
return nil, appErr
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("PatchBot", "app.bot.patchbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return bot, nil
}
// GetBot returns the given bot.
func (a *App) GetBot(botUserId string, includeDeleted bool) (*model.Bot, *model.AppError) {
bot, err := a.Srv().Store().Bot().Get(botUserId, includeDeleted)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(err)
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("GetBot", "app.bot.getbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return bot, nil
}
// GetBots returns the requested page of bots.
func (a *App) GetBots(options *model.BotGetOptions) (model.BotList, *model.AppError) {
bots, err := a.Srv().Store().Bot().GetAll(options)
if err != nil {
return nil, model.NewAppError("GetBots", "app.bot.getbots.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return bots, nil
}
// UpdateBotActive marks a bot as active or inactive, along with its corresponding user.
func (a *App) UpdateBotActive(c request.CTX, botUserId string, active bool) (*model.Bot, *model.AppError) {
user, nErr := a.Srv().Store().User().Get(context.Background(), botUserId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("PatchBot", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("PatchBot", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if _, err := a.UpdateActive(c, user, active); err != nil {
return nil, err
}
bot, nErr := a.Srv().Store().Bot().Get(botUserId, true)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(nErr)
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("UpdateBotActive", "app.bot.getbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
changed := true
if active && bot.DeleteAt != 0 {
bot.DeleteAt = 0
} else if !active && bot.DeleteAt == 0 {
bot.DeleteAt = model.GetMillis()
} else {
changed = false
}
if changed {
bot, nErr = a.Srv().Store().Bot().Update(bot)
if nErr != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
switch {
case errors.As(nErr, &nfErr):
return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(nErr)
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
return nil, appErr
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("PatchBot", "app.bot.patchbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
}
return bot, nil
}
// PermanentDeleteBot permanently deletes a bot and its corresponding user.
func (a *App) PermanentDeleteBot(botUserId string) *model.AppError {
if err := a.Srv().Store().Bot().PermanentDelete(botUserId); err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return model.NewAppError("PermanentDeleteBot", "app.bot.permenent_delete.bad_id", map[string]any{"user_id": invErr.Value}, "", http.StatusBadRequest).Wrap(err)
default: // last fallback in case it doesn't map to an existing app error.
return model.NewAppError("PatchBot", "app.bot.permanent_delete.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if err := a.Srv().Store().User().PermanentDelete(botUserId); err != nil {
return model.NewAppError("PermanentDeleteBot", "app.user.permanent_delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
// UpdateBotOwner changes a bot's owner to the given value.
func (a *App) UpdateBotOwner(botUserId, newOwnerId string) (*model.Bot, *model.AppError) {
bot, err := a.Srv().Store().Bot().Get(botUserId, true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(err)
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("UpdateBotOwner", "app.bot.getbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
bot.OwnerId = newOwnerId
bot, err = a.Srv().Store().Bot().Update(bot)
if err != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
switch {
case errors.As(err, &nfErr):
return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(err)
case errors.As(err, &appErr): // in case we haven't converted to plain error.
return nil, appErr
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("PatchBot", "app.bot.patchbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return bot, nil
}
// disableUserBots disables all bots owned by the given user.
func (a *App) disableUserBots(c request.CTX, userID string) *model.AppError {
perPage := 20
for {
options := &model.BotGetOptions{
OwnerId: userID,
IncludeDeleted: false,
OnlyOrphaned: false,
Page: 0,
PerPage: perPage,
}
userBots, err := a.GetBots(options)
if err != nil {
return err
}
for _, bot := range userBots {
_, err := a.UpdateBotActive(c, bot.UserId, false)
if err != nil {
c.Logger().Warn("Unable to deactivate bot.", mlog.String("bot_user_id", bot.UserId), mlog.Err(err))
}
}
// Get next set of bots if we got the max number of bots
if len(userBots) == perPage {
options.Page += 1
continue
}
break
}
return nil
}
func (a *App) notifySysadminsBotOwnerDeactivated(c request.CTX, userID string) *model.AppError {
perPage := 25
botOptions := &model.BotGetOptions{
OwnerId: userID,
IncludeDeleted: false,
OnlyOrphaned: false,
Page: 0,
PerPage: perPage,
}
// get owner bots
var userBots []*model.Bot
for {
bots, err := a.GetBots(botOptions)
if err != nil {
return err
}
userBots = append(userBots, bots...)
if len(bots) < perPage {
break
}
botOptions.Page += 1
}
// user does not own bots
if len(userBots) == 0 {
return nil
}
userOptions := &model.UserGetOptions{
Page: 0,
PerPage: perPage,
Role: model.SystemAdminRoleId,
Inactive: false,
}
// get sysadmins
var sysAdmins []*model.User
for {
sysAdminsList, err := a.GetUsersFromProfiles(userOptions)
if err != nil {
return err
}
sysAdmins = append(sysAdmins, sysAdminsList...)
if len(sysAdminsList) < perPage {
break
}
userOptions.Page += 1
}
// user being disabled
user, err := a.GetUser(userID)
if err != nil {
return err
}
// for each sysadmin, notify user that owns bots was disabled
for _, sysAdmin := range sysAdmins {
channel, appErr := a.GetOrCreateDirectChannel(c, sysAdmin.Id, sysAdmin.Id)
if appErr != nil {
return appErr
}
post := &model.Post{
UserId: sysAdmin.Id,
ChannelId: channel.Id,
Message: a.getDisableBotSysadminMessage(user, userBots),
Type: model.PostTypeSystemGeneric,
}
_, appErr = a.CreatePost(c, post, channel, false, true)
if appErr != nil {
return appErr
}
}
return nil
}
func (a *App) getDisableBotSysadminMessage(user *model.User, userBots model.BotList) string {
disableBotsSetting := *a.Config().ServiceSettings.DisableBotsWhenOwnerIsDeactivated
var printAllBots = true
numBotsToPrint := len(userBots)
if numBotsToPrint > 10 {
numBotsToPrint = 10
printAllBots = false
}
var message, botList string
for _, bot := range userBots[:numBotsToPrint] {
botList += fmt.Sprintf("* %v\n", bot.Username)
}
T := i18n.GetUserTranslations(user.Locale)
message = T("app.bot.get_disable_bot_sysadmin_message",
map[string]any{
"UserName": user.Username,
"NumBots": len(userBots),
"BotNames": botList,
"disableBotsSetting": disableBotsSetting,
"printAllBots": printAllBots,
})
return message
}
// ConvertUserToBot converts a user to bot.
func (a *App) ConvertUserToBot(user *model.User) (*model.Bot, *model.AppError) {
bot, err := a.Srv().Store().Bot().Save(model.BotFromUser(user))
if err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr): // in case we haven't converted to plain error.
return nil, appErr
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("CreateBot", "app.bot.createbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return bot, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"mime/multipart"
"net/http"
"time"
"github.com/mattermost/mattermost-server/v6/model"
)
const (
BrandFilePath = "brand/"
BrandFileName = "image.png"
)
func (a *App) SaveBrandImage(imageData *multipart.FileHeader) *model.AppError {
if *a.Config().FileSettings.DriverName == "" {
return model.NewAppError("SaveBrandImage", "api.admin.upload_brand_image.storage.app_error", nil, "", http.StatusNotImplemented)
}
file, err := imageData.Open()
if err != nil {
return model.NewAppError("SaveBrandImage", "brand.save_brand_image.open.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
defer file.Close()
if err = checkImageLimits(file, *a.Config().FileSettings.MaxImageResolution); err != nil {
return model.NewAppError("SaveBrandImage", "brand.save_brand_image.check_image_limits.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
img, _, err := a.ch.imgDecoder.Decode(file)
if err != nil {
return model.NewAppError("SaveBrandImage", "brand.save_brand_image.decode.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
buf := new(bytes.Buffer)
err = a.ch.imgEncoder.EncodePNG(buf, img)
if err != nil {
return model.NewAppError("SaveBrandImage", "brand.save_brand_image.encode.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
t := time.Now()
a.MoveFile(BrandFilePath+BrandFileName, BrandFilePath+t.Format("2006-01-02T15:04:05")+".png")
if _, err := a.WriteFile(buf, BrandFilePath+BrandFileName); err != nil {
return model.NewAppError("SaveBrandImage", "brand.save_brand_image.save_image.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) GetBrandImage() ([]byte, *model.AppError) {
if *a.Config().FileSettings.DriverName == "" {
return nil, model.NewAppError("GetBrandImage", "api.admin.get_brand_image.storage.app_error", nil, "", http.StatusNotImplemented)
}
img, err := a.ReadFile(BrandFilePath + BrandFileName)
if err != nil {
return nil, err
}
return img, nil
}
func (a *App) DeleteBrandImage() *model.AppError {
filePath := BrandFilePath + BrandFileName
fileExists, err := a.FileExists(filePath)
if err != nil {
return err
}
if !fileExists {
return model.NewAppError("DeleteBrandImage", "api.admin.delete_brand_image.storage.not_found", nil, "", http.StatusNotFound)
}
return a.RemoveFile(filePath)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
)
const (
TimestampFormat = "Mon Jan 2 15:04:05 -0700 MST 2006"
)
// Busy represents the busy state of the server. A server marked busy
// will have non-critical services disabled. If a Cluster is provided
// any changes will be propagated to each node.
type Busy struct {
busy int32 // protected via atomic for fast IsBusy calls
mux sync.RWMutex
timer *time.Timer
expires time.Time
cluster einterfaces.ClusterInterface
}
// NewBusy creates a new Busy instance with optional cluster which will
// be notified of busy state changes.
func NewBusy(cluster einterfaces.ClusterInterface) *Busy {
return &Busy{cluster: cluster}
}
// IsBusy returns true if the server has been marked as busy.
func (b *Busy) IsBusy() bool {
if b == nil {
return false
}
return atomic.LoadInt32(&b.busy) != 0
}
// Set marks the server as busy for dur duration and notifies cluster nodes.
func (b *Busy) Set(dur time.Duration) {
b.mux.Lock()
defer b.mux.Unlock()
// minimum 1 second
if dur < (time.Second * 1) {
dur = time.Second * 1
}
b.setWithoutNotify(dur)
if b.cluster != nil {
sbs := &model.ServerBusyState{Busy: true, Expires: b.expires.Unix(), ExpiresTS: b.expires.UTC().Format(TimestampFormat)}
b.notifyServerBusyChange(sbs)
}
}
// must hold mutex
func (b *Busy) setWithoutNotify(dur time.Duration) {
b.clearWithoutNotify()
atomic.StoreInt32(&b.busy, 1)
b.expires = time.Now().Add(dur)
b.timer = time.AfterFunc(dur, func() {
b.mux.Lock()
b.clearWithoutNotify()
b.mux.Unlock()
})
}
// ClearBusy marks the server as not busy and notifies cluster nodes.
func (b *Busy) Clear() {
b.mux.Lock()
defer b.mux.Unlock()
b.clearWithoutNotify()
if b.cluster != nil {
sbs := &model.ServerBusyState{Busy: false, Expires: time.Time{}.Unix(), ExpiresTS: ""}
b.notifyServerBusyChange(sbs)
}
}
// must hold mutex
func (b *Busy) clearWithoutNotify() {
if b.timer != nil {
b.timer.Stop() // don't drain timer.C channel for AfterFunc timers.
}
b.timer = nil
b.expires = time.Time{}
atomic.StoreInt32(&b.busy, 0)
}
// Expires returns the expected time that the server
// will be marked not busy. This expiry can be extended
// via additional calls to SetBusy.
func (b *Busy) Expires() time.Time {
b.mux.RLock()
defer b.mux.RUnlock()
return b.expires
}
// notifyServerBusyChange informs all cluster members of a server busy state change.
func (b *Busy) notifyServerBusyChange(sbs *model.ServerBusyState) {
if b.cluster == nil {
return
}
buf, _ := json.Marshal(sbs)
msg := &model.ClusterMessage{
Event: model.ClusterEventBusyStateChanged,
SendType: model.ClusterSendReliable,
WaitForAllToSend: true,
Data: buf,
}
b.cluster.SendClusterMessage(msg)
}
// ClusterEventChanged is called when a CLUSTER_EVENT_BUSY_STATE_CHANGED is received.
func (b *Busy) ClusterEventChanged(sbs *model.ServerBusyState) {
b.mux.Lock()
defer b.mux.Unlock()
if sbs.Busy {
expires := time.Unix(sbs.Expires, 0)
dur := time.Until(expires)
if dur > 0 {
b.setWithoutNotify(dur)
}
} else {
b.clearWithoutNotify()
}
}
func (b *Busy) ToJSON() ([]byte, error) {
b.mux.RLock()
defer b.mux.RUnlock()
sbs := &model.ServerBusyState{
Busy: atomic.LoadInt32(&b.busy) != 0,
Expires: b.expires.Unix(),
ExpiresTS: b.expires.UTC().Format(TimestampFormat),
}
sbsJSON, jsonErr := json.Marshal(sbs)
if jsonErr != nil {
return []byte{}, fmt.Errorf("failed to encode server busy state to JSON: %w", jsonErr)
}
return sbsJSON, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/mattermost/logr/v2"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// channelsWrapper provides an implementation of `product.ChannelService` to be used by products.
type channelsWrapper struct {
app *App
}
func (s *channelsWrapper) GetDirectChannel(userID1, userID2 string) (*model.Channel, *model.AppError) {
return s.app.getDirectChannel(request.EmptyContext(s.app.Log()), userID1, userID2)
}
// GetChannelByID gets a Channel by its ID.
func (s *channelsWrapper) GetChannelByID(channelID string) (*model.Channel, *model.AppError) {
return s.app.GetChannel(request.EmptyContext(s.app.Log()), channelID)
}
// GetChannelMember gets a channel member by userID.
func (s *channelsWrapper) GetChannelMember(channelID string, userID string) (*model.ChannelMember, *model.AppError) {
return s.app.GetChannelMember(request.EmptyContext(s.app.Log()), channelID, userID)
}
func (s *channelsWrapper) GetChannelsForTeamForUser(teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, *model.AppError) {
return s.app.GetChannelsForTeamForUser(request.EmptyContext(s.app.Log()), teamID, userID, opts)
}
func (s *channelsWrapper) GetChannelSidebarCategories(userID, teamID string) (*model.OrderedSidebarCategories, *model.AppError) {
return s.app.GetSidebarCategoriesForTeamForUser(request.EmptyContext(s.app.Log()), userID, teamID)
}
func (s *channelsWrapper) GetChannelMembers(channelID string, page, perPage int) (model.ChannelMembers, *model.AppError) {
return s.app.GetChannelMembersPage(request.EmptyContext(s.app.Log()), channelID, page, perPage)
}
func (s *channelsWrapper) CreateChannelSidebarCategory(userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) {
return s.app.CreateSidebarCategory(request.EmptyContext(s.app.Log()), userID, teamID, newCategory)
}
func (s *channelsWrapper) UpdateChannelSidebarCategories(userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) {
return s.app.UpdateSidebarCategories(request.EmptyContext(s.app.Log()), userID, teamID, categories)
}
func (s *channelsWrapper) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
return s.app.CreateChannel(request.EmptyContext(s.app.Log()), channel, false)
}
func (s *channelsWrapper) AddUserToChannel(channelID, userID, asUserID string) (*model.ChannelMember, *model.AppError) {
ctx := request.EmptyContext(s.app.Log())
channel, err := s.app.GetChannel(ctx, channelID)
if err != nil {
return nil, err
}
return s.app.AddChannelMember(ctx, userID, channel, ChannelMemberOpts{
UserRequestorID: asUserID,
})
}
func (s *channelsWrapper) UpdateChannelMemberRoles(channelID, userID, newRoles string) (*model.ChannelMember, *model.AppError) {
return s.app.UpdateChannelMemberRoles(request.EmptyContext(s.app.Log()), channelID, userID, newRoles)
}
func (s *channelsWrapper) DeleteChannelMember(channelID, userID string) *model.AppError {
return s.app.LeaveChannel(request.EmptyContext(s.app.Log()), channelID, userID)
}
func (s *channelsWrapper) AddChannelMember(channelID, userID string) (*model.ChannelMember, *model.AppError) {
channel, err := s.GetChannelByID(channelID)
if err != nil {
return nil, err
}
return s.app.AddChannelMember(request.EmptyContext(s.app.Log()), userID, channel, ChannelMemberOpts{
// For now, don't allow overriding these via the plugin API.
UserRequestorID: "",
PostRootID: "",
})
}
func (s *channelsWrapper) GetDirectChannelOrCreate(userID1, userID2 string) (*model.Channel, *model.AppError) {
return s.app.GetOrCreateDirectChannel(request.EmptyContext(s.app.Log()), userID1, userID2)
}
// Ensure the wrapper implements the product service.
var _ product.ChannelService = (*channelsWrapper)(nil)
// DefaultChannelNames returns the list of system-wide default channel names.
//
// By default the list will be (not necessarily in this order):
//
// ['town-square', 'off-topic']
//
// However, if TeamSettings.ExperimentalDefaultChannels contains a list of channels then that list will replace
// 'off-topic' and be included in the return results in addition to 'town-square'. For example:
//
// ['town-square', 'game-of-thrones', 'wow']
func (a *App) DefaultChannelNames(c request.CTX) []string {
names := []string{"town-square"}
if len(a.Config().TeamSettings.ExperimentalDefaultChannels) == 0 {
names = append(names, "off-topic")
} else {
seenChannels := map[string]bool{"town-square": true}
for _, channelName := range a.Config().TeamSettings.ExperimentalDefaultChannels {
if !seenChannels[channelName] {
names = append(names, channelName)
seenChannels[channelName] = true
}
}
}
return names
}
func (a *App) JoinDefaultChannels(c request.CTX, teamID string, user *model.User, shouldBeAdmin bool, userRequestorId string) *model.AppError {
var requestor *model.User
var nErr error
if userRequestorId != "" {
requestor, nErr = a.Srv().Store().User().Get(context.Background(), userRequestorId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return model.NewAppError("JoinDefaultChannels", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return model.NewAppError("JoinDefaultChannels", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
}
for _, channelName := range a.DefaultChannelNames(c) {
channel, channelErr := a.Srv().Store().Channel().GetByName(teamID, channelName, true)
if channelErr != nil {
c.Logger().Warn("No default channel with this name", mlog.String("channelName", channelName), mlog.String("teamID", teamID), mlog.Err(channelErr))
continue
}
if channel.Type != model.ChannelTypeOpen {
continue
}
cm := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
SchemeGuest: user.IsGuest(),
SchemeUser: !user.IsGuest(),
SchemeAdmin: shouldBeAdmin,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, nErr = a.Srv().Store().Channel().SaveMember(cm)
if histErr := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis()); histErr != nil {
return model.NewAppError("JoinDefaultChannels", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(histErr)
}
if *a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages {
if aErr := a.postJoinMessageForDefaultChannel(c, user, requestor, channel); aErr != nil {
c.Logger().Warn("Failed to post join/leave message", mlog.Err(aErr))
}
}
a.invalidateCacheForChannelMembers(channel.Id)
message := model.NewWebSocketEvent(model.WebsocketEventUserAdded, "", channel.Id, "", nil, "")
message.Add("user_id", user.Id)
message.Add("team_id", channel.TeamId)
a.Publish(message)
// A/B Test on the welcome post
if a.Config().FeatureFlags.SendWelcomePost && channelName == model.DefaultChannelName {
nbTeams, err := a.Srv().Store().Team().AnalyticsTeamCount(&model.TeamSearch{
IncludeDeleted: model.NewBool(true),
})
if err != nil {
c.Logger().Warn("unable to get number of teams", logr.Err(err))
return nil
}
if nbTeams == 1 && a.IsFirstAdmin(user) {
// Post the welcome message
if _, err := a.CreatePost(c, &model.Post{
ChannelId: channel.Id,
Type: model.PostTypeWelcomePost,
UserId: user.Id,
}, channel, false, false); err != nil {
c.Logger().Warn("unable to post welcome message", logr.Err(err))
return nil
}
ts := a.Srv().GetTelemetryService()
if ts != nil {
ts.SendTelemetry("welcome-message-sent", map[string]any{
"category": "growth",
})
}
}
}
}
if nErr != nil {
var appErr *model.AppError
var cErr *store.ErrConflict
switch {
case errors.As(nErr, &cErr):
if cErr.Resource == "ChannelMembers" {
return model.NewAppError("JoinDefaultChannels", "app.channel.save_member.exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
}
case errors.As(nErr, &appErr):
return appErr
default:
return model.NewAppError("JoinDefaultChannels", "app.channel.create_direct_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return nil
}
func (a *App) postJoinMessageForDefaultChannel(c request.CTX, user *model.User, requestor *model.User, channel *model.Channel) *model.AppError {
if channel.Name == model.DefaultChannelName {
if requestor == nil {
if err := a.postJoinTeamMessage(c, user, channel); err != nil {
return err
}
} else {
if err := a.postAddToTeamMessage(c, requestor, user, channel, ""); err != nil {
return err
}
}
} else {
if requestor == nil {
if err := a.postJoinChannelMessage(c, user, channel); err != nil {
return err
}
} else {
if err := a.PostAddToChannelMessage(c, requestor, user, channel, ""); err != nil {
return err
}
}
}
return nil
}
func (a *App) CreateChannelWithUser(c request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError) {
if channel.IsGroupOrDirect() {
return nil, model.NewAppError("CreateChannelWithUser", "api.channel.create_channel.direct_channel.app_error", nil, "", http.StatusBadRequest)
}
if channel.TeamId == "" {
return nil, model.NewAppError("CreateChannelWithUser", "app.channel.create_channel.no_team_id.app_error", nil, "", http.StatusBadRequest)
}
// Get total number of channels on current team
count, err := a.GetNumberOfChannelsOnTeam(c, channel.TeamId)
if err != nil {
return nil, err
}
if int64(count+1) > *a.Config().TeamSettings.MaxChannelsPerTeam {
return nil, model.NewAppError("CreateChannelWithUser", "api.channel.create_channel.max_channel_limit.app_error", map[string]any{"MaxChannelsPerTeam": *a.Config().TeamSettings.MaxChannelsPerTeam}, "", http.StatusBadRequest)
}
channel.CreatorId = userID
rchannel, err := a.CreateChannel(c, channel, true)
if err != nil {
return nil, err
}
var user *model.User
if user, err = a.GetUser(userID); err != nil {
return nil, err
}
a.postJoinChannelMessage(c, user, channel)
message := model.NewWebSocketEvent(model.WebsocketEventChannelCreated, "", "", userID, nil, "")
message.Add("channel_id", channel.Id)
message.Add("team_id", channel.TeamId)
a.Publish(message)
return rchannel, nil
}
// RenameChannel is used to rename the channel Name and the DisplayName fields
func (a *App) RenameChannel(c request.CTX, channel *model.Channel, newChannelName string, newDisplayName string) (*model.Channel, *model.AppError) {
if channel.Type == model.ChannelTypeDirect {
return nil, model.NewAppError("RenameChannel", "api.channel.rename_channel.cant_rename_direct_messages.app_error", nil, "", http.StatusBadRequest)
}
if channel.Type == model.ChannelTypeGroup {
return nil, model.NewAppError("RenameChannel", "api.channel.rename_channel.cant_rename_group_messages.app_error", nil, "", http.StatusBadRequest)
}
channel.Name = newChannelName
if newDisplayName != "" {
channel.DisplayName = newDisplayName
}
newChannel, err := a.UpdateChannel(c, channel)
if err != nil {
return nil, err
}
return newChannel, nil
}
func (a *App) CreateChannel(c request.CTX, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) {
channel.DisplayName = strings.TrimSpace(channel.DisplayName)
sc, nErr := a.Srv().Store().Channel().Save(channel, *a.Config().TeamSettings.MaxChannelsPerTeam)
if nErr != nil {
var invErr *store.ErrInvalidInput
var cErr *store.ErrConflict
var ltErr *store.ErrLimitExceeded
var appErr *model.AppError
switch {
case errors.As(nErr, &invErr):
switch {
case invErr.Entity == "Channel" && invErr.Field == "DeleteAt":
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save.archived_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case invErr.Entity == "Channel" && invErr.Field == "Type":
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save.direct_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case invErr.Entity == "Channel" && invErr.Field == "Id":
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_channel.existing.app_error", nil, "id="+invErr.Value.(string), http.StatusBadRequest).Wrap(nErr)
}
case errors.As(nErr, &cErr):
return sc, model.NewAppError("CreateChannel", store.ChannelExistsError, nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, <Err):
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_channel.limit.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
return nil, appErr
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("CreateChannel", "app.channel.create_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if addMember {
user, nErr := a.Srv().Store().User().Get(context.Background(), channel.CreatorId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("CreateChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("CreateChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
cm := &model.ChannelMember{
ChannelId: sc.Id,
UserId: user.Id,
SchemeGuest: user.IsGuest(),
SchemeUser: !user.IsGuest(),
SchemeAdmin: true,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
if _, nErr := a.Srv().Store().Channel().SaveMember(cm); nErr != nil {
var appErr *model.AppError
var cErr *store.ErrConflict
switch {
case errors.As(nErr, &cErr):
switch cErr.Resource {
case "ChannelMembers":
return nil, model.NewAppError("CreateChannel", "app.channel.save_member.exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
}
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CreateChannel", "app.channel.create_direct_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if err := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(channel.CreatorId, sc.Id, model.GetMillis()); err != nil {
return nil, model.NewAppError("CreateChannel", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.InvalidateCacheForUser(channel.CreatorId)
}
a.Srv().Go(func() {
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.ChannelHasBeenCreated(pluginContext, sc)
return true
}, plugin.ChannelHasBeenCreatedID)
})
return sc, nil
}
func (a *App) GetOrCreateDirectChannel(c request.CTX, userID, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
channel, nErr := a.getDirectChannel(c, userID, otherUserID)
if nErr != nil {
return nil, nErr
}
if channel != nil {
return channel, nil
}
if *a.Config().TeamSettings.RestrictDirectMessage == model.DirectMessageTeam &&
!a.SessionHasPermissionTo(*c.Session(), model.PermissionManageSystem) {
users, err := a.GetUsersByIds([]string{userID, otherUserID}, &store.UserGetByIdsOpts{})
if err != nil {
return nil, err
}
var isBot bool
for _, user := range users {
if user.IsBot {
isBot = true
break
}
}
// if one of the users is a bot, don't restrict to team members
if !isBot {
commonTeamIDs, err := a.GetCommonTeamIDsForTwoUsers(userID, otherUserID)
if err != nil {
return nil, err
}
if len(commonTeamIDs) == 0 {
return nil, model.NewAppError("createDirectChannel", "api.channel.create_channel.direct_channel.team_restricted_error", nil, "", http.StatusForbidden)
}
}
}
channel, err := a.createDirectChannel(c, userID, otherUserID, channelOptions...)
if err != nil {
if err.Id == store.ChannelExistsError {
return channel, nil
}
return nil, err
}
a.handleCreationEvent(c, userID, otherUserID, channel)
return channel, nil
}
func (a *App) getOrCreateDirectChannelWithUser(c request.CTX, user, otherUser *model.User) (*model.Channel, *model.AppError) {
channel, nErr := a.getDirectChannel(c, user.Id, otherUser.Id)
if nErr != nil {
return nil, nErr
}
if channel != nil {
return channel, nil
}
channel, err := a.createDirectChannelWithUser(c, user, otherUser)
if err != nil {
if err.Id == store.ChannelExistsError {
return channel, nil
}
return nil, err
}
a.handleCreationEvent(c, user.Id, otherUser.Id, channel)
return channel, nil
}
func (a *App) handleCreationEvent(c request.CTX, userID, otherUserID string, channel *model.Channel) {
a.InvalidateCacheForUser(userID)
a.InvalidateCacheForUser(otherUserID)
a.Srv().Go(func() {
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.ChannelHasBeenCreated(pluginContext, channel)
return true
}, plugin.ChannelHasBeenCreatedID)
})
message := model.NewWebSocketEvent(model.WebsocketEventDirectAdded, "", channel.Id, "", nil, "")
message.Add("creator_id", userID)
message.Add("teammate_id", otherUserID)
a.Publish(message)
}
func (a *App) createDirectChannel(c request.CTX, userID string, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
users, err := a.Srv().Store().User().GetMany(context.Background(), []string{userID, otherUserID})
if err != nil {
return nil, model.NewAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if len(users) == 0 {
return nil, model.NewAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, fmt.Sprintf("No users found for ids: %s. %s", userID, otherUserID), http.StatusBadRequest)
}
// We are doing this because we allow a user to create a direct channel with themselves
if userID == otherUserID {
users = append(users, users[0])
}
// After we counted for direct channels with the same user, if we do not have two users then we failed to find one
if len(users) != 2 {
return nil, model.NewAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, fmt.Sprintf("No users found for ids: %s. %s", userID, otherUserID), http.StatusBadRequest)
}
// The potential swap dance below is necessary in order to guarantee determinism when creating a direct channel.
// When we query the database for some given user ids, the database result is not deterministic, meaning we can get
// the same results but in different order. In order to conform the contract of Channel.CreateDirectChannel method
// below we need to identify which user is who.
user := users[0]
otherUser := users[1]
if user.Id != userID {
user = users[1]
otherUser = users[0]
}
return a.createDirectChannelWithUser(c, user, otherUser, channelOptions...)
}
func (a *App) createDirectChannelWithUser(c request.CTX, user, otherUser *model.User, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
channel, nErr := a.Srv().Store().Channel().CreateDirectChannel(user, otherUser, channelOptions...)
if nErr != nil {
var invErr *store.ErrInvalidInput
var cErr *store.ErrConflict
var ltErr *store.ErrLimitExceeded
var appErr *model.AppError
switch {
case errors.As(nErr, &invErr):
switch {
case invErr.Entity == "Channel" && invErr.Field == "DeleteAt":
return nil, model.NewAppError("createDirectChannelWithUser", "store.sql_channel.save.archived_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case invErr.Entity == "Channel" && invErr.Field == "Type":
return nil, model.NewAppError("createDirectChannelWithUser", "store.sql_channel.save_direct_channel.not_direct.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case invErr.Entity == "Channel" && invErr.Field == "Id":
return nil, model.NewAppError("SqlChannelStore.Save", "store.sql_channel.save_channel.existing.app_error", nil, "id="+invErr.Value.(string), http.StatusBadRequest).Wrap(nErr)
}
case errors.As(nErr, &cErr):
switch cErr.Resource {
case "Channel":
return channel, model.NewAppError("createDirectChannelWithUser", store.ChannelExistsError, nil, "", http.StatusBadRequest).Wrap(nErr)
case "ChannelMembers":
return nil, model.NewAppError("createDirectChannelWithUser", "app.channel.save_member.exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
}
case errors.As(nErr, <Err):
return nil, model.NewAppError("createDirectChannelWithUser", "store.sql_channel.save_channel.limit.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
return nil, appErr
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("createDirectChannelWithUser", "app.channel.create_direct_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if err := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis()); err != nil {
return nil, model.NewAppError("createDirectChannelWithUser", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if user.Id != otherUser.Id {
if err := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(otherUser.Id, channel.Id, model.GetMillis()); err != nil {
return nil, model.NewAppError("createDirectChannelWithUser", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// When the newly created channel is shared and the creator is local
// create a local shared channel record
if channel.IsShared() && !user.IsRemote() {
sc := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
Home: true,
ReadOnly: false,
ShareName: channel.Name,
ShareDisplayName: channel.DisplayName,
SharePurpose: channel.Purpose,
ShareHeader: channel.Header,
CreatorId: user.Id,
Type: channel.Type,
}
if _, err := a.SaveSharedChannel(c, sc); err != nil {
return nil, model.NewAppError("CreateDirectChannel", "app.sharedchannel.dm_channel_creation.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return channel, nil
}
func (a *App) CreateGroupChannel(c request.CTX, userIDs []string, creatorId string) (*model.Channel, *model.AppError) {
channel, err := a.createGroupChannel(c, userIDs)
if err != nil {
if err.Id == store.ChannelExistsError {
return channel, nil
}
return nil, err
}
for _, userID := range userIDs {
a.InvalidateCacheForUser(userID)
}
message := model.NewWebSocketEvent(model.WebsocketEventGroupAdded, "", channel.Id, "", nil, "")
message.Add("teammate_ids", model.ArrayToJSON(userIDs))
a.Publish(message)
return channel, nil
}
func (a *App) createGroupChannel(c request.CTX, userIDs []string) (*model.Channel, *model.AppError) {
if len(userIDs) > model.ChannelGroupMaxUsers || len(userIDs) < model.ChannelGroupMinUsers {
return nil, model.NewAppError("CreateGroupChannel", "api.channel.create_group.bad_size.app_error", nil, "", http.StatusBadRequest)
}
users, err := a.Srv().Store().User().GetProfileByIds(context.Background(), userIDs, nil, true)
if err != nil {
return nil, model.NewAppError("createGroupChannel", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(users) != len(userIDs) {
return nil, model.NewAppError("CreateGroupChannel", "api.channel.create_group.bad_user.app_error", nil, "user_ids="+model.ArrayToJSON(userIDs), http.StatusBadRequest)
}
group := &model.Channel{
Name: model.GetGroupNameFromUserIds(userIDs),
DisplayName: model.GetGroupDisplayNameFromUsers(users, true),
Type: model.ChannelTypeGroup,
}
channel, nErr := a.Srv().Store().Channel().Save(group, *a.Config().TeamSettings.MaxChannelsPerTeam)
if nErr != nil {
var invErr *store.ErrInvalidInput
var cErr *store.ErrConflict
var ltErr *store.ErrLimitExceeded
var appErr *model.AppError
switch {
case errors.As(nErr, &invErr):
switch {
case invErr.Entity == "Channel" && invErr.Field == "DeleteAt":
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save.archived_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case invErr.Entity == "Channel" && invErr.Field == "Type":
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save.direct_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case invErr.Entity == "Channel" && invErr.Field == "Id":
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_channel.existing.app_error", nil, "id="+invErr.Value.(string), http.StatusBadRequest).Wrap(nErr)
}
case errors.As(nErr, &cErr):
return channel, model.NewAppError("CreateChannel", store.ChannelExistsError, nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, <Err):
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_channel.limit.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
return nil, appErr
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("CreateChannel", "app.channel.create_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
for _, user := range users {
cm := &model.ChannelMember{
UserId: user.Id,
ChannelId: channel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: user.IsGuest(),
SchemeUser: !user.IsGuest(),
}
if _, nErr = a.Srv().Store().Channel().SaveMember(cm); nErr != nil {
var appErr *model.AppError
var cErr *store.ErrConflict
switch {
case errors.As(nErr, &cErr):
switch cErr.Resource {
case "ChannelMembers":
return nil, model.NewAppError("createGroupChannel", "app.channel.save_member.exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
}
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("createGroupChannel", "app.channel.create_direct_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if err := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis()); err != nil {
return nil, model.NewAppError("createGroupChannel", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return channel, nil
}
func (a *App) GetGroupChannel(c request.CTX, userIDs []string) (*model.Channel, *model.AppError) {
if len(userIDs) > model.ChannelGroupMaxUsers || len(userIDs) < model.ChannelGroupMinUsers {
return nil, model.NewAppError("GetGroupChannel", "api.channel.create_group.bad_size.app_error", nil, "", http.StatusBadRequest)
}
users, err := a.Srv().Store().User().GetProfileByIds(context.Background(), userIDs, nil, true)
if err != nil {
return nil, model.NewAppError("GetGroupChannel", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(users) != len(userIDs) {
return nil, model.NewAppError("GetGroupChannel", "api.channel.create_group.bad_user.app_error", nil, "user_ids="+model.ArrayToJSON(userIDs), http.StatusBadRequest)
}
channel, appErr := a.GetChannelByName(c, model.GetGroupNameFromUserIds(userIDs), "", true)
if appErr != nil {
return nil, appErr
}
return channel, nil
}
// UpdateChannel updates a given channel by its Id. It also publishes the CHANNEL_UPDATED event.
func (a *App) UpdateChannel(c request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) {
_, err := a.Srv().Store().Channel().Update(channel)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("UpdateChannel", "app.channel.update.bad_id", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("UpdateChannel", "app.channel.update_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
a.Srv().Platform().InvalidateCacheForChannel(channel)
messageWs := model.NewWebSocketEvent(model.WebsocketEventChannelUpdated, "", channel.Id, "", nil, "")
channelJSON, jsonErr := json.Marshal(channel)
if jsonErr != nil {
return nil, model.NewAppError("UpdateChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
messageWs.Add("channel", string(channelJSON))
a.Publish(messageWs)
return channel, nil
}
// CreateChannelScheme creates a new Scheme of scope channel and assigns it to the channel.
func (a *App) CreateChannelScheme(c request.CTX, channel *model.Channel) (*model.Scheme, *model.AppError) {
scheme, err := a.CreateScheme(&model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Scope: model.SchemeScopeChannel,
})
if err != nil {
return nil, err
}
channel.SchemeId = &scheme.Id
if _, err := a.UpdateChannelScheme(c, channel); err != nil {
return nil, err
}
return scheme, nil
}
// DeleteChannelScheme deletes a channels scheme and sets its SchemeId to nil.
func (a *App) DeleteChannelScheme(c request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) {
if channel.SchemeId != nil && *channel.SchemeId != "" {
if _, err := a.DeleteScheme(*channel.SchemeId); err != nil {
return nil, err
}
}
channel.SchemeId = nil
return a.UpdateChannelScheme(c, channel)
}
// UpdateChannelScheme saves the new SchemeId of the channel passed.
func (a *App) UpdateChannelScheme(c request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) {
var oldChannel *model.Channel
var err *model.AppError
if oldChannel, err = a.GetChannel(c, channel.Id); err != nil {
return nil, err
}
oldChannel.SchemeId = channel.SchemeId
return a.UpdateChannel(c, oldChannel)
}
func (a *App) UpdateChannelPrivacy(c request.CTX, oldChannel *model.Channel, user *model.User) (*model.Channel, *model.AppError) {
channel, err := a.UpdateChannel(c, oldChannel)
if err != nil {
return channel, err
}
if err := a.postChannelPrivacyMessage(c, user, channel); err != nil {
if channel.Type == model.ChannelTypeOpen {
channel.Type = model.ChannelTypePrivate
} else {
channel.Type = model.ChannelTypeOpen
}
// revert to previous channel privacy
a.UpdateChannel(c, channel)
return channel, err
}
a.Srv().Platform().InvalidateCacheForChannel(channel)
messageWs := model.NewWebSocketEvent(model.WebsocketEventChannelConverted, channel.TeamId, "", "", nil, "")
messageWs.Add("channel_id", channel.Id)
a.Publish(messageWs)
return channel, nil
}
func (a *App) postChannelPrivacyMessage(c request.CTX, user *model.User, channel *model.Channel) *model.AppError {
var authorId string
var authorUsername string
if user != nil {
authorId = user.Id
authorUsername = user.Username
} else {
systemBot, err := a.GetSystemBot()
if err != nil {
return model.NewAppError("postChannelPrivacyMessage", "api.channel.post_channel_privacy_message.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
authorId = systemBot.UserId
authorUsername = systemBot.Username
}
message := (map[model.ChannelType]string{
model.ChannelTypeOpen: i18n.T("api.channel.change_channel_privacy.private_to_public"),
model.ChannelTypePrivate: i18n.T("api.channel.change_channel_privacy.public_to_private"),
})[channel.Type]
post := &model.Post{
ChannelId: channel.Id,
Message: message,
Type: model.PostTypeChangeChannelPrivacy,
UserId: authorId,
Props: model.StringInterface{
"username": authorUsername,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("postChannelPrivacyMessage", "api.channel.post_channel_privacy_message.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) RestoreChannel(c request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError) {
if channel.DeleteAt == 0 {
return nil, model.NewAppError("restoreChannel", "api.channel.restore_channel.restored.app_error", nil, "", http.StatusBadRequest)
}
if err := a.Srv().Store().Channel().Restore(channel.Id, model.GetMillis()); err != nil {
return nil, model.NewAppError("RestoreChannel", "app.channel.restore.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
channel.DeleteAt = 0
a.Srv().Platform().InvalidateCacheForChannel(channel)
message := model.NewWebSocketEvent(model.WebsocketEventChannelRestored, channel.TeamId, "", "", nil, "")
message.Add("channel_id", channel.Id)
a.Publish(message)
var user *model.User
if userID != "" {
var nErr error
user, nErr = a.Srv().Store().User().Get(context.Background(), userID)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("RestoreChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("RestoreChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
}
if user != nil {
T := i18n.GetUserTranslations(user.Locale)
post := &model.Post{
ChannelId: channel.Id,
Message: T("api.channel.restore_channel.unarchived", map[string]any{"Username": user.Username}),
Type: model.PostTypeChannelRestored,
UserId: userID,
Props: model.StringInterface{
"username": user.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
c.Logger().Warn("Failed to post unarchive message", mlog.Err(err))
}
} else {
a.Srv().Go(func() {
systemBot, err := a.GetSystemBot()
if err != nil {
c.Logger().Error("Failed to post unarchive message", mlog.Err(err))
return
}
post := &model.Post{
ChannelId: channel.Id,
Message: i18n.T("api.channel.restore_channel.unarchived", map[string]any{"Username": systemBot.Username}),
Type: model.PostTypeChannelRestored,
UserId: systemBot.UserId,
Props: model.StringInterface{
"username": systemBot.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
c.Logger().Error("Failed to post unarchive message", mlog.Err(err))
}
})
}
return channel, nil
}
func (a *App) PatchChannel(c request.CTX, channel *model.Channel, patch *model.ChannelPatch, userID string) (*model.Channel, *model.AppError) {
oldChannelDisplayName := channel.DisplayName
oldChannelHeader := channel.Header
oldChannelPurpose := channel.Purpose
channel.Patch(patch)
channel, err := a.UpdateChannel(c, channel)
if err != nil {
return nil, err
}
if oldChannelDisplayName != channel.DisplayName {
if err = a.PostUpdateChannelDisplayNameMessage(c, userID, channel, oldChannelDisplayName, channel.DisplayName); err != nil {
c.Logger().Warn(err.Error())
}
}
if channel.Header != oldChannelHeader {
if err = a.PostUpdateChannelHeaderMessage(c, userID, channel, oldChannelHeader, channel.Header); err != nil {
c.Logger().Warn(err.Error())
}
}
if channel.Purpose != oldChannelPurpose {
if err = a.PostUpdateChannelPurposeMessage(c, userID, channel, oldChannelPurpose, channel.Purpose); err != nil {
c.Logger().Warn(err.Error())
}
}
return channel, nil
}
// GetSchemeRolesForChannel Checks if a channel or its team has an override scheme for channel roles and returns the scheme roles or default channel roles.
func (a *App) GetSchemeRolesForChannel(c request.CTX, channelID string) (guestRoleName, userRoleName, adminRoleName string, err *model.AppError) {
channel, err := a.GetChannel(c, channelID)
if err != nil {
return
}
if channel.SchemeId != nil && *channel.SchemeId != "" {
var scheme *model.Scheme
scheme, err = a.GetScheme(*channel.SchemeId)
if err != nil {
return
}
guestRoleName = scheme.DefaultChannelGuestRole
userRoleName = scheme.DefaultChannelUserRole
adminRoleName = scheme.DefaultChannelAdminRole
return
}
return a.GetTeamSchemeChannelRoles(c, channel.TeamId)
}
// GetTeamSchemeChannelRoles Checks if a team has an override scheme and returns the scheme channel role names or default channel role names.
func (a *App) GetTeamSchemeChannelRoles(c request.CTX, teamID string) (guestRoleName, userRoleName, adminRoleName string, err *model.AppError) {
team, err := a.GetTeam(teamID)
if err != nil {
return
}
if team.SchemeId != nil && *team.SchemeId != "" {
var scheme *model.Scheme
scheme, err = a.GetScheme(*team.SchemeId)
if err != nil {
return
}
guestRoleName = scheme.DefaultChannelGuestRole
userRoleName = scheme.DefaultChannelUserRole
adminRoleName = scheme.DefaultChannelAdminRole
} else {
guestRoleName = model.ChannelGuestRoleId
userRoleName = model.ChannelUserRoleId
adminRoleName = model.ChannelAdminRoleId
}
return
}
// GetChannelModerationsForChannel Gets a channels ChannelModerations from either the higherScoped roles or from the channel scheme roles.
func (a *App) GetChannelModerationsForChannel(c request.CTX, channel *model.Channel) ([]*model.ChannelModeration, *model.AppError) {
guestRoleName, memberRoleName, _, err := a.GetSchemeRolesForChannel(c, channel.Id)
if err != nil {
return nil, err
}
memberRole, err := a.GetRoleByName(context.Background(), memberRoleName)
if err != nil {
return nil, err
}
var guestRole *model.Role
if guestRoleName != "" {
guestRole, err = a.GetRoleByName(context.Background(), guestRoleName)
if err != nil {
return nil, err
}
}
higherScopedGuestRoleName, higherScopedMemberRoleName, _, err := a.GetTeamSchemeChannelRoles(c, channel.TeamId)
if err != nil {
return nil, err
}
higherScopedMemberRole, err := a.GetRoleByName(context.Background(), higherScopedMemberRoleName)
if err != nil {
return nil, err
}
var higherScopedGuestRole *model.Role
if higherScopedGuestRoleName != "" {
higherScopedGuestRole, err = a.GetRoleByName(context.Background(), higherScopedGuestRoleName)
if err != nil {
return nil, err
}
}
return buildChannelModerations(c, channel.Type, memberRole, guestRole, higherScopedMemberRole, higherScopedGuestRole), nil
}
// PatchChannelModerationsForChannel Updates a channels scheme roles based on a given ChannelModerationPatch, if the permissions match the higher scoped role the scheme is deleted.
func (a *App) PatchChannelModerationsForChannel(c request.CTX, channel *model.Channel, channelModerationsPatch []*model.ChannelModerationPatch) ([]*model.ChannelModeration, *model.AppError) {
higherScopedGuestRoleName, higherScopedMemberRoleName, _, err := a.GetTeamSchemeChannelRoles(c, channel.TeamId)
if err != nil {
return nil, err
}
ctx := sqlstore.WithMaster(context.Background())
higherScopedMemberRole, err := a.GetRoleByName(ctx, higherScopedMemberRoleName)
if err != nil {
return nil, err
}
var higherScopedGuestRole *model.Role
if higherScopedGuestRoleName != "" {
higherScopedGuestRole, err = a.GetRoleByName(ctx, higherScopedGuestRoleName)
if err != nil {
return nil, err
}
}
higherScopedMemberPermissions := higherScopedMemberRole.GetChannelModeratedPermissions(channel.Type)
var higherScopedGuestPermissions map[string]bool
if higherScopedGuestRole != nil {
higherScopedGuestPermissions = higherScopedGuestRole.GetChannelModeratedPermissions(channel.Type)
}
for _, moderationPatch := range channelModerationsPatch {
if moderationPatch.Roles.Members != nil && *moderationPatch.Roles.Members && !higherScopedMemberPermissions[*moderationPatch.Name] {
return nil, &model.AppError{Message: "Cannot add a permission that is restricted by the team or system permission scheme"}
}
if moderationPatch.Roles.Guests != nil && *moderationPatch.Roles.Guests && !higherScopedGuestPermissions[*moderationPatch.Name] {
return nil, &model.AppError{Message: "Cannot add a permission that is restricted by the team or system permission scheme"}
}
}
var scheme *model.Scheme
// Channel has no scheme so create one
if channel.SchemeId == nil || *channel.SchemeId == "" {
scheme, err = a.CreateChannelScheme(c, channel)
if err != nil {
return nil, err
}
// Send a websocket event about this new role. The other new roles—member and guest—get emitted when they're updated.
var adminRole *model.Role
adminRole, err = a.GetRoleByName(ctx, scheme.DefaultChannelAdminRole)
if err != nil {
return nil, err
}
if appErr := a.sendUpdatedRoleEvent(adminRole); appErr != nil {
return nil, appErr
}
message := model.NewWebSocketEvent(model.WebsocketEventChannelSchemeUpdated, "", channel.Id, "", nil, "")
a.Publish(message)
c.Logger().Info("Permission scheme created.", mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
} else {
scheme, err = a.GetScheme(*channel.SchemeId)
if err != nil {
return nil, err
}
}
guestRoleName := scheme.DefaultChannelGuestRole
memberRoleName := scheme.DefaultChannelUserRole
memberRole, err := a.GetRoleByName(ctx, memberRoleName)
if err != nil {
return nil, err
}
var guestRole *model.Role
if guestRoleName != "" {
guestRole, err = a.GetRoleByName(ctx, guestRoleName)
if err != nil {
return nil, err
}
}
memberRolePatch := memberRole.RolePatchFromChannelModerationsPatch(channelModerationsPatch, "members")
var guestRolePatch *model.RolePatch
if guestRole != nil {
guestRolePatch = guestRole.RolePatchFromChannelModerationsPatch(channelModerationsPatch, "guests")
}
for _, channelModerationPatch := range channelModerationsPatch {
permissionModified := *channelModerationPatch.Name
if channelModerationPatch.Roles.Guests != nil && utils.StringInSlice(permissionModified, model.ChannelModeratedPermissionsChangedByPatch(guestRole, guestRolePatch)) {
if *channelModerationPatch.Roles.Guests {
c.Logger().Info("Permission enabled for guests.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
} else {
c.Logger().Info("Permission disabled for guests.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
}
}
if channelModerationPatch.Roles.Members != nil && utils.StringInSlice(permissionModified, model.ChannelModeratedPermissionsChangedByPatch(memberRole, memberRolePatch)) {
if *channelModerationPatch.Roles.Members {
c.Logger().Info("Permission enabled for members.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
} else {
c.Logger().Info("Permission disabled for members.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
}
}
}
memberRolePermissionsUnmodified := len(model.ChannelModeratedPermissionsChangedByPatch(higherScopedMemberRole, memberRolePatch)) == 0
guestRolePermissionsUnmodified := len(model.ChannelModeratedPermissionsChangedByPatch(higherScopedGuestRole, guestRolePatch)) == 0
if memberRolePermissionsUnmodified && guestRolePermissionsUnmodified {
// The channel scheme matches the permissions of its higherScoped scheme so delete the scheme
if _, err = a.DeleteChannelScheme(c, channel); err != nil {
return nil, err
}
message := model.NewWebSocketEvent(model.WebsocketEventChannelSchemeUpdated, "", channel.Id, "", nil, "")
a.Publish(message)
memberRole = higherScopedMemberRole
guestRole = higherScopedGuestRole
c.Logger().Info("Permission scheme deleted.", mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
} else {
memberRole, err = a.PatchRole(memberRole, memberRolePatch)
if err != nil {
return nil, err
}
guestRole, err = a.PatchRole(guestRole, guestRolePatch)
if err != nil {
return nil, err
}
}
cErr := a.forEachChannelMember(c, channel.Id, func(channelMember model.ChannelMember) error {
a.Srv().Store().Channel().InvalidateAllChannelMembersForUser(channelMember.UserId)
evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", channelMember.UserId, nil, "")
memberJSON, jsonErr := json.Marshal(channelMember)
if jsonErr != nil {
return jsonErr
}
evt.Add("channelMember", string(memberJSON))
a.Publish(evt)
return nil
})
if cErr != nil {
return nil, model.NewAppError("PatchChannelModerationsForChannel", "api.channel.patch_channel_moderations.cache_invalidation.error", nil, "", http.StatusInternalServerError).Wrap(cErr)
}
return buildChannelModerations(c, channel.Type, memberRole, guestRole, higherScopedMemberRole, higherScopedGuestRole), nil
}
func buildChannelModerations(c request.CTX, channelType model.ChannelType, memberRole *model.Role, guestRole *model.Role, higherScopedMemberRole *model.Role, higherScopedGuestRole *model.Role) []*model.ChannelModeration {
var memberPermissions, guestPermissions, higherScopedMemberPermissions, higherScopedGuestPermissions map[string]bool
if memberRole != nil {
memberPermissions = memberRole.GetChannelModeratedPermissions(channelType)
}
if guestRole != nil {
guestPermissions = guestRole.GetChannelModeratedPermissions(channelType)
}
if higherScopedMemberRole != nil {
higherScopedMemberPermissions = higherScopedMemberRole.GetChannelModeratedPermissions(channelType)
}
if higherScopedGuestRole != nil {
higherScopedGuestPermissions = higherScopedGuestRole.GetChannelModeratedPermissions(channelType)
}
var channelModerations []*model.ChannelModeration
for _, permissionKey := range model.ChannelModeratedPermissions {
roles := &model.ChannelModeratedRoles{}
roles.Members = &model.ChannelModeratedRole{
Value: memberPermissions[permissionKey],
Enabled: higherScopedMemberPermissions[permissionKey],
}
if permissionKey == "manage_members" {
roles.Guests = nil
} else {
roles.Guests = &model.ChannelModeratedRole{
Value: guestPermissions[permissionKey],
Enabled: higherScopedGuestPermissions[permissionKey],
}
}
moderation := &model.ChannelModeration{
Name: permissionKey,
Roles: roles,
}
channelModerations = append(channelModerations, moderation)
}
return channelModerations
}
func (a *App) UpdateChannelMemberRoles(c request.CTX, channelID string, userID string, newRoles string) (*model.ChannelMember, *model.AppError) {
var member *model.ChannelMember
var err *model.AppError
if member, err = a.GetChannelMember(c, channelID, userID); err != nil {
return nil, err
}
schemeGuestRole, schemeUserRole, schemeAdminRole, err := a.GetSchemeRolesForChannel(c, channelID)
if err != nil {
return nil, err
}
prevSchemeGuestValue := member.SchemeGuest
var newExplicitRoles []string
member.SchemeGuest = false
member.SchemeUser = false
member.SchemeAdmin = false
for _, roleName := range strings.Fields(newRoles) {
var role *model.Role
role, err = a.GetRoleByName(context.Background(), roleName)
if err != nil {
err.StatusCode = http.StatusBadRequest
return nil, err
}
if !role.SchemeManaged {
// The role is not scheme-managed, so it's OK to apply it to the explicit roles field.
newExplicitRoles = append(newExplicitRoles, roleName)
} else {
// The role is scheme-managed, so need to check if it is part of the scheme for this channel or not.
switch roleName {
case schemeAdminRole:
member.SchemeAdmin = true
case schemeUserRole:
member.SchemeUser = true
case schemeGuestRole:
member.SchemeGuest = true
default:
// If not part of the scheme for this channel, then it is not allowed to apply it as an explicit role.
return nil, model.NewAppError("UpdateChannelMemberRoles", "api.channel.update_channel_member_roles.scheme_role.app_error", nil, "role_name="+roleName, http.StatusBadRequest)
}
}
}
if member.SchemeUser && member.SchemeGuest {
return nil, model.NewAppError("UpdateChannelMemberRoles", "api.channel.update_channel_member_roles.guest_and_user.app_error", nil, "", http.StatusBadRequest)
}
if prevSchemeGuestValue != member.SchemeGuest {
return nil, model.NewAppError("UpdateChannelMemberRoles", "api.channel.update_channel_member_roles.changing_guest_role.app_error", nil, "", http.StatusBadRequest)
}
member.ExplicitRoles = strings.Join(newExplicitRoles, " ")
return a.updateChannelMember(c, member)
}
func (a *App) UpdateChannelMemberSchemeRoles(c request.CTX, channelID string, userID string, isSchemeGuest bool, isSchemeUser bool, isSchemeAdmin bool) (*model.ChannelMember, *model.AppError) {
member, err := a.GetChannelMember(c, channelID, userID)
if err != nil {
return nil, err
}
member.SchemeAdmin = isSchemeAdmin
member.SchemeUser = isSchemeUser
member.SchemeGuest = isSchemeGuest
if member.SchemeUser && member.SchemeGuest {
return nil, model.NewAppError("UpdateChannelMemberSchemeRoles", "api.channel.update_channel_member_roles.guest_and_user.app_error", nil, "", http.StatusBadRequest)
}
// If the migration is not completed, we also need to check the default channel_admin/channel_user roles are not present in the roles field.
if err = a.IsPhase2MigrationCompleted(); err != nil {
member.ExplicitRoles = RemoveRoles([]string{model.ChannelGuestRoleId, model.ChannelUserRoleId, model.ChannelAdminRoleId}, member.ExplicitRoles)
}
return a.updateChannelMember(c, member)
}
func (a *App) UpdateChannelMemberNotifyProps(c request.CTX, data map[string]string, channelID string, userID string) (*model.ChannelMember, *model.AppError) {
filteredProps := make(map[string]string)
// update whichever notify properties have been provided, but don't change the others
if markUnread, exists := data[model.MarkUnreadNotifyProp]; exists {
filteredProps[model.MarkUnreadNotifyProp] = markUnread
}
if desktop, exists := data[model.DesktopNotifyProp]; exists {
filteredProps[model.DesktopNotifyProp] = desktop
}
if desktop_threads, exists := data[model.DesktopThreadsNotifyProp]; exists {
filteredProps[model.DesktopThreadsNotifyProp] = desktop_threads
}
if email, exists := data[model.EmailNotifyProp]; exists {
filteredProps[model.EmailNotifyProp] = email
}
if push, exists := data[model.PushNotifyProp]; exists {
filteredProps[model.PushNotifyProp] = push
}
if push_threads, exists := data[model.PushThreadsNotifyProp]; exists {
filteredProps[model.PushThreadsNotifyProp] = push_threads
}
if ignoreChannelMentions, exists := data[model.IgnoreChannelMentionsNotifyProp]; exists {
filteredProps[model.IgnoreChannelMentionsNotifyProp] = ignoreChannelMentions
}
member, err := a.Srv().Store().Channel().UpdateMemberNotifyProps(channelID, userID, filteredProps)
if err != nil {
var appErr *model.AppError
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &nfErr):
return nil, model.NewAppError("updateMemberNotifyProps", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("updateMemberNotifyProps", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
a.InvalidateCacheForUser(member.UserId)
a.invalidateCacheForChannelMembersNotifyProps(member.ChannelId)
// Notify the clients that the member notify props changed
evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", member.UserId, nil, "")
memberJSON, jsonErr := json.Marshal(member)
if jsonErr != nil {
return nil, model.NewAppError("UpdateChannelMemberNotifyProps", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
evt.Add("channelMember", string(memberJSON))
a.Publish(evt)
return member, nil
}
func (a *App) updateChannelMember(c request.CTX, member *model.ChannelMember) (*model.ChannelMember, *model.AppError) {
member, err := a.Srv().Store().Channel().UpdateMember(member)
if err != nil {
var appErr *model.AppError
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &nfErr):
return nil, model.NewAppError("updateChannelMember", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("updateChannelMember", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
a.InvalidateCacheForUser(member.UserId)
// Notify the clients that the member notify props changed
evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", member.UserId, nil, "")
memberJSON, jsonErr := json.Marshal(member)
if jsonErr != nil {
return nil, model.NewAppError("updateChannelMember", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
evt.Add("channelMember", string(memberJSON))
a.Publish(evt)
return member, nil
}
func (a *App) DeleteChannel(c request.CTX, channel *model.Channel, userID string) *model.AppError {
ihc := make(chan store.StoreResult, 1)
ohc := make(chan store.StoreResult, 1)
go func() {
webhooks, err := a.Srv().Store().Webhook().GetIncomingByChannel(channel.Id)
ihc <- store.StoreResult{Data: webhooks, NErr: err}
close(ihc)
}()
go func() {
outgoingHooks, err := a.Srv().Store().Webhook().GetOutgoingByChannel(channel.Id, -1, -1)
ohc <- store.StoreResult{Data: outgoingHooks, NErr: err}
close(ohc)
}()
var user *model.User
if userID != "" {
var nErr error
user, nErr = a.Srv().Store().User().Get(context.Background(), userID)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return model.NewAppError("DeleteChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return model.NewAppError("DeleteChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
}
ihcresult := <-ihc
if ihcresult.NErr != nil {
return model.NewAppError("DeleteChannel", "app.webhooks.get_incoming_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(ihcresult.NErr)
}
ohcresult := <-ohc
if ohcresult.NErr != nil {
return model.NewAppError("DeleteChannel", "app.webhooks.get_outgoing_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(ohcresult.NErr)
}
incomingHooks := ihcresult.Data.([]*model.IncomingWebhook)
outgoingHooks := ohcresult.Data.([]*model.OutgoingWebhook)
if channel.DeleteAt > 0 {
err := model.NewAppError("deleteChannel", "api.channel.delete_channel.deleted.app_error", nil, "", http.StatusBadRequest)
return err
}
if channel.Name == model.DefaultChannelName {
err := model.NewAppError("deleteChannel", "api.channel.delete_channel.cannot.app_error", map[string]any{"Channel": model.DefaultChannelName}, "", http.StatusBadRequest)
return err
}
if user != nil {
T := i18n.GetUserTranslations(user.Locale)
post := &model.Post{
ChannelId: channel.Id,
Message: fmt.Sprintf(T("api.channel.delete_channel.archived"), user.Username),
Type: model.PostTypeChannelDeleted,
UserId: userID,
Props: model.StringInterface{
"username": user.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
c.Logger().Warn("Failed to post archive message", mlog.Err(err))
}
} else {
systemBot, err := a.GetSystemBot()
if err != nil {
c.Logger().Warn("Failed to post archive message", mlog.Err(err))
} else {
post := &model.Post{
ChannelId: channel.Id,
Message: fmt.Sprintf(i18n.T("api.channel.delete_channel.archived"), systemBot.Username),
Type: model.PostTypeChannelDeleted,
UserId: systemBot.UserId,
Props: model.StringInterface{
"username": systemBot.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
c.Logger().Warn("Failed to post archive message", mlog.Err(err))
}
}
}
now := model.GetMillis()
for _, hook := range incomingHooks {
if err := a.Srv().Store().Webhook().DeleteIncoming(hook.Id, now); err != nil {
c.Logger().Warn("Encountered error deleting incoming webhook", mlog.String("hook_id", hook.Id), mlog.Err(err))
}
a.Srv().Platform().InvalidateCacheForWebhook(hook.Id)
}
for _, hook := range outgoingHooks {
if err := a.Srv().Store().Webhook().DeleteOutgoing(hook.Id, now); err != nil {
c.Logger().Warn("Encountered error deleting outgoing webhook", mlog.String("hook_id", hook.Id), mlog.Err(err))
}
}
deleteAt := model.GetMillis()
if err := a.Srv().Store().Channel().Delete(channel.Id, deleteAt); err != nil {
return model.NewAppError("DeleteChannel", "app.channel.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.Srv().Platform().InvalidateCacheForChannel(channel)
message := model.NewWebSocketEvent(model.WebsocketEventChannelDeleted, channel.TeamId, "", "", nil, "")
message.Add("channel_id", channel.Id)
message.Add("delete_at", deleteAt)
a.Publish(message)
return nil
}
func (a *App) addUserToChannel(c request.CTX, user *model.User, channel *model.Channel) (*model.ChannelMember, *model.AppError) {
if channel.Type != model.ChannelTypeOpen && channel.Type != model.ChannelTypePrivate {
return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user_to_channel.type.app_error", nil, "", http.StatusBadRequest)
}
channelMember, nErr := a.Srv().Store().Channel().GetMember(context.Background(), channel.Id, user.Id)
if nErr != nil {
var nfErr *store.ErrNotFound
if !errors.As(nErr, &nfErr) {
return nil, model.NewAppError("AddUserToChannel", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
} else {
return channelMember, nil
}
if channel.IsGroupConstrained() {
nonMembers, err := a.FilterNonGroupChannelMembers([]string{user.Id}, channel)
if err != nil {
return nil, model.NewAppError("addUserToChannel", "api.channel.add_user_to_channel.type.app_error", nil, "", http.StatusInternalServerError)
}
if len(nonMembers) > 0 {
return nil, model.NewAppError("addUserToChannel", "api.channel.add_members.user_denied", map[string]any{"UserIDs": nonMembers}, "", http.StatusBadRequest)
}
}
newMember := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: user.IsGuest(),
SchemeUser: !user.IsGuest(),
}
if !user.IsGuest() {
var userShouldBeAdmin bool
userShouldBeAdmin, appErr := a.UserIsInAdminRoleGroup(user.Id, channel.Id, model.GroupSyncableTypeChannel)
if appErr != nil {
return nil, appErr
}
newMember.SchemeAdmin = userShouldBeAdmin
}
newMember, nErr = a.Srv().Store().Channel().SaveMember(newMember)
if nErr != nil {
return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.app_error", nil,
fmt.Sprintf("failed to add member: %v, user_id: %s, channel_id: %s", nErr, user.Id, channel.Id), http.StatusInternalServerError)
}
if nErr := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis()); nErr != nil {
return nil, model.NewAppError("AddUserToChannel", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
a.InvalidateCacheForUser(user.Id)
a.invalidateCacheForChannelMembers(channel.Id)
return newMember, nil
}
// AddUserToChannel adds a user to a given channel.
func (a *App) AddUserToChannel(c request.CTX, user *model.User, channel *model.Channel, skipTeamMemberIntegrityCheck bool) (*model.ChannelMember, *model.AppError) {
if !skipTeamMemberIntegrityCheck {
teamMember, nErr := a.Srv().Store().Team().GetMember(context.Background(), channel.TeamId, user.Id)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("AddUserToChannel", "app.team.get_member.missing.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("AddUserToChannel", "app.team.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if teamMember.DeleteAt > 0 {
return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.deleted.app_error", nil, "", http.StatusBadRequest)
}
}
newMember, err := a.addUserToChannel(c, user, channel)
if err != nil {
return nil, err
}
message := model.NewWebSocketEvent(model.WebsocketEventUserAdded, "", channel.Id, "", nil, "")
message.Add("user_id", user.Id)
message.Add("team_id", channel.TeamId)
a.Publish(message)
return newMember, nil
}
type ChannelMemberOpts struct {
UserRequestorID string
PostRootID string
// SkipTeamMemberIntegrityCheck is used to indicate whether it should be checked
// that a user has already been removed from that team or not.
// This is useful to avoid in scenarios when we just added the team member,
// and thereby know that there is no need to check this.
SkipTeamMemberIntegrityCheck bool
}
// AddChannelMember adds a user to a channel. It is a wrapper over AddUserToChannel.
func (a *App) AddChannelMember(c request.CTX, userID string, channel *model.Channel, opts ChannelMemberOpts) (*model.ChannelMember, *model.AppError) {
if member, err := a.Srv().Store().Channel().GetMember(context.Background(), channel.Id, userID); err != nil {
var nfErr *store.ErrNotFound
if !errors.As(err, &nfErr) {
return nil, model.NewAppError("AddChannelMember", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
} else {
return member, nil
}
var user *model.User
var err *model.AppError
if user, err = a.GetUser(userID); err != nil {
return nil, err
}
var userRequestor *model.User
if opts.UserRequestorID != "" {
if userRequestor, err = a.GetUser(opts.UserRequestorID); err != nil {
return nil, err
}
}
cm, err := a.AddUserToChannel(c, user, channel, opts.SkipTeamMemberIntegrityCheck)
if err != nil {
return nil, err
}
a.Srv().Go(func() {
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.UserHasJoinedChannel(pluginContext, cm, userRequestor)
return true
}, plugin.UserHasJoinedChannelID)
})
if opts.UserRequestorID == "" || userID == opts.UserRequestorID {
if err := a.postJoinChannelMessage(c, user, channel); err != nil {
return nil, err
}
} else {
a.Srv().Go(func() {
a.PostAddToChannelMessage(c, userRequestor, user, channel, opts.PostRootID)
})
}
return cm, nil
}
func (a *App) AddDirectChannels(c request.CTX, teamID string, user *model.User) *model.AppError {
var profiles []*model.User
options := &model.UserGetOptions{InTeamId: teamID, Page: 0, PerPage: 100}
profiles, err := a.Srv().Store().User().GetProfiles(options)
if err != nil {
return model.NewAppError("AddDirectChannels", "api.user.add_direct_channels_and_forget.failed.error", map[string]any{"UserId": user.Id, "TeamId": teamID, "Error": err.Error()}, "", http.StatusInternalServerError)
}
var preferences model.Preferences
for _, profile := range profiles {
if profile.Id == user.Id {
continue
}
preference := model.Preference{
UserId: user.Id,
Category: model.PreferenceCategoryDirectChannelShow,
Name: profile.Id,
Value: "true",
}
preferences = append(preferences, preference)
if len(preferences) >= 10 {
break
}
}
if err := a.Srv().Store().Preference().Save(preferences); err != nil {
return model.NewAppError("AddDirectChannels", "api.user.add_direct_channels_and_forget.failed.error", map[string]any{"UserId": user.Id, "TeamId": teamID, "Error": err.Error()}, "", http.StatusInternalServerError)
}
return nil
}
func (a *App) PostUpdateChannelHeaderMessage(c request.CTX, userID string, channel *model.Channel, oldChannelHeader, newChannelHeader string) *model.AppError {
user, err := a.Srv().Store().User().Get(context.Background(), userID)
if err != nil {
return model.NewAppError("PostUpdateChannelHeaderMessage", "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error", nil, "", http.StatusBadRequest).Wrap(err)
}
var message string
if oldChannelHeader == "" {
message = fmt.Sprintf(i18n.T("api.channel.post_update_channel_header_message_and_forget.updated_to"), user.Username, newChannelHeader)
} else if newChannelHeader == "" {
message = fmt.Sprintf(i18n.T("api.channel.post_update_channel_header_message_and_forget.removed"), user.Username, oldChannelHeader)
} else {
message = fmt.Sprintf(i18n.T("api.channel.post_update_channel_header_message_and_forget.updated_from"), user.Username, oldChannelHeader, newChannelHeader)
}
post := &model.Post{
ChannelId: channel.Id,
Message: message,
Type: model.PostTypeHeaderChange,
UserId: userID,
Props: model.StringInterface{
"username": user.Username,
"old_header": oldChannelHeader,
"new_header": newChannelHeader,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("", "api.channel.post_update_channel_header_message_and_forget.post.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) PostUpdateChannelPurposeMessage(c request.CTX, userID string, channel *model.Channel, oldChannelPurpose string, newChannelPurpose string) *model.AppError {
user, err := a.Srv().Store().User().Get(context.Background(), userID)
if err != nil {
return model.NewAppError("PostUpdateChannelPurposeMessage", "app.channel.post_update_channel_purpose_message.retrieve_user.error", nil, "", http.StatusBadRequest).Wrap(err)
}
var message string
if oldChannelPurpose == "" {
message = fmt.Sprintf(i18n.T("app.channel.post_update_channel_purpose_message.updated_to"), user.Username, newChannelPurpose)
} else if newChannelPurpose == "" {
message = fmt.Sprintf(i18n.T("app.channel.post_update_channel_purpose_message.removed"), user.Username, oldChannelPurpose)
} else {
message = fmt.Sprintf(i18n.T("app.channel.post_update_channel_purpose_message.updated_from"), user.Username, oldChannelPurpose, newChannelPurpose)
}
post := &model.Post{
ChannelId: channel.Id,
Message: message,
Type: model.PostTypePurposeChange,
UserId: userID,
Props: model.StringInterface{
"username": user.Username,
"old_purpose": oldChannelPurpose,
"new_purpose": newChannelPurpose,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("", "app.channel.post_update_channel_purpose_message.post.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) PostUpdateChannelDisplayNameMessage(c request.CTX, userID string, channel *model.Channel, oldChannelDisplayName, newChannelDisplayName string) *model.AppError {
user, err := a.Srv().Store().User().Get(context.Background(), userID)
if err != nil {
return model.NewAppError("PostUpdateChannelDisplayNameMessage", "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error", nil, "", http.StatusBadRequest).Wrap(err)
}
message := fmt.Sprintf(i18n.T("api.channel.post_update_channel_displayname_message_and_forget.updated_from"), user.Username, oldChannelDisplayName, newChannelDisplayName)
post := &model.Post{
ChannelId: channel.Id,
Message: message,
Type: model.PostTypeDisplaynameChange,
UserId: userID,
Props: model.StringInterface{
"username": user.Username,
"old_displayname": oldChannelDisplayName,
"new_displayname": newChannelDisplayName,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("PostUpdateChannelDisplayNameMessage", "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) GetChannel(c request.CTX, channelID string) (*model.Channel, *model.AppError) {
return a.Srv().getChannel(c, channelID)
}
func (s *Server) getChannel(c request.CTX, channelID string) (*model.Channel, *model.AppError) {
channel, err := s.Store().Channel().Get(channelID, true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetChannel", "app.channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetChannel", "app.channel.get.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return channel, nil
}
func (a *App) GetChannels(c request.CTX, channelIDs []string) ([]*model.Channel, *model.AppError) {
channels, err := a.Srv().Store().Channel().GetMany(channelIDs, true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetChannel", "app.channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetChannel", "app.channel.get.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return channels, nil
}
func (a *App) GetChannelByName(c request.CTX, channelName, teamID string, includeDeleted bool) (*model.Channel, *model.AppError) {
var channel *model.Channel
var err error
if includeDeleted {
channel, err = a.Srv().Store().Channel().GetByNameIncludeDeleted(teamID, channelName, false)
} else {
channel, err = a.Srv().Store().Channel().GetByName(teamID, channelName, false)
}
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetChannelByName", "app.channel.get_by_name.missing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetChannelByName", "app.channel.get_by_name.existing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return channel, nil
}
func (a *App) GetChannelsByNames(c request.CTX, channelNames []string, teamID string) ([]*model.Channel, *model.AppError) {
channels, err := a.Srv().Store().Channel().GetByNames(teamID, channelNames, true)
if err != nil {
return nil, model.NewAppError("GetChannelsByNames", "app.channel.get_by_name.existing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channels, nil
}
func (a *App) GetChannelByNameForTeamName(c request.CTX, channelName, teamName string, includeDeleted bool) (*model.Channel, *model.AppError) {
var team *model.Team
team, err := a.Srv().Store().Team().GetByName(teamName)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetChannelByNameForTeamName", "app.team.get_by_name.missing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetChannelByNameForTeamName", "app.team.get_by_name.app_error", nil, "", http.StatusNotFound).Wrap(err)
}
}
var result *model.Channel
var nErr error
if includeDeleted {
result, nErr = a.Srv().Store().Channel().GetByNameIncludeDeleted(team.Id, channelName, false)
} else {
result, nErr = a.Srv().Store().Channel().GetByName(team.Id, channelName, false)
}
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("GetChannelByNameForTeamName", "app.channel.get_by_name.missing.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("GetChannelByNameForTeamName", "app.channel.get_by_name.existing.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return result, nil
}
func (s *Server) getChannelsForTeamForUser(c request.CTX, teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, *model.AppError) {
list, err := s.Store().Channel().GetChannels(teamID, userID, opts)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.not_found.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return list, nil
}
func (a *App) GetChannelsForTeamForUser(c request.CTX, teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, *model.AppError) {
return a.Srv().getChannelsForTeamForUser(c, teamID, userID, opts)
}
func (a *App) GetChannelsForTeamForUserWithCursor(c request.CTX, teamID string, userID string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, *model.AppError) {
list, err := a.Srv().Store().Channel().GetChannelsWithCursor(teamID, userID, opts, afterChannelID)
if err != nil {
return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, nil
}
func (a *App) GetChannelsForUser(c request.CTX, userID string, includeDeleted bool, lastDeleteAt, pageSize int, fromChannelID string) (model.ChannelList, *model.AppError) {
list, err := a.Srv().Store().Channel().GetChannelsByUser(userID, includeDeleted, lastDeleteAt, pageSize, fromChannelID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.not_found.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return list, nil
}
func (a *App) GetAllChannels(c request.CTX, page, perPage int, opts model.ChannelSearchOpts) (model.ChannelListWithTeamData, *model.AppError) {
if opts.ExcludeDefaultChannels {
opts.ExcludeChannelNames = a.DefaultChannelNames(c)
}
storeOpts := store.ChannelSearchOpts{
ExcludeChannelNames: opts.ExcludeChannelNames,
NotAssociatedToGroup: opts.NotAssociatedToGroup,
IncludeDeleted: opts.IncludeDeleted,
ExcludePolicyConstrained: opts.ExcludePolicyConstrained,
IncludePolicyID: opts.IncludePolicyID,
}
channels, err := a.Srv().Store().Channel().GetAllChannels(page*perPage, perPage, storeOpts)
if err != nil {
return nil, model.NewAppError("GetAllChannels", "app.channel.get_all_channels.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channels, nil
}
func (a *App) GetAllChannelsCount(c request.CTX, opts model.ChannelSearchOpts) (int64, *model.AppError) {
if opts.ExcludeDefaultChannels {
opts.ExcludeChannelNames = a.DefaultChannelNames(c)
}
storeOpts := store.ChannelSearchOpts{
ExcludeChannelNames: opts.ExcludeChannelNames,
NotAssociatedToGroup: opts.NotAssociatedToGroup,
IncludeDeleted: opts.IncludeDeleted,
}
count, err := a.Srv().Store().Channel().GetAllChannelsCount(storeOpts)
if err != nil {
return 0, model.NewAppError("GetAllChannelsCount", "app.channel.get_all_channels_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return count, nil
}
func (a *App) GetDeletedChannels(c request.CTX, teamID string, offset int, limit int, userID string) (model.ChannelList, *model.AppError) {
list, err := a.Srv().Store().Channel().GetDeleted(teamID, offset, limit, userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetDeletedChannels", "app.channel.get_deleted.missing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetDeletedChannels", "app.channel.get_deleted.existing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return list, nil
}
func (a *App) GetChannelsUserNotIn(c request.CTX, teamID string, userID string, offset int, limit int) (model.ChannelList, *model.AppError) {
channels, err := a.Srv().Store().Channel().GetMoreChannels(teamID, userID, offset, limit)
if err != nil {
return nil, model.NewAppError("GetChannelsUserNotIn", "app.channel.get_more_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channels, nil
}
func (a *App) GetPublicChannelsByIdsForTeam(c request.CTX, teamID string, channelIDs []string) (model.ChannelList, *model.AppError) {
list, err := a.Srv().Store().Channel().GetPublicChannelsByIdsForTeam(teamID, channelIDs)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetPublicChannelsByIdsForTeam", "app.channel.get_channels_by_ids.not_found.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetPublicChannelsByIdsForTeam", "app.channel.get_channels_by_ids.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return list, nil
}
func (a *App) GetPublicChannelsForTeam(c request.CTX, teamID string, offset int, limit int) (model.ChannelList, *model.AppError) {
list, err := a.Srv().Store().Channel().GetPublicChannelsForTeam(teamID, offset, limit)
if err != nil {
return nil, model.NewAppError("GetPublicChannelsForTeam", "app.channel.get_public_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, nil
}
func (a *App) GetPrivateChannelsForTeam(c request.CTX, teamID string, offset int, limit int) (model.ChannelList, *model.AppError) {
list, err := a.Srv().Store().Channel().GetPrivateChannelsForTeam(teamID, offset, limit)
if err != nil {
return nil, model.NewAppError("GetPrivateChannelsForTeam", "app.channel.get_private_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, nil
}
func (a *App) GetChannelMember(c request.CTX, channelID string, userID string) (*model.ChannelMember, *model.AppError) {
return a.Srv().getChannelMember(c, channelID, userID)
}
func (s *Server) getChannelMember(c request.CTX, channelID string, userID string) (*model.ChannelMember, *model.AppError) {
channelMember, err := s.Store().Channel().GetMember(c.Context(), channelID, userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetChannelMember", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetChannelMember", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return channelMember, nil
}
func (a *App) GetChannelMembersPage(c request.CTX, channelID string, page, perPage int) (model.ChannelMembers, *model.AppError) {
channelMembers, err := a.Srv().Store().Channel().GetMembers(channelID, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetChannelMembersPage", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelMembers, nil
}
func (a *App) GetChannelMembersTimezones(c request.CTX, channelID string) ([]string, *model.AppError) {
membersTimezones, err := a.Srv().Store().Channel().GetChannelMembersTimezones(channelID)
if err != nil {
return nil, model.NewAppError("GetChannelMembersTimezones", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
var timezones []string
for _, membersTimezone := range membersTimezones {
if membersTimezone["automaticTimezone"] == "" && membersTimezone["manualTimezone"] == "" {
continue
}
timezones = append(timezones, model.GetPreferredTimezone(membersTimezone))
}
return model.RemoveDuplicateStrings(timezones), nil
}
func (a *App) GetChannelMembersByIds(c request.CTX, channelID string, userIDs []string) (model.ChannelMembers, *model.AppError) {
members, err := a.Srv().Store().Channel().GetMembersByIds(channelID, userIDs)
if err != nil {
return nil, model.NewAppError("GetChannelMembersByIds", "app.channel.get_members_by_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return members, nil
}
func (a *App) GetChannelMembersForUser(c request.CTX, teamID string, userID string) (model.ChannelMembers, *model.AppError) {
channelMembers, err := a.Srv().Store().Channel().GetMembersForUser(teamID, userID)
if err != nil {
return nil, model.NewAppError("GetChannelMembersForUser", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelMembers, nil
}
func (a *App) GetChannelMembersForUserWithPagination(c request.CTX, userID string, page, perPage int) ([]*model.ChannelMember, *model.AppError) {
m, err := a.Srv().Store().Channel().GetMembersForUserWithPagination(userID, page, perPage)
if err != nil {
return nil, model.NewAppError("GetChannelMembersForUserWithPagination", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
members := make([]*model.ChannelMember, 0, len(m))
for _, member := range m {
member := member
members = append(members, &member.ChannelMember)
}
return members, nil
}
func (a *App) GetChannelMembersWithTeamDataForUserWithPagination(c request.CTX, userID string, page, perPage int) (model.ChannelMembersWithTeamData, *model.AppError) {
m, err := a.Srv().Store().Channel().GetMembersForUserWithPagination(userID, page, perPage)
if err != nil {
return nil, model.NewAppError("GetChannelMembersForUserWithPagination", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return m, nil
}
func (a *App) GetChannelMemberCount(c request.CTX, channelID string) (int64, *model.AppError) {
count, err := a.Srv().Store().Channel().GetMemberCount(channelID, true)
if err != nil {
return 0, model.NewAppError("GetChannelMemberCount", "app.channel.get_member_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return count, nil
}
func (a *App) GetChannelFileCount(c request.CTX, channelID string) (int64, *model.AppError) {
count, err := a.Srv().Store().Channel().GetFileCount(channelID)
if err != nil {
return 0, model.NewAppError("SqlChannelStore.GetFileCount", "app.channel.get_file_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return count, nil
}
func (a *App) GetChannelGuestCount(c request.CTX, channelID string) (int64, *model.AppError) {
count, err := a.Srv().Store().Channel().GetGuestCount(channelID, true)
if err != nil {
return 0, model.NewAppError("SqlChannelStore.GetGuestCount", "app.channel.get_member_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return count, nil
}
func (a *App) GetChannelPinnedPostCount(c request.CTX, channelID string) (int64, *model.AppError) {
count, err := a.Srv().Store().Channel().GetPinnedPostCount(channelID, true)
if err != nil {
return 0, model.NewAppError("GetChannelPinnedPostCount", "app.channel.get_pinnedpost_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return count, nil
}
func (a *App) GetChannelCounts(c request.CTX, teamID string, userID string) (*model.ChannelCounts, *model.AppError) {
counts, err := a.Srv().Store().Channel().GetChannelCounts(teamID, userID)
if err != nil {
return nil, model.NewAppError("SqlChannelStore.GetChannelCounts", "app.channel.get_channel_counts.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return counts, nil
}
func (a *App) GetChannelUnread(c request.CTX, channelID, userID string) (*model.ChannelUnread, *model.AppError) {
channelUnread, err := a.Srv().Store().Channel().GetChannelUnread(channelID, userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetChannelUnread", "app.channel.get_unread.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetChannelUnread", "app.channel.get_unread.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if channelUnread.NotifyProps[model.MarkUnreadNotifyProp] == model.ChannelMarkUnreadMention {
channelUnread.MsgCount = 0
channelUnread.MsgCountRoot = 0
}
return channelUnread, nil
}
func (a *App) JoinChannel(c request.CTX, channel *model.Channel, userID string) *model.AppError {
userChan := make(chan store.StoreResult, 1)
memberChan := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), userID)
userChan <- store.StoreResult{Data: user, NErr: err}
close(userChan)
}()
go func() {
member, err := a.Srv().Store().Channel().GetMember(context.Background(), channel.Id, userID)
memberChan <- store.StoreResult{Data: member, NErr: err}
close(memberChan)
}()
uresult := <-userChan
if uresult.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(uresult.NErr, &nfErr):
return model.NewAppError("CreateChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(uresult.NErr)
default:
return model.NewAppError("CreateChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(uresult.NErr)
}
}
mresult := <-memberChan
if mresult.NErr == nil && mresult.Data != nil {
// user is already in the channel
return nil
}
user := uresult.Data.(*model.User)
if channel.Type != model.ChannelTypeOpen {
return model.NewAppError("JoinChannel", "api.channel.join_channel.permissions.app_error", nil, "", http.StatusBadRequest)
}
cm, err := a.AddUserToChannel(c, user, channel, false)
if err != nil {
return err
}
a.Srv().Go(func() {
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.UserHasJoinedChannel(pluginContext, cm, nil)
return true
}, plugin.UserHasJoinedChannelID)
})
if err := a.postJoinChannelMessage(c, user, channel); err != nil {
return err
}
return nil
}
func (a *App) postJoinChannelMessage(c request.CTX, user *model.User, channel *model.Channel) *model.AppError {
message := fmt.Sprintf(i18n.T("api.channel.join_channel.post_and_forget"), user.Username)
postType := model.PostTypeJoinChannel
if user.IsGuest() {
message = fmt.Sprintf(i18n.T("api.channel.guest_join_channel.post_and_forget"), user.Username)
postType = model.PostTypeGuestJoinChannel
}
post := &model.Post{
ChannelId: channel.Id,
Message: message,
Type: postType,
UserId: user.Id,
Props: model.StringInterface{
"username": user.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("postJoinChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) postJoinTeamMessage(c request.CTX, user *model.User, channel *model.Channel) *model.AppError {
post := &model.Post{
ChannelId: channel.Id,
Message: fmt.Sprintf(i18n.T("api.team.join_team.post_and_forget"), user.Username),
Type: model.PostTypeJoinTeam,
UserId: user.Id,
Props: model.StringInterface{
"username": user.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("postJoinTeamMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) LeaveChannel(c request.CTX, channelID string, userID string) *model.AppError {
sc := make(chan store.StoreResult, 1)
go func() {
channel, err := a.Srv().Store().Channel().Get(channelID, true)
sc <- store.StoreResult{Data: channel, NErr: err}
close(sc)
}()
uc := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), userID)
uc <- store.StoreResult{Data: user, NErr: err}
close(uc)
}()
mcc := make(chan store.StoreResult, 1)
go func() {
count, err := a.Srv().Store().Channel().GetMemberCount(channelID, false)
mcc <- store.StoreResult{Data: count, NErr: err}
close(mcc)
}()
cresult := <-sc
if cresult.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(cresult.NErr, &nfErr):
return model.NewAppError("LeaveChannel", "app.channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(cresult.NErr)
default:
return model.NewAppError("LeaveChannel", "app.channel.get.find.app_error", nil, "", http.StatusInternalServerError).Wrap(cresult.NErr)
}
}
uresult := <-uc
if uresult.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(uresult.NErr, &nfErr):
return model.NewAppError("LeaveChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(uresult.NErr)
default:
return model.NewAppError("LeaveChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(uresult.NErr)
}
}
ccresult := <-mcc
if ccresult.NErr != nil {
return model.NewAppError("LeaveChannel", "app.channel.get_member_count.app_error", nil, "", http.StatusInternalServerError).Wrap(ccresult.NErr)
}
channel := cresult.Data.(*model.Channel)
user := uresult.Data.(*model.User)
membersCount := ccresult.Data.(int64)
if channel.IsGroupOrDirect() {
err := model.NewAppError("LeaveChannel", "api.channel.leave.direct.app_error", nil, "", http.StatusBadRequest)
return err
}
if channel.Type == model.ChannelTypePrivate && membersCount == 1 {
err := model.NewAppError("LeaveChannel", "api.channel.leave.last_member.app_error", nil, "userId="+user.Id, http.StatusBadRequest)
return err
}
if err := a.removeUserFromChannel(c, userID, userID, channel); err != nil {
return err
}
if channel.Name == model.DefaultChannelName && !*a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages {
return nil
}
a.Srv().Go(func() {
a.postLeaveChannelMessage(c, user, channel)
})
return nil
}
func (a *App) postLeaveChannelMessage(c request.CTX, user *model.User, channel *model.Channel) *model.AppError {
post := &model.Post{
ChannelId: channel.Id,
// Message here embeds `@username`, not just `username`, to ensure that mentions
// treat this as a username mention even though the user has now left the channel.
// The client renders its own system message, ignoring this value altogether.
Message: fmt.Sprintf(i18n.T("api.channel.leave.left"), fmt.Sprintf("@%s", user.Username)),
Type: model.PostTypeLeaveChannel,
UserId: user.Id,
Props: model.StringInterface{
"username": user.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("postLeaveChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) PostAddToChannelMessage(c request.CTX, user *model.User, addedUser *model.User, channel *model.Channel, postRootId string) *model.AppError {
message := fmt.Sprintf(i18n.T("api.channel.add_member.added"), addedUser.Username, user.Username)
postType := model.PostTypeAddToChannel
if addedUser.IsGuest() {
message = fmt.Sprintf(i18n.T("api.channel.add_guest.added"), addedUser.Username, user.Username)
postType = model.PostTypeAddGuestToChannel
}
post := &model.Post{
ChannelId: channel.Id,
Message: message,
Type: postType,
UserId: user.Id,
RootId: postRootId,
Props: model.StringInterface{
"userId": user.Id,
"username": user.Username,
model.PostPropsAddedUserId: addedUser.Id,
"addedUsername": addedUser.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("postAddToChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) postAddToTeamMessage(c request.CTX, user *model.User, addedUser *model.User, channel *model.Channel, postRootId string) *model.AppError {
post := &model.Post{
ChannelId: channel.Id,
Message: fmt.Sprintf(i18n.T("api.team.add_user_to_team.added"), addedUser.Username, user.Username),
Type: model.PostTypeAddToTeam,
UserId: user.Id,
RootId: postRootId,
Props: model.StringInterface{
"userId": user.Id,
"username": user.Username,
model.PostPropsAddedUserId: addedUser.Id,
"addedUsername": addedUser.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("postAddToTeamMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) postRemoveFromChannelMessage(c request.CTX, removerUserId string, removedUser *model.User, channel *model.Channel) *model.AppError {
messageUserId := removerUserId
if messageUserId == "" {
systemBot, err := a.GetSystemBot()
if err != nil {
return model.NewAppError("postRemoveFromChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
messageUserId = systemBot.UserId
}
post := &model.Post{
ChannelId: channel.Id,
// Message here embeds `@username`, not just `username`, to ensure that mentions
// treat this as a username mention even though the user has now left the channel.
// The client renders its own system message, ignoring this value altogether.
Message: fmt.Sprintf(i18n.T("api.channel.remove_member.removed"), fmt.Sprintf("@%s", removedUser.Username)),
Type: model.PostTypeRemoveFromChannel,
UserId: messageUserId,
Props: model.StringInterface{
"removedUserId": removedUser.Id,
"removedUsername": removedUser.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("postRemoveFromChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) removeUserFromChannel(c request.CTX, userIDToRemove string, removerUserId string, channel *model.Channel) *model.AppError {
user, nErr := a.Srv().Store().User().Get(context.Background(), userIDToRemove)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return model.NewAppError("removeUserFromChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return model.NewAppError("removeUserFromChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
isGuest := user.IsGuest()
if channel.Name == model.DefaultChannelName {
if !isGuest {
return model.NewAppError("RemoveUserFromChannel", "api.channel.remove.default.app_error", map[string]any{"Channel": model.DefaultChannelName}, "", http.StatusBadRequest)
}
}
if channel.IsGroupConstrained() && userIDToRemove != removerUserId && !user.IsBot {
nonMembers, err := a.FilterNonGroupChannelMembers([]string{userIDToRemove}, channel)
if err != nil {
return model.NewAppError("removeUserFromChannel", "api.channel.remove_user_from_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(nonMembers) == 0 {
return model.NewAppError("removeUserFromChannel", "api.channel.remove_members.denied", map[string]any{"UserIDs": nonMembers}, "", http.StatusBadRequest)
}
}
cm, err := a.GetChannelMember(c, channel.Id, userIDToRemove)
if err != nil {
return err
}
if err := a.Srv().Store().Channel().RemoveMember(channel.Id, userIDToRemove); err != nil {
return model.NewAppError("removeUserFromChannel", "app.channel.remove_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().ChannelMemberHistory().LogLeaveEvent(userIDToRemove, channel.Id, model.GetMillis()); err != nil {
return model.NewAppError("removeUserFromChannel", "app.channel_member_history.log_leave_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if isGuest {
currentMembers, err := a.GetChannelMembersForUser(c, channel.TeamId, userIDToRemove)
if err != nil {
return err
}
if len(currentMembers) == 0 {
teamMember, err := a.GetTeamMember(channel.TeamId, userIDToRemove)
if err != nil {
return model.NewAppError("removeUserFromChannel", "api.team.remove_user_from_team.missing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if err := a.ch.srv.teamService.RemoveTeamMember(teamMember); err != nil {
return model.NewAppError("removeUserFromChannel", "api.team.remove_user_from_team.missing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if err = a.postProcessTeamMemberLeave(c, teamMember, removerUserId); err != nil {
return err
}
}
}
a.InvalidateCacheForUser(userIDToRemove)
a.invalidateCacheForChannelMembers(channel.Id)
var actorUser *model.User
if removerUserId != "" {
actorUser, _ = a.GetUser(removerUserId)
}
a.Srv().Go(func() {
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.UserHasLeftChannel(pluginContext, cm, actorUser)
return true
}, plugin.UserHasLeftChannelID)
})
message := model.NewWebSocketEvent(model.WebsocketEventUserRemoved, "", channel.Id, "", nil, "")
message.Add("user_id", userIDToRemove)
message.Add("remover_id", removerUserId)
a.Publish(message)
// because the removed user no longer belongs to the channel we need to send a separate websocket event
userMsg := model.NewWebSocketEvent(model.WebsocketEventUserRemoved, "", "", userIDToRemove, nil, "")
userMsg.Add("channel_id", channel.Id)
userMsg.Add("remover_id", removerUserId)
a.Publish(userMsg)
return nil
}
func (a *App) RemoveUserFromChannel(c request.CTX, userIDToRemove string, removerUserId string, channel *model.Channel) *model.AppError {
var err *model.AppError
if err = a.removeUserFromChannel(c, userIDToRemove, removerUserId, channel); err != nil {
return err
}
var user *model.User
if user, err = a.GetUser(userIDToRemove); err != nil {
return err
}
if userIDToRemove == removerUserId {
if err := a.postLeaveChannelMessage(c, user, channel); err != nil {
return err
}
} else {
if err := a.postRemoveFromChannelMessage(c, removerUserId, user, channel); err != nil {
c.Logger().Error("Failed to post user removal message", mlog.Err(err))
}
}
return nil
}
func (a *App) GetNumberOfChannelsOnTeam(c request.CTX, teamID string) (int, *model.AppError) {
// Get total number of channels on current team
list, err := a.Srv().Store().Channel().GetTeamChannels(teamID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return 0, model.NewAppError("GetNumberOfChannelsOnTeam", "app.channel.get_channels.not_found.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return 0, model.NewAppError("GetNumberOfChannelsOnTeam", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return len(list), nil
}
func (a *App) SetActiveChannel(c request.CTX, userID string, channelID string) *model.AppError {
status, err := a.Srv().Platform().GetStatus(userID)
oldStatus := model.StatusOffline
if err != nil {
status = &model.Status{UserId: userID, Status: model.StatusOnline, Manual: false, LastActivityAt: model.GetMillis(), ActiveChannel: channelID}
} else {
oldStatus = status.Status
status.ActiveChannel = channelID
if !status.Manual && channelID != "" {
status.Status = model.StatusOnline
}
status.LastActivityAt = model.GetMillis()
}
a.Srv().Platform().AddStatusCache(status)
if status.Status != oldStatus {
a.Srv().Platform().BroadcastStatus(status)
}
return nil
}
func (a *App) IsCRTEnabledForUser(c request.CTX, userID string) bool {
appCRT := *a.Config().ServiceSettings.CollapsedThreads
if appCRT == model.CollapsedThreadsDisabled {
return false
}
if appCRT == model.CollapsedThreadsAlwaysOn {
return true
}
threadsEnabled := appCRT == model.CollapsedThreadsDefaultOn
// check if a participant has overridden collapsed threads settings
if preference, err := a.Srv().Store().Preference().Get(userID, model.PreferenceCategoryDisplaySettings, model.PreferenceNameCollapsedThreadsEnabled); err == nil {
threadsEnabled = preference.Value == "on"
}
return threadsEnabled
}
// ValidateUserPermissionsOnChannels filters channelIds based on whether userId is authorized to manage channel members. Unauthorized channels are removed from the returned list.
func (a *App) ValidateUserPermissionsOnChannels(c request.CTX, userId string, channelIds []string) []string {
var allowedChannelIds []string
for _, channelId := range channelIds {
channel, err := a.GetChannel(c, channelId)
if err != nil {
mlog.Info("Invite users to team - couldn't get channel " + channelId)
continue
}
if channel.Type == model.ChannelTypePrivate && a.HasPermissionToChannel(c, userId, channelId, model.PermissionManagePrivateChannelMembers) {
allowedChannelIds = append(allowedChannelIds, channelId)
} else if channel.Type == model.ChannelTypeOpen && a.HasPermissionToChannel(c, userId, channelId, model.PermissionManagePublicChannelMembers) {
allowedChannelIds = append(allowedChannelIds, channelId)
} else {
mlog.Info("Invite users to team - no permission to add members to that channel. UserId: " + userId + " ChannelId: " + channelId)
}
}
return allowedChannelIds
}
// MarkChanelAsUnreadFromPost will take a post and set the channel as unread from that one.
func (a *App) MarkChannelAsUnreadFromPost(c request.CTX, postID string, userID string, collapsedThreadsSupported bool) (*model.ChannelUnreadAt, *model.AppError) {
if !collapsedThreadsSupported || !a.IsCRTEnabledForUser(c, userID) {
return a.markChannelAsUnreadFromPostCRTUnsupported(c, postID, userID)
}
post, err := a.GetSinglePost(postID, false)
if err != nil {
return nil, err
}
user, err := a.GetUser(userID)
if err != nil {
return nil, err
}
unreadMentions, unreadMentionsRoot, urgentMentions, err := a.countMentionsFromPost(c, user, post)
if err != nil {
return nil, err
}
channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, unreadMentionsRoot, urgentMentions, true)
if nErr != nil {
return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
a.sendWebSocketPostUnreadEvent(c, channelUnread, postID, false)
a.UpdateMobileAppBadge(userID)
return channelUnread, nil
}
func (a *App) markChannelAsUnreadFromPostCRTUnsupported(c request.CTX, postID string, userID string) (*model.ChannelUnreadAt, *model.AppError) {
post, appErr := a.GetSinglePost(postID, false)
if appErr != nil {
return nil, appErr
}
user, appErr := a.GetUser(userID)
if appErr != nil {
return nil, appErr
}
threadId := post.RootId
if post.RootId == "" {
threadId = post.Id
}
unreadMentions, unreadMentionsRoot, urgentMentions, appErr := a.countMentionsFromPost(c, user, post)
if appErr != nil {
return nil, appErr
}
// if root post,
// In CRT Supported Client: badge on channel only sums mentions in root posts including and below the post that was marked.
// In CRT Unsupported Client: badge on channel sums mentions in all posts (root & replies) including and below the post that was marked unread.
if post.RootId == "" {
channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, unreadMentionsRoot, urgentMentions, true)
if nErr != nil {
return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
a.sendWebSocketPostUnreadEvent(c, channelUnread, postID, true)
a.UpdateMobileAppBadge(userID)
return channelUnread, nil
}
// if reply post, autofollow thread and
// In CRT Supported Client: Mark the specific thread as unread but not the channel where the thread exists.
// If there are replies with mentions below the marked reply in the thread, then sum the mentions for the threads mention badge.
// In CRT Unsupported Client: Channel is marked as unread and new messages line inserted above the marked post.
// Badge on channel sums mentions in all posts (root & replies) including and below the post that was marked unread.
rootPost, appErr := a.GetSinglePost(post.RootId, false)
if appErr != nil {
return nil, appErr
}
channel, nErr := a.Srv().Store().Channel().Get(post.ChannelId, true)
if nErr != nil {
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
if *a.Config().ServiceSettings.ThreadAutoFollow {
threadMembership, mErr := a.Srv().Store().Thread().GetMembershipForUser(user.Id, threadId)
var errNotFound *store.ErrNotFound
if mErr != nil && !errors.As(mErr, &errNotFound) {
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr)
}
// Follow thread if we're not already following it
if threadMembership == nil {
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
threadMembership, mErr = a.Srv().Store().Thread().MaintainMembership(user.Id, threadId, opts)
if mErr != nil {
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr)
}
}
// If threadmembership already exists but user had previously unfollowed the thread, then follow the thread again.
threadMembership.Following = true
threadMembership.LastViewed = post.CreateAt - 1
threadMembership.UnreadMentions, appErr = a.countThreadMentions(c, user, rootPost, channel.TeamId, post.CreateAt-1)
if appErr != nil {
return nil, appErr
}
threadMembership, mErr = a.Srv().Store().Thread().UpdateMembership(threadMembership)
if mErr != nil {
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr)
}
thread, mErr := a.Srv().Store().Thread().GetThreadForUser(threadMembership, true, a.isPostPriorityEnabled())
if mErr != nil {
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr)
}
a.sanitizeProfiles(thread.Participants, false)
thread.Post.SanitizeProps()
if a.IsCRTEnabledForUser(c, userID) {
payload, jsonErr := json.Marshal(thread)
if jsonErr != nil {
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message := model.NewWebSocketEvent(model.WebsocketEventThreadUpdated, channel.TeamId, "", userID, nil, "")
message.Add("thread", string(payload))
a.Publish(message)
}
}
channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, 0, 0, false)
if nErr != nil {
return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
a.sendWebSocketPostUnreadEvent(c, channelUnread, postID, false)
a.UpdateMobileAppBadge(userID)
return channelUnread, nil
}
func (a *App) sendWebSocketPostUnreadEvent(c request.CTX, channelUnread *model.ChannelUnreadAt, postID string, withMsgCountRoot bool) {
message := model.NewWebSocketEvent(model.WebsocketEventPostUnread, channelUnread.TeamId, channelUnread.ChannelId, channelUnread.UserId, nil, "")
message.Add("msg_count", channelUnread.MsgCount)
if withMsgCountRoot {
message.Add("msg_count_root", channelUnread.MsgCountRoot)
}
message.Add("mention_count", channelUnread.MentionCount)
message.Add("mention_count_root", channelUnread.MentionCountRoot)
message.Add("urgent_mention_count", channelUnread.UrgentMentionCount)
message.Add("last_viewed_at", channelUnread.LastViewedAt)
message.Add("post_id", postID)
a.Publish(message)
}
func (a *App) AutocompleteChannels(c request.CTX, userID, term string) (model.ChannelListWithTeamData, *model.AppError) {
includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
term = strings.TrimSpace(term)
user, appErr := a.GetUser(userID)
if appErr != nil {
return nil, appErr
}
channelList, err := a.Srv().Store().Channel().Autocomplete(userID, term, includeDeleted, user.IsGuest())
if err != nil {
return nil, model.NewAppError("AutocompleteChannels", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelList, nil
}
func (a *App) AutocompleteChannelsForTeam(c request.CTX, teamID, userID, term string) (model.ChannelList, *model.AppError) {
includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
term = strings.TrimSpace(term)
user, appErr := a.GetUser(userID)
if appErr != nil {
return nil, appErr
}
channelList, err := a.Srv().Store().Channel().AutocompleteInTeam(teamID, userID, term, includeDeleted, user.IsGuest())
if err != nil {
return nil, model.NewAppError("AutocompleteChannels", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelList, nil
}
func (a *App) AutocompleteChannelsForSearch(c request.CTX, teamID string, userID string, term string) (model.ChannelList, *model.AppError) {
includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
term = strings.TrimSpace(term)
channelList, err := a.Srv().Store().Channel().AutocompleteInTeamForSearch(teamID, userID, term, includeDeleted)
if err != nil {
return nil, model.NewAppError("AutocompleteChannelsForSearch", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelList, nil
}
// SearchAllChannels returns a list of channels, the total count of the results of the search (if the paginate search option is true), and an error.
func (a *App) SearchAllChannels(c request.CTX, term string, opts model.ChannelSearchOpts) (model.ChannelListWithTeamData, int64, *model.AppError) {
if opts.ExcludeDefaultChannels {
opts.ExcludeChannelNames = a.DefaultChannelNames(c)
}
storeOpts := store.ChannelSearchOpts{
ExcludeChannelNames: opts.ExcludeChannelNames,
NotAssociatedToGroup: opts.NotAssociatedToGroup,
IncludeDeleted: opts.IncludeDeleted,
Deleted: opts.Deleted,
TeamIds: opts.TeamIds,
GroupConstrained: opts.GroupConstrained,
ExcludeGroupConstrained: opts.ExcludeGroupConstrained,
PolicyID: opts.PolicyID,
IncludePolicyID: opts.IncludePolicyID,
IncludeSearchById: opts.IncludeSearchById,
ExcludePolicyConstrained: opts.ExcludePolicyConstrained,
Public: opts.Public,
Private: opts.Private,
Page: opts.Page,
PerPage: opts.PerPage,
}
term = strings.TrimSpace(term)
channelList, totalCount, err := a.Srv().Store().Channel().SearchAllChannels(term, storeOpts)
if err != nil {
return nil, 0, model.NewAppError("SearchAllChannels", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelList, totalCount, nil
}
func (a *App) SearchChannels(c request.CTX, teamID string, term string) (model.ChannelList, *model.AppError) {
includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
term = strings.TrimSpace(term)
channelList, err := a.Srv().Store().Channel().SearchInTeam(teamID, term, includeDeleted)
if err != nil {
return nil, model.NewAppError("SearchChannels", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelList, nil
}
func (a *App) SearchArchivedChannels(c request.CTX, teamID string, term string, userID string) (model.ChannelList, *model.AppError) {
term = strings.TrimSpace(term)
channelList, err := a.Srv().Store().Channel().SearchArchivedInTeam(teamID, term, userID)
if err != nil {
return nil, model.NewAppError("SearchArchivedChannels", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelList, nil
}
func (a *App) SearchChannelsForUser(c request.CTX, userID, teamID, term string) (model.ChannelList, *model.AppError) {
includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
term = strings.TrimSpace(term)
channelList, err := a.Srv().Store().Channel().SearchForUserInTeam(userID, teamID, term, includeDeleted)
if err != nil {
return nil, model.NewAppError("SearchChannelsForUser", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelList, nil
}
func (a *App) SearchGroupChannels(c request.CTX, userID, term string) (model.ChannelList, *model.AppError) {
if term == "" {
return model.ChannelList{}, nil
}
channelList, err := a.Srv().Store().Channel().SearchGroupChannels(userID, term)
if err != nil {
return nil, model.NewAppError("SearchGroupChannels", "app.channel.search_group_channels.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelList, nil
}
func (a *App) SearchChannelsUserNotIn(c request.CTX, teamID string, userID string, term string) (model.ChannelList, *model.AppError) {
term = strings.TrimSpace(term)
channelList, err := a.Srv().Store().Channel().SearchMore(userID, teamID, term)
if err != nil {
return nil, model.NewAppError("SearchChannelsUserNotIn", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelList, nil
}
func (a *App) MarkChannelsAsViewed(c request.CTX, channelIDs []string, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError) {
// I start looking for channels with notifications before I mark it as read, to clear the push notifications if needed
channelsToClearPushNotifications := []string{}
if a.canSendPushNotifications() {
for _, channelID := range channelIDs {
channel, errCh := a.Srv().Store().Channel().Get(channelID, true)
if errCh != nil {
c.Logger().Warn("Failed to get channel", mlog.Err(errCh))
continue
}
member, err := a.Srv().Store().Channel().GetMember(context.Background(), channelID, userID)
if err != nil {
c.Logger().Warn("Failed to get membership", mlog.Err(err))
continue
}
notify := member.NotifyProps[model.PushNotifyProp]
if notify == model.ChannelNotifyDefault {
user, err := a.GetUser(userID)
if err != nil {
c.Logger().Warn("Failed to get user", mlog.String("user_id", userID), mlog.Err(err))
continue
}
notify = user.NotifyProps[model.PushNotifyProp]
}
if notify == model.UserNotifyAll {
if count, err := a.Srv().Store().User().GetAnyUnreadPostCountForChannel(userID, channelID); err == nil {
if count > 0 {
channelsToClearPushNotifications = append(channelsToClearPushNotifications, channelID)
}
}
} else if notify == model.UserNotifyMention || channel.Type == model.ChannelTypeDirect {
if count, err := a.Srv().Store().User().GetUnreadCountForChannel(userID, channelID); err == nil {
if count > 0 {
channelsToClearPushNotifications = append(channelsToClearPushNotifications, channelID)
}
}
}
}
}
var err error
updateThreads := *a.Config().ServiceSettings.ThreadAutoFollow && (!collapsedThreadsSupported || !a.IsCRTEnabledForUser(c, userID))
if updateThreads {
err = a.Srv().Store().Thread().MarkAllAsReadByChannels(userID, channelIDs)
if err != nil {
return nil, model.NewAppError("MarkChannelsAsViewed", "app.channel.update_last_viewed_at.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
times, err := a.Srv().Store().Channel().UpdateLastViewedAt(channelIDs, userID)
if err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("MarkChannelsAsViewed", "app.channel.update_last_viewed_at.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("MarkChannelsAsViewed", "app.channel.update_last_viewed_at.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if *a.Config().ServiceSettings.EnableChannelViewedMessages {
for _, channelID := range channelIDs {
message := model.NewWebSocketEvent(model.WebsocketEventChannelViewed, "", "", userID, nil, "")
message.Add("channel_id", channelID)
a.Publish(message)
}
}
for _, channelID := range channelsToClearPushNotifications {
a.clearPushNotification(currentSessionId, userID, channelID, "")
}
if updateThreads && a.IsCRTEnabledForUser(c, userID) {
timestamp := model.GetMillis()
for _, channelID := range channelIDs {
message := model.NewWebSocketEvent(model.WebsocketEventThreadReadChanged, "", channelID, userID, nil, "")
message.Add("timestamp", timestamp)
a.Publish(message)
}
}
return times, nil
}
func (a *App) ViewChannel(c request.CTX, view *model.ChannelView, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError) {
if err := a.SetActiveChannel(c, userID, view.ChannelId); err != nil {
return nil, err
}
channelIDs := []string{}
if view.ChannelId != "" {
channelIDs = append(channelIDs, view.ChannelId)
}
if view.PrevChannelId != "" {
channelIDs = append(channelIDs, view.PrevChannelId)
}
if len(channelIDs) == 0 {
return map[string]int64{}, nil
}
return a.MarkChannelsAsViewed(c, channelIDs, userID, currentSessionId, collapsedThreadsSupported)
}
func (a *App) PermanentDeleteChannel(c request.CTX, channel *model.Channel) *model.AppError {
if err := a.Srv().Store().Post().PermanentDeleteByChannel(channel.Id); err != nil {
return model.NewAppError("PermanentDeleteChannel", "app.post.permanent_delete_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Channel().PermanentDeleteMembersByChannel(channel.Id); err != nil {
return model.NewAppError("PermanentDeleteChannel", "app.channel.remove_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Webhook().PermanentDeleteIncomingByChannel(channel.Id); err != nil {
return model.NewAppError("PermanentDeleteChannel", "app.webhooks.permanent_delete_incoming_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Webhook().PermanentDeleteOutgoingByChannel(channel.Id); err != nil {
return model.NewAppError("PermanentDeleteChannel", "app.webhooks.permanent_delete_outgoing_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
deleteAt := model.GetMillis()
if nErr := a.Srv().Store().Channel().PermanentDelete(channel.Id); nErr != nil {
return model.NewAppError("PermanentDeleteChannel", "app.channel.permanent_delete.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
a.Srv().Platform().InvalidateCacheForChannel(channel)
message := model.NewWebSocketEvent(model.WebsocketEventChannelDeleted, channel.TeamId, "", "", nil, "")
message.Add("channel_id", channel.Id)
message.Add("delete_at", deleteAt)
a.Publish(message)
return nil
}
func (a *App) RemoveAllDeactivatedMembersFromChannel(c request.CTX, channel *model.Channel) *model.AppError {
err := a.Srv().Store().Channel().RemoveAllDeactivatedMembers(channel.Id)
if err != nil {
return model.NewAppError("RemoveAllDeactivatedMembersFromChannel", "app.channel.remove_all_deactivated_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
// MoveChannel method is prone to data races if someone joins to channel during the move process. However this
// function is only exposed to sysadmins and the possibility of this edge case is relatively small.
func (a *App) MoveChannel(c request.CTX, team *model.Team, channel *model.Channel, user *model.User) *model.AppError {
// Check that all channel members are in the destination team.
channelMembers, err := a.GetChannelMembersPage(c, channel.Id, 0, 10000000)
if err != nil {
return err
}
channelMemberIds := []string{}
for _, channelMember := range channelMembers {
channelMemberIds = append(channelMemberIds, channelMember.UserId)
}
if len(channelMemberIds) > 0 {
teamMembers, err2 := a.GetTeamMembersByIds(team.Id, channelMemberIds, nil)
if err2 != nil {
return err2
}
if len(teamMembers) != len(channelMembers) {
teamMembersMap := make(map[string]*model.TeamMember, len(teamMembers))
for _, teamMember := range teamMembers {
teamMembersMap[teamMember.UserId] = teamMember
}
for _, channelMember := range channelMembers {
if _, ok := teamMembersMap[channelMember.UserId]; !ok {
c.Logger().Warn("Not member of the target team", mlog.String("userId", channelMember.UserId))
}
}
return model.NewAppError("MoveChannel", "app.channel.move_channel.members_do_not_match.error", nil, "", http.StatusInternalServerError)
}
}
// keep instance of the previous team
previousTeam, nErr := a.Srv().Store().Team().Get(channel.TeamId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return model.NewAppError("MoveChannel", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return model.NewAppError("MoveChannel", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if nErr := a.Srv().Store().Channel().UpdateSidebarChannelCategoryOnMove(channel, team.Id); nErr != nil {
return model.NewAppError("MoveChannel", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
channel.TeamId = team.Id
if _, err := a.Srv().Store().Channel().Update(channel); err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return model.NewAppError("MoveChannel", "app.channel.update.bad_id", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr):
return appErr
default:
return model.NewAppError("MoveChannel", "app.channel.update_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if incomingWebhooks, err := a.GetIncomingWebhooksForTeamPage(previousTeam.Id, 0, 10000000); err != nil {
c.Logger().Warn("Failed to get incoming webhooks", mlog.Err(err))
} else {
for _, webhook := range incomingWebhooks {
if webhook.ChannelId == channel.Id {
webhook.TeamId = team.Id
if _, err := a.Srv().Store().Webhook().UpdateIncoming(webhook); err != nil {
c.Logger().Warn("Failed to move incoming webhook to new team", mlog.String("webhook id", webhook.Id))
}
}
}
}
if outgoingWebhooks, err := a.GetOutgoingWebhooksForTeamPage(previousTeam.Id, 0, 10000000); err != nil {
c.Logger().Warn("Failed to get outgoing webhooks", mlog.Err(err))
} else {
for _, webhook := range outgoingWebhooks {
if webhook.ChannelId == channel.Id {
webhook.TeamId = team.Id
if _, err := a.Srv().Store().Webhook().UpdateOutgoing(webhook); err != nil {
c.Logger().Warn("Failed to move outgoing webhook to new team.", mlog.String("webhook id", webhook.Id))
}
}
}
}
if err := a.RemoveUsersFromChannelNotMemberOfTeam(c, user, channel, team); err != nil {
c.Logger().Warn("error while removing non-team member users", mlog.Err(err))
}
if user != nil {
if err := a.postChannelMoveMessage(c, user, channel, previousTeam); err != nil {
c.Logger().Warn("error while posting move channel message", mlog.Err(err))
}
}
return nil
}
func (a *App) postChannelMoveMessage(c request.CTX, user *model.User, channel *model.Channel, previousTeam *model.Team) *model.AppError {
post := &model.Post{
ChannelId: channel.Id,
Message: fmt.Sprintf(i18n.T("api.team.move_channel.success"), previousTeam.Name),
Type: model.PostTypeMoveChannel,
UserId: user.Id,
Props: model.StringInterface{
"username": user.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("postChannelMoveMessage", "api.team.move_channel.post.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) RemoveUsersFromChannelNotMemberOfTeam(c request.CTX, remover *model.User, channel *model.Channel, team *model.Team) *model.AppError {
channelMembers, err := a.GetChannelMembersPage(c, channel.Id, 0, 10000000)
if err != nil {
return err
}
channelMemberIds := []string{}
channelMemberMap := make(map[string]struct{})
for _, channelMember := range channelMembers {
channelMemberMap[channelMember.UserId] = struct{}{}
channelMemberIds = append(channelMemberIds, channelMember.UserId)
}
if len(channelMemberIds) > 0 {
teamMembers, err := a.GetTeamMembersByIds(team.Id, channelMemberIds, nil)
if err != nil {
return err
}
if len(teamMembers) != len(channelMembers) {
for _, teamMember := range teamMembers {
delete(channelMemberMap, teamMember.UserId)
}
var removerId string
if remover != nil {
removerId = remover.Id
}
for userID := range channelMemberMap {
if err := a.removeUserFromChannel(c, userID, removerId, channel); err != nil {
return err
}
}
}
}
return nil
}
func (a *App) GetPinnedPosts(c request.CTX, channelID string) (*model.PostList, *model.AppError) {
posts, err := a.Srv().Store().Channel().GetPinnedPosts(channelID)
if err != nil {
return nil, model.NewAppError("GetPinnedPosts", "app.channel.pinned_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if appErr := a.filterInaccessiblePosts(posts, filterPostOptions{assumeSortedCreatedAt: true}); appErr != nil {
return nil, appErr
}
return posts, nil
}
func (a *App) ToggleMuteChannel(c request.CTX, channelID, userID string) (*model.ChannelMember, *model.AppError) {
member, nErr := a.Srv().Store().Channel().GetMember(context.Background(), channelID, userID)
if nErr != nil {
var appErr *model.AppError
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &appErr):
return nil, appErr
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("ToggleMuteChannel", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("ToggleMuteChannel", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
member.SetChannelMuted(!member.IsChannelMuted())
member, err := a.updateChannelMember(c, member)
if err != nil {
return nil, err
}
a.invalidateCacheForChannelMembersNotifyProps(member.ChannelId)
return member, nil
}
func (a *App) setChannelsMuted(c request.CTX, channelIDs []string, userID string, muted bool) ([]*model.ChannelMember, *model.AppError) {
members, err := a.Srv().Store().Channel().GetMembersByChannelIds(channelIDs, userID)
if err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("setChannelsMuted", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
var membersToUpdate []*model.ChannelMember
for _, member := range members {
if muted == member.IsChannelMuted() {
continue
}
updatedMember := member
updatedMember.SetChannelMuted(muted)
membersToUpdate = append(membersToUpdate, &updatedMember)
}
if len(membersToUpdate) == 0 {
return nil, nil
}
updated, err := a.Srv().Store().Channel().UpdateMultipleMembers(membersToUpdate)
if err != nil {
var appErr *model.AppError
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &nfErr):
return nil, model.NewAppError("setChannelsMuted", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("setChannelsMuted", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
for _, member := range updated {
a.invalidateCacheForChannelMembersNotifyProps(member.ChannelId)
evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", member.UserId, nil, "")
memberJSON, jsonErr := json.Marshal(member)
if jsonErr != nil {
return nil, model.NewAppError("setChannelsMuted", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
evt.Add("channelMember", string(memberJSON))
a.Publish(evt)
}
return updated, nil
}
func (a *App) FillInChannelProps(c request.CTX, channel *model.Channel) *model.AppError {
return a.FillInChannelsProps(c, model.ChannelList{channel})
}
func (a *App) FillInChannelsProps(c request.CTX, channelList model.ChannelList) *model.AppError {
// Group the channels by team and call GetChannelsByNames just once per team.
channelsByTeam := make(map[string]model.ChannelList)
for _, channel := range channelList {
channelsByTeam[channel.TeamId] = append(channelsByTeam[channel.TeamId], channel)
}
for teamID, channelList := range channelsByTeam {
allChannelMentions := make(map[string]bool)
channelMentions := make(map[*model.Channel][]string, len(channelList))
// Collect mentions across the channels so as to query just once for this team.
for _, channel := range channelList {
channelMentions[channel] = model.ChannelMentions(channel.Header)
for _, channelMention := range channelMentions[channel] {
allChannelMentions[channelMention] = true
}
}
allChannelMentionNames := make([]string, 0, len(allChannelMentions))
for channelName := range allChannelMentions {
allChannelMentionNames = append(allChannelMentionNames, channelName)
}
if len(allChannelMentionNames) > 0 {
mentionedChannels, err := a.GetChannelsByNames(c, allChannelMentionNames, teamID)
if err != nil {
return err
}
mentionedChannelsByName := make(map[string]*model.Channel)
for _, channel := range mentionedChannels {
mentionedChannelsByName[channel.Name] = channel
}
for _, channel := range channelList {
channelMentionsProp := make(map[string]any, len(channelMentions[channel]))
for _, channelMention := range channelMentions[channel] {
if mentioned, ok := mentionedChannelsByName[channelMention]; ok {
if mentioned.Type == model.ChannelTypeOpen {
channelMentionsProp[mentioned.Name] = map[string]any{
"display_name": mentioned.DisplayName,
}
}
}
}
if len(channelMentionsProp) > 0 {
channel.AddProp("channel_mentions", channelMentionsProp)
} else if channel.Props != nil {
delete(channel.Props, "channel_mentions")
}
}
}
}
return nil
}
func (a *App) forEachChannelMember(c request.CTX, channelID string, f func(model.ChannelMember) error) error {
perPage := 100
page := 0
for {
channelMembers, err := a.Srv().Store().Channel().GetMembers(channelID, page*perPage, perPage)
if err != nil {
return err
}
for _, channelMember := range channelMembers {
if err = f(channelMember); err != nil {
return err
}
}
length := len(channelMembers)
if length < perPage {
break
}
page++
}
return nil
}
func (a *App) ClearChannelMembersCache(c request.CTX, channelID string) error {
clearSessionCache := func(channelMember model.ChannelMember) error {
a.ClearSessionCacheForUser(channelMember.UserId)
message := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", channelMember.UserId, nil, "")
memberJSON, jsonErr := json.Marshal(channelMember)
if jsonErr != nil {
return jsonErr
}
message.Add("channelMember", string(memberJSON))
a.Publish(message)
return nil
}
if err := a.forEachChannelMember(c, channelID, clearSessionCache); err != nil {
return fmt.Errorf("error clearing cache for channel members: channel_id: %s, error: %w", channelID, err)
}
return nil
}
func (a *App) GetMemberCountsByGroup(ctx context.Context, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, *model.AppError) {
channelMemberCounts, err := a.Srv().Store().Channel().GetMemberCountsByGroup(ctx, channelID, includeTimezones)
if err != nil {
return nil, model.NewAppError("GetMemberCountsByGroup", "app.channel.get_member_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelMemberCounts, nil
}
func (a *App) getDirectChannel(c request.CTX, userID, otherUserID string) (*model.Channel, *model.AppError) {
return a.Srv().getDirectChannel(c, userID, otherUserID)
}
func (s *Server) getDirectChannel(c request.CTX, userID, otherUserID string) (*model.Channel, *model.AppError) {
channel, nErr := s.Store().Channel().GetByName("", model.GetDMNameFromIds(userID, otherUserID), true)
if nErr != nil {
var nfErr *store.ErrNotFound
if errors.As(nErr, &nfErr) {
return nil, nil
}
return nil, model.NewAppError("GetOrCreateDirectChannel", "web.incoming_webhook.channel.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return channel, nil
}
func (a *App) GetTopChannelsForTeamSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopChannelList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topChannels, err := a.Srv().Store().Channel().GetTopChannelsForTeamSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForTeamSince", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return topChannels, nil
}
func (a *App) GetTopChannelsForUserSince(c request.CTX, userID, teamID string, opts *model.InsightsOpts) (*model.TopChannelList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopChannelsForUserSince", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topChannels, err := a.Srv().Store().Channel().GetTopChannelsForUserSince(userID, teamID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForUserSince", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return topChannels, nil
}
// PostCountsByDuration returns the post counts for the given channels, grouped by day, starting at the given time.
// Unless one is specifically itending to omit results from part of the calendar day, it will typically makes the most sense to
// use a sinceUnixMillis parameter value as returned by model.GetStartOfDayMillis.
//
// WARNING: PostCountsByDuration PERFORMS NO AUTHORIZATION CHECKS ON THE GIVEN CHANNELS.
func (a *App) PostCountsByDuration(c request.CTX, channelIDs []string, sinceUnixMillis int64, userID *string, grouping model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("PostCountsByDuration", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
postCountByDay, err := a.Srv().Store().Channel().PostCountsByDuration(channelIDs, sinceUnixMillis, userID, grouping, groupingLocation)
if err != nil {
return nil, model.NewAppError("PostCountsByDuration", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return postCountByDay, nil
}
func (a *App) GetTopInactiveChannelsForTeamSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopInactiveChannelList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topChannels, err := a.Srv().Store().Channel().GetTopInactiveChannelsForTeamSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopInactiveChannelsForTeamSince", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return topChannels, nil
}
func (a *App) GetTopInactiveChannelsForUserSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopInactiveChannelList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopChannelsForUserSince", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topChannels, err := a.Srv().Store().Channel().GetTopInactiveChannelsForUserSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopInactiveChannelsForUserSince", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return topChannels, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) createInitialSidebarCategories(userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, *model.AppError) {
categories, nErr := a.Srv().Store().Channel().CreateInitialSidebarCategories(userID, opts)
if nErr != nil {
return nil, model.NewAppError("createInitialSidebarCategories", "app.channel.create_initial_sidebar_categories.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return categories, nil
}
func (a *App) GetSidebarCategoriesForTeamForUser(c request.CTX, userID, teamID string) (*model.OrderedSidebarCategories, *model.AppError) {
var appErr *model.AppError
categories, err := a.Srv().Store().Channel().GetSidebarCategoriesForTeamForUser(userID, teamID)
if err == nil && len(categories.Categories) == 0 {
// A user must always have categories, so migration must not have happened yet, and we should run it ourselves
categories, appErr = a.createInitialSidebarCategories(userID, &store.SidebarCategorySearchOpts{
TeamID: teamID,
ExcludeTeam: false,
})
if appErr != nil {
return nil, appErr
}
}
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetSidebarCategoriesForTeamForUser", "app.channel.sidebar_categories.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetSidebarCategoriesForTeamForUser", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return categories, nil
}
func (a *App) GetSidebarCategories(c request.CTX, userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, *model.AppError) {
var appErr *model.AppError
categories, err := a.Srv().Store().Channel().GetSidebarCategories(userID, opts)
if err == nil && len(categories.Categories) == 0 {
// A user must always have categories, so migration must not have happened yet, and we should run it ourselves
categories, appErr = a.createInitialSidebarCategories(userID, opts)
if appErr != nil {
return nil, appErr
}
}
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetSidebarCategories", "app.channel.sidebar_categories.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetSidebarCategories", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return categories, nil
}
func (a *App) GetSidebarCategoryOrder(c request.CTX, userID, teamID string) ([]string, *model.AppError) {
categories, err := a.Srv().Store().Channel().GetSidebarCategoryOrder(userID, teamID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetSidebarCategoryOrder", "app.channel.sidebar_categories.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetSidebarCategoryOrder", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return categories, nil
}
func (a *App) GetSidebarCategory(c request.CTX, categoryId string) (*model.SidebarCategoryWithChannels, *model.AppError) {
category, err := a.Srv().Store().Channel().GetSidebarCategory(categoryId)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetSidebarCategory", "app.channel.sidebar_categories.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetSidebarCategory", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return category, nil
}
func (a *App) CreateSidebarCategory(c request.CTX, userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) {
category, err := a.Srv().Store().Channel().CreateSidebarCategory(userID, teamID, newCategory)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("CreateSidebarCategory", "app.channel.sidebar_categories.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("CreateSidebarCategory", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
message := model.NewWebSocketEvent(model.WebsocketEventSidebarCategoryCreated, teamID, "", userID, nil, "")
message.Add("category_id", category.Id)
a.Publish(message)
return category, nil
}
func (a *App) UpdateSidebarCategoryOrder(c request.CTX, userID, teamID string, categoryOrder []string) *model.AppError {
err := a.Srv().Store().Channel().UpdateSidebarCategoryOrder(userID, teamID, categoryOrder)
if err != nil {
var nfErr *store.ErrNotFound
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &nfErr):
return model.NewAppError("UpdateSidebarCategoryOrder", "app.channel.sidebar_categories.app_error", nil, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &invErr):
return model.NewAppError("UpdateSidebarCategoryOrder", "app.channel.sidebar_categories.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return model.NewAppError("UpdateSidebarCategoryOrder", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
message := model.NewWebSocketEvent(model.WebsocketEventSidebarCategoryOrderUpdated, teamID, "", userID, nil, "")
message.Add("order", categoryOrder)
a.Publish(message)
return nil
}
func (a *App) UpdateSidebarCategories(c request.CTX, userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) {
updatedCategories, originalCategories, err := a.Srv().Store().Channel().UpdateSidebarCategories(userID, teamID, categories)
if err != nil {
return nil, model.NewAppError("UpdateSidebarCategories", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
message := model.NewWebSocketEvent(model.WebsocketEventSidebarCategoryUpdated, teamID, "", userID, nil, "")
updatedCategoriesJSON, jsonErr := json.Marshal(updatedCategories)
if jsonErr != nil {
return nil, model.NewAppError("UpdateSidebarCategories", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("updatedCategories", string(updatedCategoriesJSON))
a.Publish(message)
a.muteChannelsForUpdatedCategories(c, userID, updatedCategories, originalCategories)
return updatedCategories, nil
}
func (a *App) muteChannelsForUpdatedCategories(c request.CTX, userID string, updatedCategories []*model.SidebarCategoryWithChannels, originalCategories []*model.SidebarCategoryWithChannels) {
var channelsToMute []string
var channelsToUnmute []string
// Mute or unmute all channels in categories that were muted or unmuted
for i, updatedCategory := range updatedCategories {
if i > len(originalCategories)-1 {
// The two slices should be the same length, but double check that to be safe
continue
}
originalCategory := originalCategories[i]
if updatedCategory.Muted && !originalCategory.Muted {
channelsToMute = append(channelsToMute, updatedCategory.Channels...)
} else if !updatedCategory.Muted && originalCategory.Muted {
channelsToUnmute = append(channelsToUnmute, updatedCategory.Channels...)
}
}
// Mute any channels moved from an unmuted category into a muted one and vice versa
channelsDiff := diffChannelsBetweenCategories(updatedCategories, originalCategories)
if len(channelsDiff) != 0 {
makeCategoryMap := func(categories []*model.SidebarCategoryWithChannels) map[string]*model.SidebarCategoryWithChannels {
result := make(map[string]*model.SidebarCategoryWithChannels)
for _, category := range categories {
result[category.Id] = category
}
return result
}
updatedCategoriesById := makeCategoryMap(updatedCategories)
originalCategoriesById := makeCategoryMap(originalCategories)
for channelID, diff := range channelsDiff {
fromCategory := originalCategoriesById[diff.fromCategoryId]
toCategory := updatedCategoriesById[diff.toCategoryId]
if toCategory.Muted && !fromCategory.Muted {
channelsToMute = append(channelsToMute, channelID)
} else if !toCategory.Muted && fromCategory.Muted {
channelsToUnmute = append(channelsToUnmute, channelID)
}
}
}
if len(channelsToMute) > 0 {
_, err := a.setChannelsMuted(c, channelsToMute, userID, true)
if err != nil {
c.Logger().Error(
"Failed to mute channels to match category",
mlog.String("user_id", userID),
mlog.Err(err),
)
}
}
if len(channelsToUnmute) > 0 {
_, err := a.setChannelsMuted(c, channelsToUnmute, userID, false)
if err != nil {
c.Logger().Error(
"Failed to unmute channels to match category",
mlog.String("user_id", userID),
mlog.Err(err),
)
}
}
}
type categoryChannelDiff struct {
fromCategoryId string
toCategoryId string
}
func diffChannelsBetweenCategories(updatedCategories []*model.SidebarCategoryWithChannels, originalCategories []*model.SidebarCategoryWithChannels) map[string]*categoryChannelDiff {
// mapChannelIdsToCategories returns a map of channel IDs to the IDs of the categories that they're a member of.
mapChannelIdsToCategories := func(categories []*model.SidebarCategoryWithChannels) map[string]string {
result := make(map[string]string)
for _, category := range categories {
for _, channelID := range category.Channels {
result[channelID] = category.Id
}
}
return result
}
updatedChannelIdsMap := mapChannelIdsToCategories(updatedCategories)
originalChannelIdsMap := mapChannelIdsToCategories(originalCategories)
// Check for any channels that have changed categories. Note that we don't worry about any channels that have moved
// outside of these categories since that heavily complicates things and doesn't currently happen in our apps.
channelsDiff := make(map[string]*categoryChannelDiff)
for channelID, originalCategoryId := range originalChannelIdsMap {
updatedCategoryId := updatedChannelIdsMap[channelID]
if originalCategoryId != updatedCategoryId && updatedCategoryId != "" {
channelsDiff[channelID] = &categoryChannelDiff{originalCategoryId, updatedCategoryId}
}
}
return channelsDiff
}
func (a *App) DeleteSidebarCategory(c request.CTX, userID, teamID, categoryId string) *model.AppError {
err := a.Srv().Store().Channel().DeleteSidebarCategory(categoryId)
if err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return model.NewAppError("DeleteSidebarCategory", "app.channel.sidebar_categories.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return model.NewAppError("DeleteSidebarCategory", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
message := model.NewWebSocketEvent(model.WebsocketEventSidebarCategoryDeleted, teamID, "", userID, nil, "")
message.Add("category_id", categoryId)
a.Publish(message)
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"fmt"
"runtime"
"strings"
"sync"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/imaging"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/services/imageproxy"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const ServerKey product.ServiceKey = "server"
// licenseSvc is added to act as a starting point for future integrated products.
// It has the same signature and functionality with the license related APIs of the plugin-api.
type licenseSvc interface {
GetLicense() *model.License
RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError
}
// Channels contains all channels related state.
type Channels struct {
srv *Server
cfgSvc product.ConfigService
filestore filestore.FileBackend
licenseSvc licenseSvc
routerSvc *routerService
postActionCookieSecret []byte
pluginCommandsLock sync.RWMutex
pluginCommands []*PluginCommand
pluginsLock sync.RWMutex
pluginsEnvironment *plugin.Environment
pluginConfigListenerID string
productCommandsLock sync.RWMutex
productCommands []*ProductCommand
imageProxy *imageproxy.ImageProxy
// cached counts that are used during notice condition validation
cachedPostCount int64
cachedUserCount int64
cachedDBMSVersion string
// previously fetched notices
cachedNotices model.ProductNotices
AccountMigration einterfaces.AccountMigrationInterface
Compliance einterfaces.ComplianceInterface
DataRetention einterfaces.DataRetentionInterface
MessageExport einterfaces.MessageExportInterface
Saml einterfaces.SamlInterface
Notification einterfaces.NotificationInterface
Ldap einterfaces.LdapInterface
// These are used to prevent concurrent upload requests
// for a given upload session which could cause inconsistencies
// and data corruption.
uploadLockMapMut sync.Mutex
uploadLockMap map[string]bool
imgDecoder *imaging.Decoder
imgEncoder *imaging.Encoder
dndTaskMut sync.Mutex
dndTask *model.ScheduledTask
postReminderMut sync.Mutex
postReminderTask *model.ScheduledTask
// collectionTypes maps from collection types to the registering plugin id
collectionTypes map[string]string
// topicTypes maps from topic types to collection types
topicTypes map[string]string
collectionAndTopicTypesMut sync.Mutex
}
func init() {
product.RegisterProduct("channels", product.Manifest{
Initializer: func(services map[product.ServiceKey]any) (product.Product, error) {
return NewChannels(services)
},
Dependencies: map[product.ServiceKey]struct{}{
ServerKey: {},
product.ConfigKey: {},
product.LicenseKey: {},
product.FilestoreKey: {},
},
})
}
func NewChannels(services map[product.ServiceKey]any) (*Channels, error) {
s, ok := services[ServerKey].(*Server)
if !ok {
return nil, errors.New("server not passed")
}
ch := &Channels{
srv: s,
imageProxy: imageproxy.MakeImageProxy(s.platform, s.httpService, s.Log()),
uploadLockMap: map[string]bool{},
collectionTypes: map[string]string{},
topicTypes: map[string]string{},
}
// To get another service:
// 1. Prepare the service interface
// 2. Add the field to *Channels
// 3. Add the service key to the slice.
// 4. Add a new case in the switch statement.
requiredServices := []product.ServiceKey{
product.ConfigKey,
product.LicenseKey,
product.FilestoreKey,
}
for _, svcKey := range requiredServices {
svc, ok := services[svcKey]
if !ok {
return nil, fmt.Errorf("Service %s not passed", svcKey)
}
switch svcKey {
// Keep adding more services here
case product.ConfigKey:
cfgSvc, ok := svc.(product.ConfigService)
if !ok {
return nil, errors.New("Config service did not satisfy ConfigSvc interface")
}
ch.cfgSvc = cfgSvc
case product.FilestoreKey:
filestore, ok := svc.(filestore.FileBackend)
if !ok {
return nil, errors.New("Filestore service did not satisfy FileBackend interface")
}
ch.filestore = filestore
case product.LicenseKey:
svc, ok := svc.(licenseSvc)
if !ok {
return nil, errors.New("License service did not satisfy licenseSvc interface")
}
ch.licenseSvc = svc
}
}
// We are passing a partially filled Channels struct so that the enterprise
// methods can have access to app methods.
// Otherwise, passing server would mean it has to call s.Channels(),
// which would be nil at this point.
if complianceInterface != nil {
ch.Compliance = complianceInterface(New(ServerConnector(ch)))
}
if messageExportInterface != nil {
ch.MessageExport = messageExportInterface(New(ServerConnector(ch)))
}
if dataRetentionInterface != nil {
ch.DataRetention = dataRetentionInterface(New(ServerConnector(ch)))
}
if accountMigrationInterface != nil {
ch.AccountMigration = accountMigrationInterface(New(ServerConnector(ch)))
}
if ldapInterface != nil {
ch.Ldap = ldapInterface(New(ServerConnector(ch)))
}
if notificationInterface != nil {
ch.Notification = notificationInterface(New(ServerConnector(ch)))
}
if samlInterfaceNew != nil {
ch.Saml = samlInterfaceNew(New(ServerConnector(ch)))
if err := ch.Saml.ConfigureSP(); err != nil {
s.Log().Error("An error occurred while configuring SAML Service Provider", mlog.Err(err))
}
ch.AddConfigListener(func(_, _ *model.Config) {
if err := ch.Saml.ConfigureSP(); err != nil {
s.Log().Error("An error occurred while configuring SAML Service Provider", mlog.Err(err))
}
})
}
var imgErr error
decoderConcurrency := int(*ch.cfgSvc.Config().FileSettings.MaxImageDecoderConcurrency)
if decoderConcurrency == -1 {
decoderConcurrency = runtime.NumCPU()
}
ch.imgDecoder, imgErr = imaging.NewDecoder(imaging.DecoderOptions{
ConcurrencyLevel: decoderConcurrency,
})
if imgErr != nil {
return nil, errors.Wrap(imgErr, "failed to create image decoder")
}
ch.imgEncoder, imgErr = imaging.NewEncoder(imaging.EncoderOptions{
ConcurrencyLevel: runtime.NumCPU(),
})
if imgErr != nil {
return nil, errors.Wrap(imgErr, "failed to create image encoder")
}
ch.routerSvc = newRouterService()
services[product.RouterKey] = ch.routerSvc
// Setup routes.
pluginsRoute := ch.srv.Router.PathPrefix("/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}").Subrouter()
pluginsRoute.HandleFunc("", ch.ServePluginRequest)
pluginsRoute.HandleFunc("/public/{public_file:.*}", ch.ServePluginPublicRequest)
pluginsRoute.HandleFunc("/{anything:.*}", ch.ServePluginRequest)
services[product.ChannelKey] = &channelsWrapper{
app: &App{ch: ch},
}
services[product.PostKey] = &postServiceWrapper{
app: &App{ch: ch},
}
services[product.PermissionsKey] = &permissionsServiceWrapper{
app: &App{ch: ch},
}
services[product.TeamKey] = &teamServiceWrapper{
app: &App{ch: ch},
}
services[product.BotKey] = &botServiceWrapper{
app: &App{ch: ch},
}
services[product.HooksKey] = &hooksService{
ch: ch,
}
services[product.UserKey] = &App{ch: ch}
services[product.PreferencesKey] = &preferencesServiceWrapper{
app: &App{ch: ch},
}
services[product.CommandKey] = &App{ch: ch}
services[product.ThreadsKey] = &App{ch: ch}
return ch, nil
}
func (ch *Channels) Start() error {
// Start plugins
ctx := request.EmptyContext(ch.srv.Log())
ch.initPlugins(ctx, *ch.cfgSvc.Config().PluginSettings.Directory, *ch.cfgSvc.Config().PluginSettings.ClientDirectory)
ch.AddConfigListener(func(prevCfg, cfg *model.Config) {
// We compute the difference between configs
// to ensure we don't re-init plugins unnecessarily.
diffs, err := config.Diff(prevCfg, cfg)
if err != nil {
ch.srv.Log().Warn("Error in comparing configs", mlog.Err(err))
return
}
hasDiff := false
// TODO: This could be a method on ConfigDiffs itself
for _, diff := range diffs {
if strings.HasPrefix(diff.Path, "PluginSettings.") {
hasDiff = true
break
}
}
// Do only if some plugin related settings has changed.
if hasDiff {
if *cfg.PluginSettings.Enable {
ch.initPlugins(ctx, *cfg.PluginSettings.Directory, *ch.cfgSvc.Config().PluginSettings.ClientDirectory)
} else {
ch.ShutDownPlugins()
}
}
})
// TODO: This should be moved to the platform service.
if err := ch.srv.platform.EnsureAsymmetricSigningKey(); err != nil {
return errors.Wrapf(err, "unable to ensure asymmetric signing key")
}
if err := ch.ensurePostActionCookieSecret(); err != nil {
return errors.Wrapf(err, "unable to ensure PostAction cookie secret")
}
return nil
}
func (ch *Channels) Stop() error {
ch.ShutDownPlugins()
ch.dndTaskMut.Lock()
if ch.dndTask != nil {
ch.dndTask.Cancel()
}
ch.dndTaskMut.Unlock()
return nil
}
func (ch *Channels) AddConfigListener(listener func(*model.Config, *model.Config)) string {
return ch.cfgSvc.AddConfigListener(listener)
}
func (ch *Channels) RemoveConfigListener(id string) {
ch.cfgSvc.RemoveConfigListener(id)
}
func (ch *Channels) License() *model.License {
return ch.licenseSvc.GetLicense()
}
func (ch *Channels) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError {
return ch.licenseSvc.RequestTrialLicense(requesterID, users, termsAccepted,
receiveEmailsAccepted)
}
func (a *App) HooksManager() *product.HooksManager {
return a.ch.srv.hooksManager
}
// Ensure hooksService implements `product.HooksService`
var _ product.HooksService = (*hooksService)(nil)
type hooksService struct {
ch *Channels
}
func (s *hooksService) RegisterHooks(productID string, hooks any) error {
return s.ch.srv.hooksManager.AddProduct(productID, hooks)
}
func (ch *Channels) RunMultiHook(hookRunnerFunc func(hooks plugin.Hooks) bool, hookId int) {
if env := ch.GetPluginsEnvironment(); env != nil {
env.RunMultiPluginHook(hookRunnerFunc, hookId)
}
// run hook for the products
ch.srv.hooksManager.RunMultiHook(hookRunnerFunc, hookId)
}
func (ch *Channels) HooksForPluginOrProduct(id string) (plugin.Hooks, error) {
var hooks plugin.Hooks
if env := ch.GetPluginsEnvironment(); env != nil {
// we intentionally ignore the error here, because the id can be a product id
// we are going to check if we have the hooks or not
hooks, _ = env.HooksForPlugin(id)
if hooks != nil {
return hooks, nil
}
}
hooks = ch.srv.hooksManager.HooksForProduct(id)
if hooks != nil {
return hooks, nil
}
return nil, fmt.Errorf("could not find hooks for id %s", id)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"fmt"
"io"
"net/http"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// Ensure cloud service wrapper implements `product.CloudService`
var _ product.CloudService = (*cloudWrapper)(nil)
// cloudWrapper provides an implementation of `product.CloudService` for use by products.
type cloudWrapper struct {
cloud einterfaces.CloudInterface
}
func (c *cloudWrapper) GetCloudLimits() (*model.ProductLimits, error) {
if c.cloud != nil {
return c.cloud.GetCloudLimits("")
}
return &model.ProductLimits{}, nil
}
func (a *App) getSysAdminsEmailRecipients() ([]*model.User, *model.AppError) {
userOptions := &model.UserGetOptions{
Page: 0,
PerPage: 100,
Role: model.SystemAdminRoleId,
Inactive: false,
}
return a.GetUsersFromProfiles(userOptions)
}
func getCurrentPlanName(a *App) (string, *model.AppError) {
subscription, err := a.Cloud().GetSubscription("")
if err != nil {
return "", model.NewAppError("getCurrentPlanName", "app.cloud.get_subscription.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if subscription == nil {
return "", model.NewAppError("getCurrentPlanName", "app.cloud.get_subscription.app_error", nil, "", http.StatusInternalServerError)
}
products, err := a.Cloud().GetCloudProducts("", false)
if err != nil {
return "", model.NewAppError("getCurrentPlanName", "app.cloud.get_cloud_products.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if products == nil {
return "", model.NewAppError("getCurrentPlanName", "app.cloud.get_cloud_products.app_error", nil, "", http.StatusInternalServerError)
}
planName := getCurrentProduct(subscription.ProductID, products).Name
return planName, nil
}
func (a *App) SendPaymentFailedEmail(failedPayment *model.FailedPayment) *model.AppError {
sysAdmins, err := a.getSysAdminsEmailRecipients()
if err != nil {
return err
}
planName, err := getCurrentPlanName(a)
if err != nil {
return model.NewAppError("SendPaymentFailedEmail", "app.cloud.get_current_plan_name.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, admin := range sysAdmins {
_, err := a.Srv().EmailService.SendPaymentFailedEmail(admin.Email, admin.Locale, failedPayment, planName, *a.Config().ServiceSettings.SiteURL)
if err != nil {
a.Log().Error("Error sending payment failed email", mlog.Err(err))
}
}
return nil
}
func getCurrentProduct(subscriptionProductID string, products []*model.Product) *model.Product {
for _, product := range products {
if product.ID == subscriptionProductID {
return product
}
}
return nil
}
func (a *App) SendDelinquencyEmail(emailToSend model.DelinquencyEmail) *model.AppError {
sysAdmins, aErr := a.getSysAdminsEmailRecipients()
if aErr != nil {
return aErr
}
planName, aErr := getCurrentPlanName(a)
if aErr != nil {
return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_current_plan_name.app_error", nil, aErr.Error(), http.StatusInternalServerError)
}
subscription, err := a.Cloud().GetSubscription("")
if err != nil {
return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_subscription.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if subscription == nil {
return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_subscription.app_error", nil, "", http.StatusInternalServerError)
}
if subscription.DelinquentSince == nil {
return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_subscription_delinquency_date.app_error", nil, "", http.StatusInternalServerError)
}
delinquentSince := time.Unix(*subscription.DelinquentSince, 0)
delinquencyDate := delinquentSince.Format("01/02/2006")
for _, admin := range sysAdmins {
switch emailToSend {
case model.DelinquencyEmail7:
err := a.Srv().EmailService.SendDelinquencyEmail7(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName)
if err != nil {
a.Log().Error("Error sending delinquency email 7", mlog.Err(err))
}
case model.DelinquencyEmail14:
err := a.Srv().EmailService.SendDelinquencyEmail14(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName)
if err != nil {
a.Log().Error("Error sending delinquency email 14", mlog.Err(err))
}
case model.DelinquencyEmail30:
err := a.Srv().EmailService.SendDelinquencyEmail30(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName)
if err != nil {
a.Log().Error("Error sending delinquency email 30", mlog.Err(err))
}
case model.DelinquencyEmail45:
err := a.Srv().EmailService.SendDelinquencyEmail45(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName, delinquencyDate)
if err != nil {
a.Log().Error("Error sending delinquency email 45", mlog.Err(err))
}
case model.DelinquencyEmail60:
err := a.Srv().EmailService.SendDelinquencyEmail60(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL)
if err != nil {
a.Log().Error("Error sending delinquency email 60", mlog.Err(err))
}
case model.DelinquencyEmail75:
err := a.Srv().EmailService.SendDelinquencyEmail75(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName, delinquencyDate)
if err != nil {
a.Log().Error("Error sending delinquency email 75", mlog.Err(err))
}
case model.DelinquencyEmail90:
err := a.Srv().EmailService.SendDelinquencyEmail90(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL)
if err != nil {
a.Log().Error("Error sending delinquency email 90", mlog.Err(err))
}
}
}
return nil
}
func (a *App) AdjustInProductLimits(limits *model.ProductLimits, subscription *model.Subscription) *model.AppError {
if limits.Teams != nil && limits.Teams.Active != nil && *limits.Teams.Active > 0 {
err := a.AdjustTeamsFromProductLimits(limits.Teams)
if err != nil {
return err
}
}
return nil
}
func getNextBillingDateString() string {
now := time.Now()
t := time.Date(now.Year(), now.Month()+1, 1, 0, 0, 0, 0, time.UTC)
return fmt.Sprintf("%s %d, %d", t.Month(), t.Day(), t.Year())
}
func (a *App) SendUpgradeConfirmationEmail(isYearly bool) *model.AppError {
sysAdmins, e := a.getSysAdminsEmailRecipients()
if e != nil {
return e
}
if len(sysAdmins) == 0 {
return model.NewAppError("app.SendCloudUpgradeConfirmationEmail", "app.user.send_emails.app_error", nil, "", http.StatusInternalServerError)
}
subscription, err := a.Cloud().GetSubscription("")
if err != nil {
return model.NewAppError("app.SendCloudUpgradeConfirmationEmail", "app.user.send_emails.app_error", nil, "", http.StatusInternalServerError)
}
billingDate := getNextBillingDateString()
// we want to at least have one email sent out to an admin
countNotOks := 0
embeddedFiles := make(map[string]io.Reader)
if isYearly {
pdf, filename, pdfErr := a.Cloud().GetInvoicePDF("", subscription.LastInvoice.ID)
if pdfErr != nil {
a.Log().Error("Error retrieving the invoice for subscription id", mlog.String("subscription", subscription.ID), mlog.Err(pdfErr))
} else {
embeddedFiles = map[string]io.Reader{
filename: bytes.NewReader(pdf),
}
}
}
for _, admin := range sysAdmins {
name := admin.FirstName
if name == "" {
name = admin.Username
}
err := a.Srv().EmailService.SendCloudUpgradeConfirmationEmail(admin.Email, name, billingDate, admin.Locale, *a.Config().ServiceSettings.SiteURL, subscription.GetWorkSpaceNameFromDNS(), isYearly, embeddedFiles)
if err != nil {
a.Log().Error("Error sending trial ended email to", mlog.String("email", admin.Email), mlog.Err(err))
countNotOks++
}
}
// if not even one admin got an email, we consider that this operation errored
if countNotOks == len(sysAdmins) {
return model.NewAppError("app.SendCloudUpgradeConfirmationEmail", "app.user.send_emails.app_error", nil, "", http.StatusInternalServerError)
}
return nil
}
// SendNoCardPaymentFailedEmail
func (a *App) SendNoCardPaymentFailedEmail() *model.AppError {
sysAdmins, err := a.getSysAdminsEmailRecipients()
if err != nil {
return err
}
for _, admin := range sysAdmins {
err := a.Srv().EmailService.SendNoCardPaymentFailedEmail(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL)
if err != nil {
a.Log().Error("Error sending payment failed email", mlog.Err(err))
}
}
return nil
}
// Create/ Update a subscription history event
func (a *App) SendSubscriptionHistoryEvent(userID string) (*model.SubscriptionHistory, error) {
license := a.Srv().License()
// No need to create a Subscription History Event if the license isn't cloud
if !license.IsCloud() {
return nil, nil
}
// Get user count
userCount, err := a.Srv().Store().User().Count(model.UserCountOptions{})
if err != nil {
return nil, err
}
return a.Cloud().CreateOrUpdateSubscriptionHistoryEvent(userID, int(userCount))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
// Registers a given function to be called when the cluster leader may have changed. Returns a unique ID for the
// listener which can later be used to remove it. If clustering is not enabled in this build, the callback will never
// be called.
func (s *Server) AddClusterLeaderChangedListener(listener func()) string {
return s.platform.AddClusterLeaderChangedListener(listener)
}
// Removes a listener function by the unique ID returned when AddConfigListener was called
func (s *Server) RemoveClusterLeaderChangedListener(id string) {
s.platform.RemoveClusterLeaderChangedListener(id)
}
func (s *Server) InvokeClusterLeaderChangedListeners() {
s.platform.InvokeClusterLeaderChangedListeners()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
func (s *Server) IsLeader() bool {
return s.platform.IsLeader()
}
func (a *App) IsLeader() bool {
return a.Srv().IsLeader()
}
func (a *App) GetClusterId() string {
return a.Srv().Platform().GetClusterId()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (s *Server) clusterInstallPluginHandler(msg *model.ClusterMessage) {
var data model.PluginEventData
if jsonErr := json.Unmarshal(msg.Data, &data); jsonErr != nil {
mlog.Warn("Failed to decode from JSON", mlog.Err(jsonErr))
}
s.Channels().installPluginFromData(data)
}
func (s *Server) clusterRemovePluginHandler(msg *model.ClusterMessage) {
var data model.PluginEventData
if jsonErr := json.Unmarshal(msg.Data, &data); jsonErr != nil {
mlog.Warn("Failed to decode from JSON", mlog.Err(jsonErr))
}
s.Channels().removePluginFromData(data)
}
func (s *Server) clusterPluginEventHandler(msg *model.ClusterMessage) {
if msg.Props == nil {
mlog.Warn("ClusterMessage.Props for plugin event should not be nil")
return
}
pluginID := msg.Props["PluginID"]
// if the plugin key is empty, the message might be coming from a product.
if pluginID == "" {
pluginID = msg.Props["ProductID"]
}
eventID := msg.Props["EventID"]
if pluginID == "" || eventID == "" {
mlog.Warn("Invalid ClusterMessage.Props values for plugin event",
mlog.String("plugin_id", pluginID), mlog.String("event_id", eventID))
return
}
channels, ok := s.products["channels"].(*Channels)
if !ok {
return
}
hooks, err := channels.HooksForPluginOrProduct(pluginID)
if err != nil {
mlog.Warn("Getting hooks for plugin failed", mlog.String("plugin_id", pluginID), mlog.Err(err))
return
}
hooks.OnPluginClusterEvent(&plugin.Context{}, model.PluginClusterEvent{
Id: eventID,
Data: msg.Data,
})
}
// registerClusterHandlers registers the cluster message handlers that are handled by the server.
//
// The cluster event handlers are spread across this function and NewLocalCacheLayer.
// Be careful to not have duplicated handlers here and there.
func (s *Server) registerClusterHandlers() {
s.platform.RegisterClusterMessageHandler(model.ClusterEventInstallPlugin, s.clusterInstallPluginHandler)
s.platform.RegisterClusterMessageHandler(model.ClusterEventRemovePlugin, s.clusterRemovePluginHandler)
s.platform.RegisterClusterMessageHandler(model.ClusterEventPluginEvent, s.clusterPluginEventHandler)
s.platform.RegisterClusterHandlers()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) RegisterCollectionAndTopic(pluginID, collectionType, topicType string) error {
// we have a race condition due to multiple plugins calling this method
a.ch.collectionAndTopicTypesMut.Lock()
defer a.ch.collectionAndTopicTypesMut.Unlock()
// check if collectionType was already registered by other plugin
existingPluginID, ok := a.ch.collectionTypes[collectionType]
if ok && existingPluginID != pluginID {
return model.NewAppError("registerCollectionAndTopic", "app.collection.add_collection.exists.app_error", nil, "", http.StatusBadRequest)
}
// check if topicType was already registered to other collection
existingCollectionType, ok := a.ch.topicTypes[topicType]
if ok && existingCollectionType != collectionType {
return model.NewAppError("registerCollectionAndTopic", "app.collection.add_topic.exists.app_error", nil, "", http.StatusBadRequest)
}
a.ch.collectionTypes[collectionType] = pluginID
a.ch.topicTypes[topicType] = collectionType
a.ch.srv.Log().Info("registered collection and topic type", mlog.String("plugin_id", pluginID), mlog.String("collection_type", collectionType), mlog.String("topic_type", topicType))
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"errors"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"sync"
"unicode"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
CmdCustomStatusTrigger = "status"
usernameSpecialChars = ".-_"
maxTriggerLen = 512
)
var atMentionRegexp = regexp.MustCompile(`\B@[[:alnum:]][[:alnum:]\.\-_:]*`)
type CommandProvider interface {
GetTrigger() string
GetCommand(a *App, T i18n.TranslateFunc) *model.Command
DoCommand(a *App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse
}
var commandProviders = make(map[string]CommandProvider)
func RegisterCommandProvider(newProvider CommandProvider) {
commandProviders[newProvider.GetTrigger()] = newProvider
}
func GetCommandProvider(name string) CommandProvider {
provider, ok := commandProviders[name]
if ok {
return provider
}
return nil
}
// @openTracingParams teamID, skipSlackParsing
func (a *App) CreateCommandPost(c request.CTX, post *model.Post, teamID string, response *model.CommandResponse, skipSlackParsing bool) (*model.Post, *model.AppError) {
if skipSlackParsing {
post.Message = response.Text
} else {
post.Message = model.ParseSlackLinksToMarkdown(response.Text)
}
post.CreateAt = model.GetMillis()
if strings.HasPrefix(post.Type, model.PostSystemMessagePrefix) {
err := model.NewAppError("CreateCommandPost", "api.context.invalid_param.app_error", map[string]any{"Name": "post.type"}, "", http.StatusBadRequest)
return nil, err
}
if response.Attachments != nil {
model.ParseSlackAttachment(post, response.Attachments)
}
if response.ResponseType == model.CommandResponseTypeInChannel {
return a.CreatePostMissingChannel(c, post, true, true)
}
if (response.ResponseType == "" || response.ResponseType == model.CommandResponseTypeEphemeral) && (response.Text != "" || response.Attachments != nil) {
a.SendEphemeralPost(c, post.UserId, post)
}
return post, nil
}
// @openTracingParams teamID
// previous ListCommands now ListAutocompleteCommands
func (a *App) ListAutocompleteCommands(teamID string, T i18n.TranslateFunc) ([]*model.Command, *model.AppError) {
commands := make([]*model.Command, 0, 32)
seen := make(map[string]bool)
// Disable custom status slash command if the feature or the setting is off
if !*a.Config().TeamSettings.EnableCustomUserStatuses {
seen[CmdCustomStatusTrigger] = true
}
for _, cmd := range a.CommandsForTeam(teamID) {
if cmd.AutoComplete && !seen[cmd.Trigger] {
seen[cmd.Trigger] = true
commands = append(commands, cmd)
}
}
if *a.Config().ServiceSettings.EnableCommands {
teamCmds, err := a.Srv().Store().Command().GetByTeam(teamID)
if err != nil {
return nil, model.NewAppError("ListAutocompleteCommands", "app.command.listautocompletecommands.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, cmd := range teamCmds {
if cmd.AutoComplete && !seen[cmd.Trigger] {
cmd.Sanitize()
seen[cmd.Trigger] = true
commands = append(commands, cmd)
}
}
}
for _, value := range commandProviders {
if cmd := value.GetCommand(a, T); cmd != nil {
cpy := *cmd
if cpy.AutoComplete && !seen[cpy.Trigger] {
cpy.Sanitize()
seen[cpy.Trigger] = true
commands = append(commands, &cpy)
}
}
}
return commands, nil
}
func (a *App) ListTeamCommands(teamID string) ([]*model.Command, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCommands {
return nil, model.NewAppError("ListTeamCommands", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
teamCmds, err := a.Srv().Store().Command().GetByTeam(teamID)
if err != nil {
return nil, model.NewAppError("ListTeamCommands", "app.command.listteamcommands.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teamCmds, nil
}
func (a *App) ListAllCommands(teamID string, T i18n.TranslateFunc) ([]*model.Command, *model.AppError) {
commands := make([]*model.Command, 0, 32)
seen := make(map[string]bool)
for _, value := range commandProviders {
if cmd := value.GetCommand(a, T); cmd != nil {
cpy := *cmd
if cpy.AutoComplete && !seen[cpy.Trigger] {
cpy.Sanitize()
seen[cpy.Trigger] = true
commands = append(commands, &cpy)
}
}
}
for _, cmd := range a.CommandsForTeam(teamID) {
if !seen[cmd.Trigger] {
seen[cmd.Trigger] = true
commands = append(commands, cmd)
}
}
if *a.Config().ServiceSettings.EnableCommands {
teamCmds, err := a.Srv().Store().Command().GetByTeam(teamID)
if err != nil {
return nil, model.NewAppError("ListAllCommands", "app.command.listallcommands.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, cmd := range teamCmds {
if !seen[cmd.Trigger] {
cmd.Sanitize()
seen[cmd.Trigger] = true
commands = append(commands, cmd)
}
}
}
return commands, nil
}
// @openTracingParams args
func (a *App) ExecuteCommand(c request.CTX, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
trigger := ""
message := ""
index := strings.IndexFunc(args.Command, unicode.IsSpace)
if index != -1 {
trigger = args.Command[:index]
message = args.Command[index+1:]
} else {
trigger = args.Command
}
trigger = strings.ToLower(trigger)
if !strings.HasPrefix(trigger, "/") {
return nil, model.NewAppError("command", "api.command.execute_command.format.app_error", map[string]any{"Trigger": trigger}, "", http.StatusBadRequest)
}
trigger = strings.TrimPrefix(trigger, "/")
clientTriggerId, triggerId, appErr := model.GenerateTriggerId(args.UserId, a.AsymmetricSigningKey())
if appErr != nil {
c.Logger().Warn("error occurred in generating trigger Id for a user ", mlog.Err(appErr))
}
args.TriggerId = triggerId
// Plugins can override built in, custom, and product commands
cmd, response, appErr := a.tryExecutePluginCommand(c, args)
if appErr != nil {
return nil, appErr
} else if cmd != nil && response != nil {
response.TriggerId = clientTriggerId
return a.HandleCommandResponse(c, cmd, args, response, true)
}
// Products can override built in and custom commands
cmd, response, appErr = a.tryExecuteProductCommand(c, args)
if appErr != nil {
return nil, appErr
} else if cmd != nil && response != nil {
response.TriggerId = clientTriggerId
return a.HandleCommandResponse(c, cmd, args, response, true)
}
// Custom commands can override built ins
cmd, response, appErr = a.tryExecuteCustomCommand(c, args, trigger, message)
if appErr != nil {
return nil, appErr
} else if cmd != nil && response != nil {
response.TriggerId = clientTriggerId
return a.HandleCommandResponse(c, cmd, args, response, false)
}
cmd, response = a.tryExecuteBuiltInCommand(c, args, trigger, message)
if cmd != nil && response != nil {
return a.HandleCommandResponse(c, cmd, args, response, true)
}
if len(trigger) > maxTriggerLen {
trigger = trigger[:maxTriggerLen]
trigger += "..."
}
return nil, model.NewAppError("command", "api.command.execute_command.not_found.app_error", map[string]any{"Trigger": trigger}, "", http.StatusNotFound)
}
// MentionsToTeamMembers returns all the @ mentions found in message that
// belong to users in the specified team, linking them to their users
func (a *App) MentionsToTeamMembers(c request.CTX, message, teamID string) model.UserMentionMap {
type mentionMapItem struct {
Name string
Id string
}
possibleMentions := possibleAtMentions(message)
mentionChan := make(chan *mentionMapItem, len(possibleMentions))
var wg sync.WaitGroup
for _, mention := range possibleMentions {
wg.Add(1)
go func(mention string) {
defer wg.Done()
user, nErr := a.Srv().Store().User().GetByUsername(mention)
var nfErr *store.ErrNotFound
if nErr != nil && !errors.As(nErr, &nfErr) {
c.Logger().Warn("Failed to retrieve user @"+mention, mlog.Err(nErr))
return
}
// If it's a http.StatusNotFound error, check for usernames in substrings
// without trailing punctuation
if nErr != nil {
trimmed, ok := trimUsernameSpecialChar(mention)
for ; ok; trimmed, ok = trimUsernameSpecialChar(trimmed) {
userFromTrimmed, nErr := a.Srv().Store().User().GetByUsername(trimmed)
if nErr != nil && !errors.As(nErr, &nfErr) {
return
}
if nErr != nil {
continue
}
_, err := a.GetTeamMember(teamID, userFromTrimmed.Id)
if err != nil {
// The user is not in the team, so we should ignore it
return
}
mentionChan <- &mentionMapItem{trimmed, userFromTrimmed.Id}
return
}
return
}
_, err := a.GetTeamMember(teamID, user.Id)
if err != nil {
// The user is not in the team, so we should ignore it
return
}
mentionChan <- &mentionMapItem{mention, user.Id}
}(mention)
}
wg.Wait()
close(mentionChan)
atMentionMap := make(model.UserMentionMap)
for mention := range mentionChan {
atMentionMap[mention.Name] = mention.Id
}
return atMentionMap
}
// MentionsToPublicChannels returns all the mentions to public channels,
// linking them to their channels
func (a *App) MentionsToPublicChannels(c request.CTX, message, teamID string) model.ChannelMentionMap {
type mentionMapItem struct {
Name string
Id string
}
channelMentions := model.ChannelMentions(message)
mentionChan := make(chan *mentionMapItem, len(channelMentions))
var wg sync.WaitGroup
for _, channelName := range channelMentions {
wg.Add(1)
go func(channelName string) {
defer wg.Done()
channel, err := a.GetChannelByName(c, channelName, teamID, false)
if err != nil {
return
}
if !channel.IsOpen() {
return
}
mentionChan <- &mentionMapItem{channelName, channel.Id}
}(channelName)
}
wg.Wait()
close(mentionChan)
channelMentionMap := make(model.ChannelMentionMap)
for mention := range mentionChan {
channelMentionMap[mention.Name] = mention.Id
}
return channelMentionMap
}
// tryExecuteBuiltInCommand attempts to run a built in command based on the given arguments. If no such command can be
// found, returns nil for all arguments.
func (a *App) tryExecuteBuiltInCommand(c request.CTX, args *model.CommandArgs, trigger string, message string) (*model.Command, *model.CommandResponse) {
provider := GetCommandProvider(trigger)
if provider == nil {
return nil, nil
}
cmd := provider.GetCommand(a, args.T)
if cmd == nil {
return nil, nil
}
return cmd, provider.DoCommand(a, c, args, message)
}
// tryExecuteCustomCommand attempts to run a custom command based on the given arguments. If no such command can be
// found, returns nil for all arguments.
func (a *App) tryExecuteCustomCommand(c request.CTX, args *model.CommandArgs, trigger string, message string) (*model.Command, *model.CommandResponse, *model.AppError) {
// Handle custom commands
if !*a.Config().ServiceSettings.EnableCommands {
return nil, nil, model.NewAppError("ExecuteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
chanChan := make(chan store.StoreResult, 1)
go func() {
channel, err := a.Srv().Store().Channel().Get(args.ChannelId, true)
chanChan <- store.StoreResult{Data: channel, NErr: err}
close(chanChan)
}()
teamChan := make(chan store.StoreResult, 1)
go func() {
team, err := a.Srv().Store().Team().Get(args.TeamId)
teamChan <- store.StoreResult{Data: team, NErr: err}
close(teamChan)
}()
userChan := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), args.UserId)
userChan <- store.StoreResult{Data: user, NErr: err}
close(userChan)
}()
teamCmds, err := a.Srv().Store().Command().GetByTeam(args.TeamId)
if err != nil {
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.command.tryexecutecustomcommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
tr := <-teamChan
if tr.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(tr.NErr, &nfErr):
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(tr.NErr)
default:
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(tr.NErr)
}
}
team := tr.Data.(*model.Team)
ur := <-userChan
if ur.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(ur.NErr, &nfErr):
return nil, nil, model.NewAppError("tryExecuteCustomCommand", MissingAccountError, nil, "", http.StatusNotFound).Wrap(ur.NErr)
default:
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(ur.NErr)
}
}
user := ur.Data.(*model.User)
cr := <-chanChan
if cr.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(cr.NErr, &nfErr):
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(cr.NErr)
default:
return nil, nil, model.NewAppError("tryExecuteCustomCommand", "app.channel.get.find.app_error", nil, "", http.StatusInternalServerError).Wrap(cr.NErr)
}
}
channel := cr.Data.(*model.Channel)
var cmd *model.Command
for _, teamCmd := range teamCmds {
if trigger == teamCmd.Trigger {
cmd = teamCmd
}
}
if cmd == nil {
return nil, nil, nil
}
c.Logger().Debug("Executing command", mlog.String("command", trigger), mlog.String("user_id", args.UserId))
p := url.Values{}
p.Set("token", cmd.Token)
p.Set("team_id", cmd.TeamId)
p.Set("team_domain", team.Name)
p.Set("channel_id", args.ChannelId)
p.Set("channel_name", channel.Name)
p.Set("user_id", args.UserId)
p.Set("user_name", user.Username)
p.Set("command", "/"+trigger)
p.Set("text", message)
p.Set("trigger_id", args.TriggerId)
userMentionMap := a.MentionsToTeamMembers(c, message, team.Id)
for key, values := range userMentionMap.ToURLValues() {
p[key] = values
}
channelMentionMap := a.MentionsToPublicChannels(c, message, team.Id)
for key, values := range channelMentionMap.ToURLValues() {
p[key] = values
}
hook, appErr := a.CreateCommandWebhook(cmd.Id, args)
if appErr != nil {
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]any{"Trigger": trigger}, "", http.StatusInternalServerError).Wrap(appErr)
}
p.Set("response_url", args.SiteURL+"/hooks/commands/"+hook.Id)
return a.DoCommandRequest(cmd, p)
}
func (a *App) DoCommandRequest(cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError) {
// Prepare the request
var req *http.Request
var err error
if cmd.Method == model.CommandMethodGet {
req, err = http.NewRequest(http.MethodGet, cmd.URL, nil)
} else {
req, err = http.NewRequest(http.MethodPost, cmd.URL, strings.NewReader(p.Encode()))
}
if err != nil {
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]any{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError).Wrap(err)
}
if cmd.Method == model.CommandMethodGet {
if req.URL.RawQuery != "" {
req.URL.RawQuery += "&"
}
req.URL.RawQuery += p.Encode()
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Token "+cmd.Token)
if cmd.Method == model.CommandMethodPost {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
// Send the request
resp, err := a.HTTPService().MakeClient(false).Do(req)
if err != nil {
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]any{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError).Wrap(err)
}
defer resp.Body.Close()
// Handle the response
body := io.LimitReader(resp.Body, MaxIntegrationResponseSize)
if resp.StatusCode != http.StatusOK {
// Ignore the error below because the resulting string will just be the empty string if bodyBytes is nil
bodyBytes, _ := io.ReadAll(body)
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed_resp.app_error", map[string]any{"Trigger": cmd.Trigger, "Status": resp.Status}, string(bodyBytes), http.StatusInternalServerError)
}
response, err := model.CommandResponseFromHTTPBody(resp.Header.Get("Content-Type"), body)
if err != nil {
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]any{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError).Wrap(err)
} else if response == nil {
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed_empty.app_error", map[string]any{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError)
}
return cmd, response, nil
}
func (a *App) HandleCommandResponse(c request.CTX, command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError) {
trigger := ""
if args.Command != "" {
parts := strings.Split(args.Command, " ")
trigger = parts[0][1:]
trigger = strings.ToLower(trigger)
}
var lastError *model.AppError
_, err := a.HandleCommandResponsePost(c, command, args, response, builtIn)
if err != nil {
mlog.Debug("Error occurred in handling command response post", mlog.Err(err))
lastError = err
}
if response.ExtraResponses != nil {
for _, resp := range response.ExtraResponses {
_, err := a.HandleCommandResponsePost(c, command, args, resp, builtIn)
if err != nil {
mlog.Debug("Error occurred in handling command response post", mlog.Err(err))
lastError = err
}
}
}
if lastError != nil {
return response, model.NewAppError("command", "api.command.execute_command.create_post_failed.app_error", map[string]any{"Trigger": trigger}, "", http.StatusInternalServerError)
}
return response, nil
}
func (a *App) HandleCommandResponsePost(c request.CTX, command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.Post, *model.AppError) {
post := &model.Post{}
post.ChannelId = args.ChannelId
post.RootId = args.RootId
post.UserId = args.UserId
post.Type = response.Type
post.SetProps(response.Props)
if response.ChannelId != "" {
_, err := a.GetChannelMember(c, response.ChannelId, args.UserId)
if err != nil {
err = model.NewAppError("HandleCommandResponsePost", "api.command.command_post.forbidden.app_error", nil, "", http.StatusForbidden).Wrap(err)
return nil, err
}
post.ChannelId = response.ChannelId
}
isBotPost := !builtIn
if *a.Config().ServiceSettings.EnablePostUsernameOverride {
if command.Username != "" {
post.AddProp("override_username", command.Username)
isBotPost = true
} else if response.Username != "" {
post.AddProp("override_username", response.Username)
isBotPost = true
}
}
if *a.Config().ServiceSettings.EnablePostIconOverride {
if command.IconURL != "" {
post.AddProp("override_icon_url", command.IconURL)
isBotPost = true
} else if response.IconURL != "" {
post.AddProp("override_icon_url", response.IconURL)
isBotPost = true
} else {
post.AddProp("override_icon_url", "")
}
}
if isBotPost {
post.AddProp("from_webhook", "true")
}
// Process Slack text replacements if the response does not contain "skip_slack_parsing": true.
if !response.SkipSlackParsing {
response.Text = a.ProcessSlackText(response.Text)
response.Attachments = a.ProcessSlackAttachments(response.Attachments)
}
if _, err := a.CreateCommandPost(c, post, args.TeamId, response, response.SkipSlackParsing); err != nil {
return post, err
}
return post, nil
}
func (a *App) CreateCommand(cmd *model.Command) (*model.Command, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCommands {
return nil, model.NewAppError("CreateCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
return a.createCommand(cmd)
}
func (a *App) createCommand(cmd *model.Command) (*model.Command, *model.AppError) {
cmd.Trigger = strings.ToLower(cmd.Trigger)
teamCmds, err := a.Srv().Store().Command().GetByTeam(cmd.TeamId)
if err != nil {
return nil, model.NewAppError("CreateCommand", "app.command.createcommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, existingCommand := range teamCmds {
if cmd.Trigger == existingCommand.Trigger {
return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest)
}
}
for _, builtInProvider := range commandProviders {
builtInCommand := builtInProvider.GetCommand(a, i18n.T)
if builtInCommand != nil && cmd.Trigger == builtInCommand.Trigger {
return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest)
}
}
command, nErr := a.Srv().Store().Command().Save(cmd)
if nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CreateCommand", "app.command.createcommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return command, nil
}
func (a *App) GetCommand(commandID string) (*model.Command, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCommands {
return nil, model.NewAppError("GetCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
command, err := a.Srv().Store().Command().Get(commandID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("SqlCommandStore.Get", "store.sql_command.get.missing.app_error", map[string]any{"command_id": commandID}, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetCommand", "app.command.getcommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return command, nil
}
func (a *App) UpdateCommand(oldCmd, updatedCmd *model.Command) (*model.Command, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCommands {
return nil, model.NewAppError("UpdateCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
updatedCmd.Trigger = strings.ToLower(updatedCmd.Trigger)
updatedCmd.Id = oldCmd.Id
updatedCmd.Token = oldCmd.Token
updatedCmd.CreateAt = oldCmd.CreateAt
updatedCmd.UpdateAt = model.GetMillis()
updatedCmd.DeleteAt = oldCmd.DeleteAt
updatedCmd.CreatorId = oldCmd.CreatorId
updatedCmd.PluginId = oldCmd.PluginId
updatedCmd.TeamId = oldCmd.TeamId
command, err := a.Srv().Store().Command().Update(updatedCmd)
if err != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("SqlCommandStore.Update", "store.sql_command.update.missing.app_error", map[string]any{"command_id": updatedCmd.Id}, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("UpdateCommand", "app.command.updatecommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return command, nil
}
func (a *App) MoveCommand(team *model.Team, command *model.Command) *model.AppError {
command.TeamId = team.Id
_, err := a.Srv().Store().Command().Update(command)
if err != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
switch {
case errors.As(err, &nfErr):
return model.NewAppError("SqlCommandStore.Update", "store.sql_command.update.missing.app_error", map[string]any{"command_id": command.Id}, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &appErr):
return appErr
default:
return model.NewAppError("MoveCommand", "app.command.movecommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return nil
}
func (a *App) RegenCommandToken(cmd *model.Command) (*model.Command, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCommands {
return nil, model.NewAppError("RegenCommandToken", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
cmd.Token = model.NewId()
command, err := a.Srv().Store().Command().Update(cmd)
if err != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("SqlCommandStore.Update", "store.sql_command.update.missing.app_error", map[string]any{"command_id": cmd.Id}, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("RegenCommandToken", "app.command.regencommandtoken.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return command, nil
}
func (a *App) DeleteCommand(commandID string) *model.AppError {
if !*a.Config().ServiceSettings.EnableCommands {
return model.NewAppError("DeleteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
err := a.Srv().Store().Command().Delete(commandID, model.GetMillis())
if err != nil {
return model.NewAppError("DeleteCommand", "app.command.deletecommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
// possibleAtMentions returns all substrings in message that look like valid @
// mentions.
func possibleAtMentions(message string) []string {
var names []string
if !strings.Contains(message, "@") {
return names
}
alreadyMentioned := make(map[string]bool)
for _, match := range atMentionRegexp.FindAllString(message, -1) {
name := model.NormalizeUsername(match[1:])
if !alreadyMentioned[name] && model.IsValidUsernameAllowRemote(name) {
names = append(names, name)
alreadyMentioned[name] = true
}
}
return names
}
// trimUsernameSpecialChar tries to remove the last character from word if it
// is a special character for usernames (dot, dash or underscore). If not, it
// returns the same string.
func trimUsernameSpecialChar(word string) (string, bool) {
len := len(word)
if len > 0 && strings.LastIndexAny(word, usernameSpecialChars) == (len-1) {
return word[:len-1], true
}
return word, false
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"sort"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// AutocompleteDynamicArgProvider dynamically provides auto-completion args for built-in commands.
type AutocompleteDynamicArgProvider interface {
GetAutoCompleteListItems(a *App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error)
}
// GetSuggestions returns suggestions for user input.
func (a *App) GetSuggestions(c *request.Context, commandArgs *model.CommandArgs, commands []*model.Command, roleID string) []model.AutocompleteSuggestion {
sort.Slice(commands, func(i, j int) bool {
return strings.Compare(strings.ToLower(commands[i].Trigger), strings.ToLower(commands[j].Trigger)) < 0
})
autocompleteData := []*model.AutocompleteData{}
for _, command := range commands {
if command.AutocompleteData == nil {
command.AutocompleteData = model.NewAutocompleteData(command.Trigger, command.AutoCompleteHint, command.AutoCompleteDesc)
}
autocompleteData = append(autocompleteData, command.AutocompleteData)
}
userInput := commandArgs.Command
suggestions := a.getSuggestions(c, commandArgs, autocompleteData, "", userInput, roleID)
for i, suggestion := range suggestions {
for _, command := range commands {
if strings.HasPrefix(suggestion.Complete, command.Trigger) {
suggestions[i].IconData = command.AutocompleteIconData
break
}
}
}
return suggestions
}
func (a *App) getSuggestions(c *request.Context, commandArgs *model.CommandArgs, commands []*model.AutocompleteData, inputParsed, inputToBeParsed, roleID string) []model.AutocompleteSuggestion {
suggestions := []model.AutocompleteSuggestion{}
index := strings.Index(inputToBeParsed, " ")
if index == -1 { // no space in input
for _, command := range commands {
if strings.HasPrefix(command.Trigger, strings.ToLower(inputToBeParsed)) && (command.RoleID == roleID || roleID == model.SystemAdminRoleId || roleID == "") {
s := model.AutocompleteSuggestion{
Complete: inputParsed + command.Trigger,
Suggestion: command.Trigger,
Description: command.HelpText,
Hint: command.Hint,
}
suggestions = append(suggestions, s)
}
}
return suggestions
}
for _, command := range commands {
if command.Trigger != strings.ToLower(inputToBeParsed[:index]) {
continue
}
if roleID != "" && roleID != model.SystemAdminRoleId && roleID != command.RoleID {
continue
}
toBeParsed := inputToBeParsed[index+1:]
parsed := inputParsed + inputToBeParsed[:index+1]
if len(command.Arguments) == 0 {
// Seek recursively in subcommands
subSuggestions := a.getSuggestions(c, commandArgs, command.SubCommands, parsed, toBeParsed, roleID)
suggestions = append(suggestions, subSuggestions...)
continue
}
found, _, _, suggestion := a.parseArguments(c, commandArgs, command.Arguments, parsed, toBeParsed)
if found {
suggestions = append(suggestions, suggestion...)
}
}
return suggestions
}
func (a *App) parseArguments(c *request.Context, commandArgs *model.CommandArgs, args []*model.AutocompleteArg, parsed, toBeParsed string) (found bool, alreadyParsed string, yetToBeParsed string, suggestions []model.AutocompleteSuggestion) {
if len(args) == 0 {
return false, parsed, toBeParsed, suggestions
}
if args[0].Required {
found, changedParsed, changedToBeParsed, suggestion := a.parseArgument(c, commandArgs, args[0], parsed, toBeParsed)
if found {
suggestions = append(suggestions, suggestion...)
return true, changedParsed, changedToBeParsed, suggestions
}
return a.parseArguments(c, commandArgs, args[1:], changedParsed, changedToBeParsed)
}
// Handling optional arguments. Optional argument can be inputted or not,
// so we have to pase both cases recursively and output combined suggestions.
foundWithOptional, changedParsedWithOptional, changedToBeParsedWithOptional, suggestionsWithOptional := a.parseArgument(c, commandArgs, args[0], parsed, toBeParsed)
if foundWithOptional {
suggestions = append(suggestions, suggestionsWithOptional...)
} else {
foundWithOptionalRest, changedParsedWithOptionalRest, changedToBeParsedWithOptionalRest, suggestionsWithOptionalRest := a.parseArguments(c, commandArgs, args[1:], changedParsedWithOptional, changedToBeParsedWithOptional)
if foundWithOptionalRest {
suggestions = append(suggestions, suggestionsWithOptionalRest...)
}
foundWithOptional = foundWithOptionalRest
changedParsedWithOptional = changedParsedWithOptionalRest
changedToBeParsedWithOptional = changedToBeParsedWithOptionalRest
}
foundWithoutOptional, changedParsedWithoutOptional, changedToBeParsedWithoutOptional, suggestionsWithoutOptional := a.parseArguments(c, commandArgs, args[1:], parsed, toBeParsed)
if foundWithoutOptional {
suggestions = append(suggestions, suggestionsWithoutOptional...)
}
// if suggestions were found we can return them
if foundWithOptional || foundWithoutOptional {
return true, parsed + toBeParsed, "", suggestions
}
// no suggestions found yet, check if optional argument was inputted
if changedParsedWithOptional != parsed && changedToBeParsedWithOptional != toBeParsed {
return false, changedParsedWithOptional, changedToBeParsedWithOptional, suggestions
}
// no suggestions and optional argument was not inputted
return foundWithoutOptional, changedParsedWithoutOptional, changedToBeParsedWithoutOptional, suggestions
}
func (a *App) parseArgument(c *request.Context, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) (found bool, alreadyParsed string, yetToBeParsed string, suggestions []model.AutocompleteSuggestion) {
if arg.Name != "" { //Parse the --name first
found, changedParsed, changedToBeParsed, suggestion := parseNamedArgument(arg, parsed, toBeParsed)
if found {
suggestions = append(suggestions, suggestion)
return true, changedParsed, changedToBeParsed, suggestions
}
if changedToBeParsed == "" {
return true, changedParsed, changedToBeParsed, suggestions
}
if changedToBeParsed == " " {
changedToBeParsed = ""
}
parsed = changedParsed
toBeParsed = changedToBeParsed
}
if arg.Type == model.AutocompleteArgTypeText {
found, changedParsed, changedToBeParsed, suggestion := parseInputTextArgument(arg, parsed, toBeParsed)
if found {
suggestions = append(suggestions, suggestion)
return true, changedParsed, changedToBeParsed, suggestions
}
parsed = changedParsed
toBeParsed = changedToBeParsed
} else if arg.Type == model.AutocompleteArgTypeStaticList {
found, changedParsed, changedToBeParsed, staticListSuggestions := parseStaticListArgument(arg, parsed, toBeParsed)
if found {
suggestions = append(suggestions, staticListSuggestions...)
return true, changedParsed, changedToBeParsed, suggestions
}
parsed = changedParsed
toBeParsed = changedToBeParsed
} else if arg.Type == model.AutocompleteArgTypeDynamicList {
found, changedParsed, changedToBeParsed, dynamicListSuggestions := a.getDynamicListArgument(c, commandArgs, arg, parsed, toBeParsed)
if found {
suggestions = append(suggestions, dynamicListSuggestions...)
return true, changedParsed, changedToBeParsed, suggestions
}
parsed = changedParsed
toBeParsed = changedToBeParsed
}
return false, parsed, toBeParsed, suggestions
}
func parseNamedArgument(arg *model.AutocompleteArg, parsed, toBeParsed string) (found bool, alreadyParsed string, yetToBeParsed string, suggestion model.AutocompleteSuggestion) {
in := strings.TrimPrefix(toBeParsed, " ")
namedArg := "--" + arg.Name
if in == "" { //The user has not started typing the argument.
return true, parsed + toBeParsed, "", model.AutocompleteSuggestion{Complete: parsed + toBeParsed + namedArg + " ", Suggestion: namedArg, Hint: "", Description: arg.HelpText}
}
if strings.HasPrefix(strings.ToLower(namedArg), strings.ToLower(in)) {
return true, parsed + toBeParsed, "", model.AutocompleteSuggestion{Complete: parsed + toBeParsed + namedArg[len(in):] + " ", Suggestion: namedArg, Hint: "", Description: arg.HelpText}
}
if !strings.HasPrefix(strings.ToLower(in), strings.ToLower(namedArg)+" ") {
return false, parsed + toBeParsed, "", model.AutocompleteSuggestion{}
}
if strings.ToLower(in) == strings.ToLower(namedArg)+" " {
return false, parsed + namedArg + " ", " ", model.AutocompleteSuggestion{}
}
return false, parsed + namedArg + " ", in[len(namedArg)+1:], model.AutocompleteSuggestion{}
}
func parseInputTextArgument(arg *model.AutocompleteArg, parsed, toBeParsed string) (found bool, alreadyParsed string, yetToBeParsed string, suggestion model.AutocompleteSuggestion) {
in := strings.TrimPrefix(toBeParsed, " ")
a := arg.Data.(*model.AutocompleteTextArg)
if in == "" { //The user has not started typing the argument.
return true, parsed + toBeParsed, "", model.AutocompleteSuggestion{Complete: parsed + toBeParsed, Suggestion: "", Hint: a.Hint, Description: arg.HelpText}
}
if in[0] == '"' { //input with multiple words
indexOfSecondQuote := strings.Index(in[1:], `"`)
if indexOfSecondQuote == -1 { //typing of the multiple word argument is not finished
return true, parsed + toBeParsed, "", model.AutocompleteSuggestion{Complete: parsed + toBeParsed, Suggestion: "", Hint: a.Hint, Description: arg.HelpText}
}
// this argument is typed already
offset := 2
if len(in) > indexOfSecondQuote+2 && in[indexOfSecondQuote+2] == ' ' {
offset++
}
return false, parsed + in[:indexOfSecondQuote+offset], in[indexOfSecondQuote+offset:], model.AutocompleteSuggestion{}
}
// input with a single word
index := strings.Index(in, " ")
if index == -1 { // typing of the single word argument is not finished
return true, parsed + toBeParsed, "", model.AutocompleteSuggestion{Complete: parsed + toBeParsed, Suggestion: "", Hint: a.Hint, Description: arg.HelpText}
}
// single word argument already typed
return false, parsed + in[:index+1], in[index+1:], model.AutocompleteSuggestion{}
}
func parseStaticListArgument(arg *model.AutocompleteArg, parsed, toBeParsed string) (found bool, alreadyParsed string, yetToBeParsed string, suggestions []model.AutocompleteSuggestion) {
a := arg.Data.(*model.AutocompleteStaticListArg)
return parseListItems(a.PossibleArguments, parsed, toBeParsed)
}
func (a *App) getDynamicListArgument(c *request.Context, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) (found bool, alreadyParsed string, yetToBeParsed string, suggestions []model.AutocompleteSuggestion) {
dynamicArg := arg.Data.(*model.AutocompleteDynamicListArg)
if strings.HasPrefix(dynamicArg.FetchURL, "builtin:") {
listItems, err := a.getBuiltinDynamicListArgument(commandArgs, arg, parsed, toBeParsed)
if err != nil {
a.Log().Error("Can't fetch dynamic list arguments for", mlog.String("url", dynamicArg.FetchURL), mlog.Err(err))
return false, parsed, toBeParsed, []model.AutocompleteSuggestion{}
}
return parseListItems(listItems, parsed, toBeParsed)
}
params := url.Values{}
params.Add("user_input", parsed+toBeParsed)
params.Add("parsed", parsed)
// Encode the information normally provided to a plugin slash command handler into the request parameters
// Encode PluginContext:
pluginContext := pluginContext(c)
params.Add("request_id", pluginContext.RequestId)
params.Add("session_id", pluginContext.SessionId)
params.Add("ip_address", pluginContext.IPAddress)
params.Add("accept_language", pluginContext.AcceptLanguage)
params.Add("user_agent", pluginContext.UserAgent)
// Encode CommandArgs:
params.Add("channel_id", commandArgs.ChannelId)
params.Add("team_id", commandArgs.TeamId)
params.Add("root_id", commandArgs.RootId)
params.Add("user_id", commandArgs.UserId)
params.Add("site_url", commandArgs.SiteURL)
resp, err := a.doPluginRequest(c, "GET", dynamicArg.FetchURL, params, nil)
if err != nil {
a.Log().Error("Can't fetch dynamic list arguments for", mlog.String("url", dynamicArg.FetchURL), mlog.Err(err))
return false, parsed, toBeParsed, []model.AutocompleteSuggestion{}
}
var listItems []model.AutocompleteListItem
if jsonErr := json.NewDecoder(resp.Body).Decode(&listItems); jsonErr != nil {
c.Logger().Warn("Failed to decode from JSON", mlog.Err(jsonErr))
}
return parseListItems(listItems, parsed, toBeParsed)
}
func parseListItems(items []model.AutocompleteListItem, parsed, toBeParsed string) (bool, string, string, []model.AutocompleteSuggestion) {
in := strings.TrimPrefix(toBeParsed, " ")
suggestions := []model.AutocompleteSuggestion{}
maxPrefix := ""
for _, arg := range items {
if strings.HasPrefix(strings.ToLower(in), strings.ToLower(arg.Item)+" ") && len(maxPrefix) < len(arg.Item)+1 {
maxPrefix = arg.Item + " "
}
}
if maxPrefix != "" { //typing of an argument finished
return false, parsed + in[:len(maxPrefix)], in[len(maxPrefix):], []model.AutocompleteSuggestion{}
}
// user has not finished typing static argument
for _, arg := range items {
if strings.HasPrefix(strings.ToLower(arg.Item), strings.ToLower(in)) {
suggestions = append(suggestions, model.AutocompleteSuggestion{Complete: parsed + arg.Item, Suggestion: arg.Item, Hint: arg.Hint, Description: arg.HelpText})
}
}
return true, parsed + toBeParsed, "", suggestions
}
func (a *App) getBuiltinDynamicListArgument(commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error) {
dynamicArg := arg.Data.(*model.AutocompleteDynamicListArg)
arr := strings.Split(dynamicArg.FetchURL, ":")
if len(arr) < 2 {
return nil, errors.New("dynamic list URL missing built-in command name")
}
cmdName := arr[1]
provider := GetCommandProvider(cmdName)
if provider == nil {
return nil, fmt.Errorf("no command provider for %s", cmdName)
}
dp, ok := provider.(AutocompleteDynamicArgProvider)
if !ok {
return nil, fmt.Errorf("auto-completion not available for built-in command %s", cmdName)
}
return dp.GetAutoCompleteListItems(a, commandArgs, arg, parsed, toBeParsed)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"net/http"
"os"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) GetComplianceReports(page, perPage int) (model.Compliances, *model.AppError) {
if license := a.Srv().License(); !*a.Config().ComplianceSettings.Enable || license == nil || !*license.Features.Compliance {
return nil, model.NewAppError("GetComplianceReports", "ent.compliance.licence_disable.app_error", nil, "", http.StatusNotImplemented)
}
compliances, err := a.Srv().Store().Compliance().GetAll(page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetComplianceReports", "app.compliance.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return compliances, nil
}
func (a *App) SaveComplianceReport(job *model.Compliance) (*model.Compliance, *model.AppError) {
if license := a.Srv().License(); !*a.Config().ComplianceSettings.Enable || license == nil || !*license.Features.Compliance || a.Compliance() == nil {
return nil, model.NewAppError("saveComplianceReport", "ent.compliance.licence_disable.app_error", nil, "", http.StatusNotImplemented)
}
job.Type = model.ComplianceTypeAdhoc
job, err := a.Srv().Store().Compliance().Save(job)
if err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("SaveComplianceReport", "app.compliance.save.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
jCopy := job.DeepCopy()
a.Srv().Go(func() {
err := a.Compliance().RunComplianceJob(jCopy)
if err != nil {
mlog.Warn("Error running compliance job", mlog.Err(err))
}
})
return job, nil
}
func (a *App) GetComplianceReport(reportId string) (*model.Compliance, *model.AppError) {
if license := a.Srv().License(); !*a.Config().ComplianceSettings.Enable || license == nil || !*license.Features.Compliance || a.Compliance() == nil {
return nil, model.NewAppError("downloadComplianceReport", "ent.compliance.licence_disable.app_error", nil, "", http.StatusNotImplemented)
}
compliance, err := a.Srv().Store().Compliance().Get(reportId)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetComplianceReport", "app.compliance.get.finding.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetComplianceReport", "app.compliance.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return compliance, nil
}
func (a *App) GetComplianceFile(job *model.Compliance) ([]byte, *model.AppError) {
f, err := os.ReadFile(*a.Config().ComplianceSettings.Directory + "compliance/" + job.JobName() + ".zip")
if err != nil {
return nil, model.NewAppError("readFile", "api.file.read_file.reading_local.app_error", nil, "", http.StatusNotImplemented).Wrap(err)
}
return f, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"crypto/ecdsa"
"crypto/rand"
"encoding/json"
"net/url"
"reflect"
"strconv"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mail"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
ErrorTermsOfServiceNoRowsFound = "app.terms_of_service.get.no_rows.app_error"
)
func (s *Server) Config() *model.Config {
return s.platform.Config()
}
func (a *App) Config() *model.Config {
return a.ch.cfgSvc.Config()
}
func (a *App) EnvironmentConfig(filter func(reflect.StructField) bool) map[string]any {
return a.Srv().platform.GetEnvironmentOverridesWithFilter(filter)
}
func (a *App) UpdateConfig(f func(*model.Config)) {
a.Srv().platform.UpdateConfig(f)
}
func (a *App) ReloadConfig() error {
return a.Srv().platform.ReloadConfig()
}
func (a *App) ClientConfig() map[string]string {
return a.ch.srv.platform.ClientConfig()
}
func (a *App) ClientConfigHash() string {
return a.ch.ClientConfigHash()
}
func (a *App) LimitedClientConfig() map[string]string {
return a.ch.srv.platform.LimitedClientConfig()
}
func (a *App) AddConfigListener(listener func(*model.Config, *model.Config)) string {
return a.Srv().platform.AddConfigListener(listener)
}
// Removes a listener function by the unique ID returned when AddConfigListener was called
func (a *App) RemoveConfigListener(id string) {
a.Srv().platform.RemoveConfigListener(id)
}
// ensurePostActionCookieSecret ensures that the key for encrypting PostActionCookie exists
// and future calls to PostActionCookieSecret will always return a valid key, same on all
// servers in the cluster
func (ch *Channels) ensurePostActionCookieSecret() error {
if ch.postActionCookieSecret != nil {
return nil
}
var secret *model.SystemPostActionCookieSecret
value, err := ch.srv.Store().System().GetByName(model.SystemPostActionCookieSecretKey)
if err == nil {
if err := json.Unmarshal([]byte(value.Value), &secret); err != nil {
return err
}
}
// If we don't already have a key, try to generate one.
if secret == nil {
newSecret := &model.SystemPostActionCookieSecret{
Secret: make([]byte, 32),
}
_, err := rand.Reader.Read(newSecret.Secret)
if err != nil {
return err
}
system := &model.System{
Name: model.SystemPostActionCookieSecretKey,
}
v, err := json.Marshal(newSecret)
if err != nil {
return err
}
system.Value = string(v)
// If we were able to save the key, use it, otherwise log the error.
if err = ch.srv.Store().System().Save(system); err != nil {
mlog.Warn("Failed to save PostActionCookieSecret", mlog.Err(err))
} else {
secret = newSecret
}
}
// If we weren't able to save a new key above, another server must have beat us to it. Get the
// key from the database, and if that fails, error out.
if secret == nil {
value, err := ch.srv.Store().System().GetByName(model.SystemPostActionCookieSecretKey)
if err != nil {
return err
}
if err := json.Unmarshal([]byte(value.Value), &secret); err != nil {
return err
}
}
ch.postActionCookieSecret = secret.Secret
return nil
}
func (s *Server) ensureInstallationDate() error {
_, appErr := s.platform.GetSystemInstallDate()
if appErr == nil {
return nil
}
installDate, nErr := s.Store().User().InferSystemInstallDate()
var installationDate int64
if nErr == nil && installDate > 0 {
installationDate = installDate
} else {
installationDate = utils.MillisFromTime(time.Now())
}
if err := s.Store().System().SaveOrUpdate(&model.System{
Name: model.SystemInstallationDateKey,
Value: strconv.FormatInt(installationDate, 10),
}); err != nil {
return err
}
return nil
}
func (s *Server) ensureFirstServerRunTimestamp() error {
_, appErr := s.getFirstServerRunTimestamp()
if appErr == nil {
return nil
}
if err := s.Store().System().SaveOrUpdate(&model.System{
Name: model.SystemFirstServerRunTimestampKey,
Value: strconv.FormatInt(utils.MillisFromTime(time.Now()), 10),
}); err != nil {
return err
}
return nil
}
// AsymmetricSigningKey will return a private key that can be used for asymmetric signing.
func (ch *Channels) AsymmetricSigningKey() *ecdsa.PrivateKey {
return ch.srv.platform.AsymmetricSigningKey()
}
func (a *App) AsymmetricSigningKey() *ecdsa.PrivateKey {
return a.ch.AsymmetricSigningKey()
}
func (ch *Channels) PostActionCookieSecret() []byte {
return ch.postActionCookieSecret
}
func (a *App) PostActionCookieSecret() []byte {
return a.ch.PostActionCookieSecret()
}
func (a *App) GetCookieDomain() string {
if *a.Config().ServiceSettings.AllowCookiesForSubdomains {
if siteURL, err := url.Parse(*a.Config().ServiceSettings.SiteURL); err == nil {
return siteURL.Hostname()
}
}
return ""
}
func (a *App) GetSiteURL() string {
return *a.Config().ServiceSettings.SiteURL
}
// GetConfigFile proxies access to the given configuration file to the underlying config store.
func (a *App) GetConfigFile(name string) ([]byte, error) {
data, err := a.Srv().platform.GetConfigFile(name)
if err != nil {
return nil, errors.Wrapf(err, "failed to get config file %s", name)
}
return data, nil
}
// GetSanitizedConfig gets the configuration for a system admin without any secrets.
func (a *App) GetSanitizedConfig() *model.Config {
cfg := a.Config().Clone()
cfg.Sanitize()
return cfg
}
// GetEnvironmentConfig returns a map of configuration keys whose values have been overridden by an environment variable.
// If filter is not nil and returns false for a struct field, that field will be omitted.
func (a *App) GetEnvironmentConfig(filter func(reflect.StructField) bool) map[string]any {
return a.EnvironmentConfig(filter)
}
// SaveConfig replaces the active configuration, optionally notifying cluster peers.
func (a *App) SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bool) (*model.Config, *model.Config, *model.AppError) {
return a.Srv().platform.SaveConfig(newCfg, sendConfigChangeClusterMessage)
}
func (a *App) HandleMessageExportConfig(cfg *model.Config, appCfg *model.Config) {
// If the Message Export feature has been toggled in the System Console, rewrite the ExportFromTimestamp field to an
// appropriate value. The rewriting occurs here to ensure it doesn't affect values written to the config file
// directly and not through the System Console UI.
if *cfg.MessageExportSettings.EnableExport != *appCfg.MessageExportSettings.EnableExport {
if *cfg.MessageExportSettings.EnableExport && *cfg.MessageExportSettings.ExportFromTimestamp == int64(0) {
// When the feature is toggled on, use the current timestamp as the start time for future exports.
cfg.MessageExportSettings.ExportFromTimestamp = model.NewInt64(model.GetMillis())
} else if !*cfg.MessageExportSettings.EnableExport {
// When the feature is disabled, reset the timestamp so that the timestamp will be set if
// the feature is re-enabled from the System Console in future.
cfg.MessageExportSettings.ExportFromTimestamp = model.NewInt64(0)
}
}
}
func (s *Server) MailServiceConfig() *mail.SMTPConfig {
emailSettings := s.platform.Config().EmailSettings
hostname := utils.GetHostnameFromSiteURL(*s.platform.Config().ServiceSettings.SiteURL)
cfg := mail.SMTPConfig{
Hostname: hostname,
ConnectionSecurity: *emailSettings.ConnectionSecurity,
SkipServerCertificateVerification: *emailSettings.SkipServerCertificateVerification,
ServerName: *emailSettings.SMTPServer,
Server: *emailSettings.SMTPServer,
Port: *emailSettings.SMTPPort,
ServerTimeout: *emailSettings.SMTPServerTimeout,
Username: *emailSettings.SMTPUsername,
Password: *emailSettings.SMTPPassword,
EnableSMTPAuth: *emailSettings.EnableSMTPAuth,
SendEmailNotifications: *emailSettings.SendEmailNotifications,
FeedbackName: *emailSettings.FeedbackName,
FeedbackEmail: *emailSettings.FeedbackEmail,
ReplyToAddress: *emailSettings.ReplyToAddress,
}
return &cfg
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
)
// WithMaster adds the context value that master DB should be selected for this request.
func WithMaster(ctx context.Context) context.Context {
return sqlstore.WithMaster(ctx)
}
func pluginContext(c request.CTX) *plugin.Context {
context := &plugin.Context{
RequestId: c.RequestId(),
SessionId: c.Session().Id,
IPAddress: c.IPAddress(),
AcceptLanguage: c.AcceptLanguage(),
UserAgent: c.UserAgent(),
}
return context
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
)
func (a *App) GetGlobalRetentionPolicy() (*model.GlobalRetentionPolicy, *model.AppError) {
if a.DataRetention() == nil {
return nil, newLicenseError("GetGlobalRetentionPolicy")
}
return a.DataRetention().GetGlobalPolicy()
}
func (a *App) GetRetentionPolicies(offset, limit int) (*model.RetentionPolicyWithTeamAndChannelCountsList, *model.AppError) {
if a.DataRetention() == nil {
return nil, newLicenseError("GetRetentionPolicies")
}
return a.DataRetention().GetPolicies(offset, limit)
}
func (a *App) GetRetentionPoliciesCount() (int64, *model.AppError) {
if a.DataRetention() == nil {
return 0, newLicenseError("GetRetentionPoliciesCount")
}
return a.DataRetention().GetPoliciesCount()
}
func (a *App) GetRetentionPolicy(policyID string) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError) {
if a.DataRetention() == nil {
return nil, newLicenseError("GetRetentionPolicy")
}
return a.DataRetention().GetPolicy(policyID)
}
func (a *App) CreateRetentionPolicy(policy *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError) {
if a.DataRetention() == nil {
return nil, newLicenseError("CreateRetentionPolicy")
}
return a.DataRetention().CreatePolicy(policy)
}
func (a *App) PatchRetentionPolicy(patch *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError) {
if a.DataRetention() == nil {
return nil, newLicenseError("PatchRetentionPolicy")
}
return a.DataRetention().PatchPolicy(patch)
}
func (a *App) DeleteRetentionPolicy(policyID string) *model.AppError {
if a.DataRetention() == nil {
return newLicenseError("DeleteRetentionPolicy")
}
return a.DataRetention().DeletePolicy(policyID)
}
func (a *App) GetTeamsForRetentionPolicy(policyID string, offset, limit int) (*model.TeamsWithCount, *model.AppError) {
if a.DataRetention() == nil {
return nil, newLicenseError("GetTeamsForRetentionPolicy")
}
return a.DataRetention().GetTeamsForPolicy(policyID, offset, limit)
}
func (a *App) AddTeamsToRetentionPolicy(policyID string, teamIDs []string) *model.AppError {
if a.DataRetention() == nil {
return newLicenseError("AddTeamsToRetentionPolicy")
}
return a.DataRetention().AddTeamsToPolicy(policyID, teamIDs)
}
func (a *App) RemoveTeamsFromRetentionPolicy(policyID string, teamIDs []string) *model.AppError {
if a.DataRetention() == nil {
return newLicenseError("RemoveTeamsFromRetentionPolicy")
}
return a.DataRetention().RemoveTeamsFromPolicy(policyID, teamIDs)
}
func (a *App) GetChannelsForRetentionPolicy(policyID string, offset, limit int) (*model.ChannelsWithCount, *model.AppError) {
if a.DataRetention() == nil {
return nil, newLicenseError("GetChannelsForRetentionPolicy")
}
return a.DataRetention().GetChannelsForPolicy(policyID, offset, limit)
}
func (a *App) AddChannelsToRetentionPolicy(policyID string, channelIDs []string) *model.AppError {
if a.DataRetention() == nil {
return newLicenseError("AddChannelsToRetentionPolicies")
}
return a.DataRetention().AddChannelsToPolicy(policyID, channelIDs)
}
func (a *App) RemoveChannelsFromRetentionPolicy(policyID string, channelIDs []string) *model.AppError {
if a.DataRetention() == nil {
return newLicenseError("RemoveChannelsFromRetentionPolicy")
}
return a.DataRetention().RemoveChannelsFromPolicy(policyID, channelIDs)
}
func (a *App) GetTeamPoliciesForUser(userID string, offset, limit int) (*model.RetentionPolicyForTeamList, *model.AppError) {
if a.DataRetention() == nil {
return nil, newLicenseError("GetTeamPoliciesForUser")
}
return a.DataRetention().GetTeamPoliciesForUser(userID, offset, limit)
}
func (a *App) GetChannelPoliciesForUser(userID string, offset, limit int) (*model.RetentionPolicyForChannelList, *model.AppError) {
if a.DataRetention() == nil {
return nil, newLicenseError("GetChannelPoliciesForUser")
}
return a.DataRetention().GetChannelPoliciesForUser(userID, offset, limit)
}
func newLicenseError(methodName string) *model.AppError {
return model.NewAppError("App."+methodName, "ent.data_retention.generic.license.error",
nil, "", http.StatusNotImplemented)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"io"
"net/http"
"net/url"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
const (
// HTTPRequestTimeout defines a high timeout for downloading large files
// from an external URL to avoid slow connections from failing to install.
HTTPRequestTimeout = 1 * time.Hour
)
func (a *App) DownloadFromURL(downloadURL string) ([]byte, error) {
return a.Srv().downloadFromURL(downloadURL)
}
func (s *Server) downloadFromURL(downloadURL string) ([]byte, error) {
if !model.IsValidHTTPURL(downloadURL) {
return nil, errors.Errorf("invalid url %s", downloadURL)
}
u, err := url.ParseRequestURI(downloadURL)
if err != nil {
return nil, errors.Errorf("failed to parse url %s", downloadURL)
}
if !*s.platform.Config().PluginSettings.AllowInsecureDownloadURL && u.Scheme != "https" {
return nil, errors.Errorf("insecure url not allowed %s", downloadURL)
}
client := s.HTTPService().MakeClient(true)
client.Timeout = HTTPRequestTimeout
var resp *http.Response
err = utils.ProgressiveRetry(func() error {
resp, err = client.Get(downloadURL)
if err != nil {
return errors.Wrapf(err, "failed to fetch from %s", downloadURL)
}
if !(resp.StatusCode >= 200 && resp.StatusCode < 300) {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
return errors.Errorf("failed to fetch from %s", downloadURL)
}
return nil
})
if err != nil {
return nil, errors.Wrap(err, "download failed after multiple retries.")
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"encoding/json"
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) GetDraft(userID, channelID, rootID string) (*model.Draft, *model.AppError) {
if !a.Config().FeatureFlags.GlobalDrafts || !*a.Config().ServiceSettings.AllowSyncedDrafts {
return nil, model.NewAppError("GetDraft", "app.draft.feature_disabled", nil, "", http.StatusNotImplemented)
}
draft, err := a.Srv().Store().Draft().Get(userID, channelID, rootID, false)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetDraft", "app.draft.get.app_error", nil, err.Error(), http.StatusNotFound)
default:
return nil, model.NewAppError("GetDraft", "app.draft.get.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return draft, nil
}
func (a *App) UpsertDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError) {
if !a.Config().FeatureFlags.GlobalDrafts || !*a.Config().ServiceSettings.AllowSyncedDrafts {
return nil, model.NewAppError("UpsertDraft", "app.draft.feature_disabled", nil, "", http.StatusNotImplemented)
}
dt, dErr := a.Srv().Store().Draft().Get(draft.UserId, draft.ChannelId, draft.RootId, true)
var notFoundErr *store.ErrNotFound
if dErr != nil && !errors.As(dErr, ¬FoundErr) {
return nil, model.NewAppError("UpsertDraft", "app.select_error", nil, dErr.Error(), http.StatusInternalServerError)
}
var err *model.AppError
if dt == nil {
dt, err = a.CreateDraft(c, draft, connectionID)
if err != nil {
return nil, err
}
} else {
dt, err = a.UpdateDraft(c, draft, connectionID)
if err != nil {
return nil, err
}
}
return dt, nil
}
func (a *App) CreateDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError) {
if !a.Config().FeatureFlags.GlobalDrafts || !*a.Config().ServiceSettings.AllowSyncedDrafts {
return nil, model.NewAppError("CreateDraft", "app.draft.feature_disabled", nil, "", http.StatusNotImplemented)
}
// Check that channel exists and has not been deleted
channel, errCh := a.Srv().Store().Channel().Get(draft.ChannelId, true)
if errCh != nil {
err := model.NewAppError("CreateDraft", "api.context.invalid_param.app_error", map[string]interface{}{"Name": "draft.channel_id"}, errCh.Error(), http.StatusBadRequest)
return nil, err
}
if channel.DeleteAt != 0 {
err := model.NewAppError("CreateDraft", "api.draft.create_draft.can_not_draft_to_deleted.error", nil, "", http.StatusBadRequest)
return nil, err
}
_, nErr := a.Srv().Store().User().Get(context.Background(), draft.UserId)
if nErr != nil {
return nil, model.NewAppError("CreateDraft", "app.user.get.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
dt, nErr := a.Srv().Store().Draft().Save(draft)
if nErr != nil {
return nil, model.NewAppError("CreateDraft", "app.draft.save.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
dt = a.prepareDraftWithFileInfos(draft.UserId, dt)
message := model.NewWebSocketEvent(model.WebsocketEventDraftCreated, "", dt.ChannelId, dt.UserId, nil, connectionID)
draftJSON, jsonErr := json.Marshal(dt)
if jsonErr != nil {
mlog.Warn("Failed to encode draft to JSON", mlog.Err(jsonErr))
}
message.Add("draft", string(draftJSON))
a.Publish(message)
return dt, nil
}
func (a *App) UpdateDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError) {
if !a.Config().FeatureFlags.GlobalDrafts {
return nil, model.NewAppError("UpsertDraft", "app.draft.feature_disabled", nil, "", http.StatusNotImplemented)
}
// Check that channel exists and has not been deleted
channel, errCh := a.Srv().Store().Channel().Get(draft.ChannelId, true)
if errCh != nil {
err := model.NewAppError("UpdateDraft", "api.context.invalid_param.app_error", map[string]interface{}{"Name": "draft.channel_id"}, errCh.Error(), http.StatusBadRequest)
return nil, err
}
if channel.DeleteAt != 0 {
err := model.NewAppError("UpdateDraft", "api.draft.create_draft.can_not_draft_to_deleted.error", nil, "", http.StatusBadRequest)
return nil, err
}
_, nErr := a.Srv().Store().User().Get(context.Background(), draft.UserId)
if nErr != nil {
return nil, model.NewAppError("UpdateDraft", "app.user.get.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
dt, nErr := a.Srv().Store().Draft().Update(draft)
if nErr != nil {
return nil, model.NewAppError("UpdateDraft", "app.draft.update.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
dt = a.prepareDraftWithFileInfos(draft.UserId, dt)
message := model.NewWebSocketEvent(model.WebsocketEventDraftUpdated, "", draft.ChannelId, draft.UserId, nil, connectionID)
draftJSON, jsonErr := json.Marshal(dt)
if jsonErr != nil {
mlog.Warn("Failed to encode draft to JSON", mlog.Err(jsonErr))
}
message.Add("draft", string(draftJSON))
a.Publish(message)
return dt, nil
}
func (a *App) GetDraftsForUser(userID, teamID string) ([]*model.Draft, *model.AppError) {
if !a.Config().FeatureFlags.GlobalDrafts || !*a.Config().ServiceSettings.AllowSyncedDrafts {
return nil, model.NewAppError("GetDraftsForUser", "app.draft.feature_disabled", nil, "", http.StatusNotImplemented)
}
drafts, err := a.Srv().Store().Draft().GetDraftsForUser(userID, teamID)
if err != nil {
return nil, model.NewAppError("GetDraftsForUser", "app.draft.get_drafts.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, draft := range drafts {
a.prepareDraftWithFileInfos(userID, draft)
}
return drafts, nil
}
func (a *App) prepareDraftWithFileInfos(userID string, draft *model.Draft) *model.Draft {
if fileInfos, err := a.getFileInfosForDraft(draft); err != nil {
mlog.Error("Failed to get files for a user's drafts", mlog.String("user_id", userID), mlog.Err(err))
} else {
draft.Metadata = &model.PostMetadata{}
draft.Metadata.Files = fileInfos
}
return draft
}
func (a *App) getFileInfosForDraft(draft *model.Draft) ([]*model.FileInfo, *model.AppError) {
if len(draft.FileIds) == 0 {
return nil, nil
}
fileInfos, err := a.Srv().Store().FileInfo().GetByIds(draft.FileIds)
if err != nil {
return nil, model.NewAppError("GetFileInfosForDraft", "app.draft.get_for_draft.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.generateMiniPreviewForInfos(fileInfos)
return fileInfos, nil
}
func (a *App) DeleteDraft(userID, channelID, rootID, connectionID string) (*model.Draft, *model.AppError) {
if !a.Config().FeatureFlags.GlobalDrafts || !*a.Config().ServiceSettings.AllowSyncedDrafts {
return nil, model.NewAppError("DeleteDraft", "app.draft.feature_disabled", nil, "", http.StatusNotImplemented)
}
draft, nErr := a.Srv().Store().Draft().Get(userID, channelID, rootID, false)
if nErr != nil {
return nil, model.NewAppError("DeleteDraft", "app.draft.get.app_error", nil, nErr.Error(), http.StatusBadRequest)
}
if err := a.Srv().Store().Draft().Delete(userID, channelID, rootID); err != nil {
return nil, model.NewAppError("DeleteDraft", "app.draft.delete.app_error", nil, err.Error(), http.StatusInternalServerError)
}
draftJSON, jsonErr := json.Marshal(draft)
if jsonErr != nil {
mlog.Warn("Failed to encode draft to JSON")
}
message := model.NewWebSocketEvent(model.WebsocketEventDraftDeleted, "", draft.ChannelId, draft.UserId, nil, connectionID)
message.Add("draft", string(draftJSON))
a.Publish(message)
return draft, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package email
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"io"
"net/http"
"net/url"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mail"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/templates"
"github.com/microcosm-cc/bluemonday"
)
// Returns category if enabled is true (default false)
// If "" is returned when enabled is false, the category headers aren't attached to the email
func getSendGridCategory(category string, enabled bool) string {
if enabled {
return category
}
return ""
}
func (es *Service) SendChangeUsernameEmail(newUsername, email, locale, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.username_change_subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName,
"TeamDisplayName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.username_change_body.title")
data.Props["Info"] = T("api.templates.username_change_body.info",
map[string]any{"TeamDisplayName": es.config().TeamSettings.SiteName, "NewUsername": newUsername})
data.Props["Warning"] = T("api.templates.email_warning")
body, err := es.templatesContainer.RenderToString("email_change_body", data)
if err != nil {
return err
}
if err := es.sendMail(email, subject, body, "ChangeUsernameEmail"); err != nil {
return err
}
return nil
}
func (es *Service) SendEmailChangeVerifyEmail(newUserEmail, locale, siteURL, token string) error {
T := i18n.GetUserTranslations(locale)
link := fmt.Sprintf("%s/do_verify_email?token=%s&email=%s", siteURL, token, url.QueryEscape(newUserEmail))
subject := T("api.templates.email_change_verify_subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName,
"TeamDisplayName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.email_change_verify_body.title")
data.Props["Info"] = T("api.templates.email_change_verify_body.info",
map[string]any{"TeamDisplayName": es.config().TeamSettings.SiteName})
data.Props["VerifyUrl"] = link
data.Props["VerifyButton"] = T("api.templates.email_change_verify_body.button")
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["EmailInfo1"] = T("api.templates.email_us_anytime_at")
data.Props["SupportEmail"] = "feedback@mattermost.com"
data.Props["FooterV2"] = T("api.templates.email_footer_v2")
body, err := es.templatesContainer.RenderToString("email_change_verify_body", data)
if err != nil {
return err
}
if err := es.sendMail(newUserEmail, subject, body, "EmailChangeVerifyEmail"); err != nil {
return err
}
return nil
}
func (es *Service) SendEmailChangeEmail(oldEmail, newEmail, locale, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.email_change_subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName,
"TeamDisplayName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.email_change_body.title")
data.Props["Info"] = T("api.templates.email_change_body.info",
map[string]any{"TeamDisplayName": es.config().TeamSettings.SiteName, "NewEmail": newEmail})
data.Props["Warning"] = T("api.templates.email_warning")
body, err := es.templatesContainer.RenderToString("email_change_body", data)
if err != nil {
return err
}
if err := es.sendMail(oldEmail, subject, body, "EmailChangeEmail"); err != nil {
return err
}
return nil
}
func (es *Service) SendVerifyEmail(userEmail, locale, siteURL, token, redirect string) error {
T := i18n.GetUserTranslations(locale)
link := fmt.Sprintf("%s/do_verify_email?token=%s&email=%s", siteURL, token, url.QueryEscape(userEmail))
if redirect != "" {
link += fmt.Sprintf("&redirect_to=%s", redirect)
}
serverURL := condenseSiteURL(siteURL)
subject := T("api.templates.verify_subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.verify_body.title")
data.Props["SubTitle1"] = T("api.templates.verify_body.subTitle1")
data.Props["ServerURL"] = T("api.templates.verify_body.serverURL", map[string]any{"ServerURL": serverURL})
data.Props["SubTitle2"] = T("api.templates.verify_body.subTitle2")
data.Props["ButtonURL"] = link
data.Props["Button"] = T("api.templates.verify_body.button")
data.Props["Info"] = T("api.templates.verify_body.info")
data.Props["Info1"] = T("api.templates.verify_body.info1")
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
body, err := es.templatesContainer.RenderToString("verify_body", data)
if err != nil {
return err
}
if err := es.sendMail(userEmail, subject, body, "VerifyEmail"); err != nil {
return err
}
return nil
}
func (es *Service) SendSignInChangeEmail(email, method, locale, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.signin_change_email.subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.signin_change_email.body.title")
data.Props["Info"] = T("api.templates.signin_change_email.body.info",
map[string]any{"SiteName": es.config().TeamSettings.SiteName, "Method": method})
data.Props["Warning"] = T("api.templates.email_warning")
body, err := es.templatesContainer.RenderToString("signin_change_body", data)
if err != nil {
return err
}
if err := es.sendMail(email, subject, body, "SignInChangeEmail"); err != nil {
return err
}
return nil
}
func (es *Service) SendWelcomeEmail(userID string, email string, verified bool, disableWelcomeEmail bool, locale, siteURL, redirect string) error {
if disableWelcomeEmail {
return nil
}
if !*es.config().EmailSettings.SendEmailNotifications && !*es.config().EmailSettings.RequireEmailVerification {
return errors.New("send email notifications and require email verification is disabled in the system console")
}
T := i18n.GetUserTranslations(locale)
serverURL := condenseSiteURL(siteURL)
subject := T("api.templates.welcome_subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName,
"ServerURL": serverURL})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.welcome_body.title")
data.Props["SubTitle1"] = T("api.templates.welcome_body.subTitle1")
data.Props["ServerURL"] = T("api.templates.welcome_body.serverURL", map[string]any{"ServerURL": serverURL})
data.Props["SubTitle2"] = T("api.templates.welcome_body.subTitle2")
data.Props["Button"] = T("api.templates.welcome_body.button")
data.Props["Info"] = T("api.templates.welcome_body.info")
data.Props["Info1"] = T("api.templates.welcome_body.info1")
data.Props["SiteURL"] = siteURL
if *es.config().NativeAppSettings.AppDownloadLink != "" {
data.Props["AppDownloadTitle"] = T("api.templates.welcome_body.app_download_title")
data.Props["AppDownloadInfo"] = T("api.templates.welcome_body.app_download_info")
data.Props["AppDownloadButton"] = T("api.templates.welcome_body.app_download_button")
data.Props["AppDownloadLink"] = *es.config().NativeAppSettings.AppDownloadLink
}
if !verified && *es.config().EmailSettings.RequireEmailVerification {
token, err := es.CreateVerifyEmailToken(userID, email)
if err != nil {
return err
}
link := fmt.Sprintf("%s/do_verify_email?token=%s&email=%s", siteURL, token.Token, url.QueryEscape(email))
if redirect != "" {
link += fmt.Sprintf("&redirect_to=%s", redirect)
}
data.Props["ButtonURL"] = link
}
body, err := es.templatesContainer.RenderToString("welcome_body", data)
if err != nil {
return err
}
if err := es.sendMail(email, subject, body, "WelcomeEmail"); err != nil {
return err
}
return nil
}
func (es *Service) SendCloudUpgradeConfirmationEmail(userEmail, name, date, locale, siteURL, workspaceName string, isYearly bool, embeddedFiles map[string]io.Reader) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.cloud_upgrade_confirmation.subject")
data := es.NewEmailTemplateData(locale)
data.Props["Title"] = T("api.templates.cloud_upgrade_confirmation.title")
data.Props["SubTitle"] = T("api.templates.cloud_upgrade_confirmation_monthly.subtitle", map[string]any{"WorkspaceName": workspaceName, "Date": date})
data.Props["SiteURL"] = siteURL
data.Props["ButtonURL"] = siteURL
data.Props["Button"] = T("api.templates.cloud_welcome_email.button")
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
if isYearly {
data.Props["SubTitle"] = T("api.templates.cloud_upgrade_confirmation_yearly.subtitle", map[string]any{"WorkspaceName": workspaceName})
data.Props["ButtonURL"] = siteURL + "/admin_console/billing/billing_history"
data.Props["Button"] = T("api.templates.cloud_welcome_email.yearly_plan_button")
}
body, err := es.templatesContainer.RenderToString("cloud_upgrade_confirmation", data)
if err != nil {
return err
}
if isYearly {
if err := es.SendMailWithEmbeddedFilesAndCustomReplyTo(userEmail, subject, body, *es.config().SupportSettings.SupportEmail, embeddedFiles, "CloudUpgradeConfirmationEmail"); err != nil {
return err
}
} else {
if err := es.sendEmailWithCustomReplyTo(userEmail, subject, body, *es.config().SupportSettings.SupportEmail, "CloudUpgradeConfirmationEmail"); err != nil {
return err
}
}
return nil
}
// SendCloudWelcomeEmail sends the cloud version of the welcome email
func (es *Service) SendCloudWelcomeEmail(userEmail, locale, teamInviteID, workSpaceName, dns, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.cloud_welcome_email.subject")
data := es.NewEmailTemplateData(locale)
data.Props["Title"] = T("api.templates.cloud_welcome_email.title")
data.Props["SubTitle"] = T("api.templates.cloud_welcome_email.subtitle")
data.Props["SubTitleInfo"] = T("api.templates.cloud_welcome_email.subtitle_info")
data.Props["Info"] = T("api.templates.cloud_welcome_email.info")
data.Props["Info2"] = T("api.templates.cloud_welcome_email.info2")
data.Props["WorkSpacePath"] = siteURL
data.Props["DNS"] = dns
data.Props["InviteInfo"] = T("api.templates.cloud_welcome_email.invite_info")
data.Props["InviteSubInfo"] = T("api.templates.cloud_welcome_email.invite_sub_info", map[string]any{"WorkSpace": workSpaceName})
data.Props["InviteSubInfoLink"] = fmt.Sprintf("%s/signup_user_complete/?id=%s", siteURL, teamInviteID)
data.Props["AddAppsInfo"] = T("api.templates.cloud_welcome_email.add_apps_info")
data.Props["AddAppsSubInfo"] = T("api.templates.cloud_welcome_email.add_apps_sub_info")
data.Props["AppMarketPlace"] = T("api.templates.cloud_welcome_email.app_market_place")
data.Props["AppMarketPlaceLink"] = "https://integrations.mattermost.com/"
data.Props["DownloadMMInfo"] = T("api.templates.cloud_welcome_email.download_mm_info")
data.Props["SignInSubInfo"] = T("api.templates.cloud_welcome_email.signin_sub_info")
data.Props["MMApps"] = T("api.templates.cloud_welcome_email.mm_apps")
data.Props["SignInSubInfo2"] = T("api.templates.cloud_welcome_email.signin_sub_info2")
data.Props["DownloadMMAppsLink"] = "https://mattermost.com/download/"
data.Props["Button"] = T("api.templates.cloud_welcome_email.button")
data.Props["GettingStartedQuestions"] = T("api.templates.cloud_welcome_email.start_questions")
body, err := es.templatesContainer.RenderToString("cloud_welcome_email", data)
if err != nil {
return err
}
if err := es.sendEmailWithCustomReplyTo(userEmail, subject, body, *es.config().SupportSettings.SupportEmail, "CloudWelcomeEmail"); err != nil {
return err
}
return nil
}
func (es *Service) SendPasswordChangeEmail(email, method, locale, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.password_change_subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName,
"TeamDisplayName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.password_change_body.title")
data.Props["Info"] = T("api.templates.password_change_body.info",
map[string]any{"TeamDisplayName": es.config().TeamSettings.SiteName, "TeamURL": siteURL, "Method": method})
data.Props["Warning"] = T("api.templates.email_warning")
body, err := es.templatesContainer.RenderToString("password_change_body", data)
if err != nil {
return err
}
if err := es.sendMail(email, subject, body, "PasswordChangeEmail"); err != nil {
return err
}
return nil
}
func (es *Service) SendUserAccessTokenAddedEmail(email, locale, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.user_access_token_subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.user_access_token_body.title")
data.Props["Info"] = T("api.templates.user_access_token_body.info",
map[string]any{"SiteName": es.config().TeamSettings.SiteName, "SiteURL": siteURL})
data.Props["Warning"] = T("api.templates.email_warning")
body, err := es.templatesContainer.RenderToString("password_change_body", data)
if err != nil {
return err
}
if err := es.sendMail(email, subject, body, "UserAccessTokenAddedEmail"); err != nil {
return err
}
return nil
}
func (es *Service) SendPasswordResetEmail(email string, token *model.Token, locale, siteURL string) (bool, error) {
T := i18n.GetUserTranslations(locale)
link := fmt.Sprintf("%s/reset_password_complete?token=%s", siteURL, url.QueryEscape(token.Token))
subject := T("api.templates.reset_subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.reset_body.title")
data.Props["SubTitle"] = T("api.templates.reset_body.subTitle")
data.Props["Info"] = T("api.templates.reset_body.info")
data.Props["ButtonURL"] = link
data.Props["Button"] = T("api.templates.reset_body.button")
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
body, err := es.templatesContainer.RenderToString("reset_body", data)
if err != nil {
return false, err
}
if err := es.sendMail(email, subject, body, "PasswordResetEmail"); err != nil {
return false, err
}
return true, nil
}
func (es *Service) SendMfaChangeEmail(email string, activated bool, locale, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.mfa_change_subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
if activated {
data.Props["Info"] = T("api.templates.mfa_activated_body.info", map[string]any{"SiteURL": siteURL})
data.Props["Title"] = T("api.templates.mfa_activated_body.title")
} else {
data.Props["Info"] = T("api.templates.mfa_deactivated_body.info", map[string]any{"SiteURL": siteURL})
data.Props["Title"] = T("api.templates.mfa_deactivated_body.title")
}
data.Props["Warning"] = T("api.templates.email_warning")
body, err := es.templatesContainer.RenderToString("mfa_change_body", data)
if err != nil {
return err
}
if err := es.sendMail(email, subject, body, "MfaChangeEmail"); err != nil {
return err
}
return nil
}
func (es *Service) SendInviteEmails(
team *model.Team,
senderName string,
senderUserId string,
invites []string,
siteURL string,
reminderData *model.TeamInviteReminderData,
errorWhenNotSent bool,
isSystemAdmin bool,
isFirstAdmin bool,
) error {
if es.perHourEmailRateLimiter == nil {
return NoRateLimiterError
}
rateLimited, result, err := es.perHourEmailRateLimiter.RateLimit(senderUserId, len(invites))
if err != nil {
return SetupRateLimiterError
}
if rateLimited {
mlog.Error("rate limit exceeded", mlog.Duration("RetryAfter", result.RetryAfter), mlog.Duration("ResetAfter", result.ResetAfter), mlog.String("user_id", senderUserId),
mlog.String("team_id", team.Id), mlog.String("retry_after_secs", fmt.Sprintf("%f", result.RetryAfter.Seconds())), mlog.String("reset_after_secs", fmt.Sprintf("%f", result.ResetAfter.Seconds())))
return RateLimitExceededError
}
for _, invite := range invites {
if invite != "" {
subject := i18n.T("api.templates.invite_subject",
map[string]any{"SenderName": senderName,
"TeamDisplayName": team.DisplayName,
"SiteName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData("")
data.Props["SiteURL"] = siteURL
data.Props["SubTitle"] = i18n.T("api.templates.invite_body.subTitle")
data.Props["Button"] = i18n.T("api.templates.invite_body.button")
data.Props["SenderName"] = senderName
data.Props["InviteFooterTitle"] = i18n.T("api.templates.invite_body_footer.title")
data.Props["InviteFooterInfo"] = i18n.T("api.templates.invite_body_footer.info")
data.Props["InviteFooterLearnMore"] = i18n.T("api.templates.invite_body_footer.learn_more")
token := model.NewToken(
TokenTypeTeamInvitation,
model.MapToJSON(map[string]string{"teamId": team.Id, "email": invite}),
)
tokenProps := make(map[string]string)
tokenProps["email"] = invite
tokenProps["display_name"] = team.DisplayName
tokenProps["name"] = team.Name
title := i18n.T("api.templates.invite_body.title", map[string]any{"SenderName": senderName, "TeamDisplayName": team.DisplayName})
if reminderData != nil {
reminder := i18n.T("api.templates.invite_body.title.reminder")
title = fmt.Sprintf("%s: %s", reminder, title)
tokenProps["reminder_interval"] = reminderData.Interval
}
data.Props["Title"] = title
tokenData := model.MapToJSON(tokenProps)
if err := es.store.Token().Save(token); err != nil {
mlog.Error("Failed to send invite email successfully ", mlog.Err(err))
continue
}
queryString := url.Values{}
queryString.Add("d", tokenData)
queryString.Add("t", token.Token)
queryString.Add("md", "email")
queryString.Add("sbr", es.GetTrackFlowStartedByRole(isFirstAdmin, isSystemAdmin))
data.Props["ButtonURL"] = fmt.Sprintf("%s/signup_user_complete/?%s", siteURL, queryString.Encode())
body, err := es.templatesContainer.RenderToString("invite_body", data)
if err != nil {
mlog.Error("Failed to send invite email successfully ", mlog.Err(err))
}
if err := es.sendMail(invite, subject, body, "InviteEmail"); err != nil {
mlog.Error("Failed to send invite email successfully ", mlog.Err(err))
if errorWhenNotSent {
return SendMailError
}
}
}
}
return nil
}
func (es *Service) SendGuestInviteEmails(
team *model.Team,
channels []*model.Channel,
senderName string,
senderUserId string,
senderProfileImage []byte,
invites []string,
siteURL string,
message string,
errorWhenNotSent bool,
isSystemAdmin bool,
isFirstAdmin bool,
) error {
if es.perHourEmailRateLimiter == nil {
return NoRateLimiterError
}
rateLimited, result, err := es.perHourEmailRateLimiter.RateLimit(senderUserId, len(invites))
if err != nil {
return SetupRateLimiterError
}
if rateLimited {
mlog.Error("rate limit exceeded", mlog.Duration("RetryAfter", result.RetryAfter), mlog.Duration("ResetAfter", result.ResetAfter), mlog.String("user_id", senderUserId),
mlog.String("team_id", team.Id), mlog.String("retry_after_secs", fmt.Sprintf("%f", result.RetryAfter.Seconds())), mlog.String("reset_after_secs", fmt.Sprintf("%f", result.ResetAfter.Seconds())))
return RateLimitExceededError
}
for _, invite := range invites {
if invite != "" {
subject := i18n.T("api.templates.invite_guest_subject",
map[string]any{"SenderName": senderName,
"TeamDisplayName": team.DisplayName,
"SiteName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData("")
data.Props["SiteURL"] = siteURL
data.Props["Title"] = i18n.T("api.templates.invite_body.title", map[string]any{"SenderName": senderName, "TeamDisplayName": team.DisplayName})
data.Props["SubTitle"] = i18n.T("api.templates.invite_body_guest.subTitle")
data.Props["Button"] = i18n.T("api.templates.invite_body.button")
data.Props["SenderName"] = senderName
if message != "" {
message = bluemonday.NewPolicy().Sanitize(message)
}
data.Props["Message"] = message
data.Props["InviteFooterTitle"] = i18n.T("api.templates.invite_body_footer.title")
data.Props["InviteFooterInfo"] = i18n.T("api.templates.invite_body_footer.info")
data.Props["InviteFooterLearnMore"] = i18n.T("api.templates.invite_body_footer.learn_more")
channelIDs := []string{}
for _, channel := range channels {
channelIDs = append(channelIDs, channel.Id)
}
token := model.NewToken(
TokenTypeGuestInvitation,
model.MapToJSON(map[string]string{
"teamId": team.Id,
"channels": strings.Join(channelIDs, " "),
"email": invite,
"guest": "true",
"senderId": senderUserId,
}),
)
tokenProps := make(map[string]string)
tokenProps["email"] = invite
tokenProps["display_name"] = team.DisplayName
tokenProps["name"] = team.Name
tokenData := model.MapToJSON(tokenProps)
if err := es.store.Token().Save(token); err != nil {
mlog.Error("Failed to send invite email successfully ", mlog.Err(err))
continue
}
data.Props["ButtonURL"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&t=%s&sbr=%s", siteURL, url.QueryEscape(tokenData), url.QueryEscape(token.Token), es.GetTrackFlowStartedByRole(isFirstAdmin, isSystemAdmin))
if !*es.config().EmailSettings.SendEmailNotifications {
mlog.Info("sending invitation ", mlog.String("to", invite), mlog.String("link", data.Props["ButtonURL"].(string)))
}
senderPhoto := ""
embeddedFiles := make(map[string]io.Reader)
if message != "" {
if senderProfileImage != nil {
senderPhoto = "user-avatar.png"
embeddedFiles = map[string]io.Reader{
senderPhoto: bytes.NewReader(senderProfileImage),
}
}
}
pData := postData{
SenderName: senderName,
Message: template.HTML(message),
SenderPhoto: senderPhoto,
}
data.Props["Posts"] = []postData{pData}
body, err := es.templatesContainer.RenderToString("invite_body", data)
if err != nil {
mlog.Error("Failed to send invite email successfully", mlog.Err(err))
}
if nErr := es.SendMailWithEmbeddedFiles(invite, subject, body, embeddedFiles, "", "", "", "InviteEmail"); nErr != nil {
mlog.Error("Failed to send invite email successfully", mlog.Err(nErr))
if errorWhenNotSent {
return SendMailError
}
}
}
}
return nil
}
func (es *Service) SendInviteEmailsToTeamAndChannels(
team *model.Team,
channels []*model.Channel,
senderName string,
senderUserId string,
senderProfileImage []byte,
invites []string,
siteURL string,
reminderData *model.TeamInviteReminderData,
message string,
errorWhenNotSent bool,
isSystemAdmin bool,
isFirstAdmin bool,
) ([]*model.EmailInviteWithError, error) {
if es.perHourEmailRateLimiter == nil {
return nil, NoRateLimiterError
}
rateLimited, result, err := es.perHourEmailRateLimiter.RateLimit(senderUserId, len(invites))
if err != nil {
return nil, SetupRateLimiterError
}
if rateLimited {
mlog.Error("rate limit exceeded", mlog.Duration("RetryAfter", result.RetryAfter), mlog.Duration("ResetAfter", result.ResetAfter), mlog.String("user_id", senderUserId),
mlog.String("team_id", team.Id), mlog.String("retry_after_secs", fmt.Sprintf("%f", result.RetryAfter.Seconds())), mlog.String("reset_after_secs", fmt.Sprintf("%f", result.ResetAfter.Seconds())))
return nil, RateLimitExceededError
}
channelsLen := len(channels)
subject := i18n.T("api.templates.invite_team_and_channels_subject", map[string]any{
"SenderName": senderName,
"TeamDisplayName": team.DisplayName,
"ChannelsLen": channelsLen,
"SiteName": es.config().TeamSettings.SiteName})
title := i18n.T("api.templates.invite_team_and_channels_body.title", map[string]any{
"SenderName": senderName,
"ChannelsLen": channelsLen,
"TeamDisplayName": team.DisplayName})
if channelsLen == 1 {
channelName := channels[0].DisplayName
subject = i18n.T("api.templates.invite_team_and_channel_subject",
map[string]any{"SenderName": senderName,
"TeamDisplayName": team.DisplayName,
"ChannelName": channelName,
"SiteName": es.config().TeamSettings.SiteName},
)
title = i18n.T("api.templates.invite_team_and_channel_body.title", map[string]any{
"SenderName": senderName,
"ChannelName": channelName,
"TeamDisplayName": team.DisplayName,
})
}
var invitesWithErrors []*model.EmailInviteWithError
for _, invite := range invites {
if invite == "" {
continue
}
channelIDs := []string{}
for _, channel := range channels {
channelIDs = append(channelIDs, channel.Id)
}
data := es.NewEmailTemplateData("")
data.Props["SiteURL"] = siteURL
data.Props["SubTitle"] = i18n.T("api.templates.invite_body.subTitle")
data.Props["Button"] = i18n.T("api.templates.invite_body.button")
data.Props["SenderName"] = senderName
data.Props["InviteFooterTitle"] = i18n.T("api.templates.invite_body_footer.title")
data.Props["InviteFooterInfo"] = i18n.T("api.templates.invite_body_footer.info")
data.Props["InviteFooterLearnMore"] = i18n.T("api.templates.invite_body_footer.learn_more")
if message != "" {
message = bluemonday.NewPolicy().Sanitize(message)
}
data.Props["Message"] = message
token := model.NewToken(
TokenTypeTeamInvitation,
model.MapToJSON(map[string]string{
"teamId": team.Id,
"email": invite,
"channels": strings.Join(channelIDs, " "),
"senderId": senderUserId,
}),
)
tokenProps := make(map[string]string)
tokenProps["email"] = invite
tokenProps["display_name"] = team.DisplayName
tokenProps["name"] = team.Name
if reminderData != nil {
reminder := i18n.T("api.templates.invite_body.title.reminder")
title = fmt.Sprintf("%s: %s", reminder, title)
tokenProps["reminder_interval"] = reminderData.Interval
}
data.Props["Title"] = title
tokenData := model.MapToJSON(tokenProps)
if err := es.store.Token().Save(token); err != nil {
mlog.Error("Failed to send invite email successfully ", mlog.Err(err))
continue
}
data.Props["ButtonURL"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&t=%s&sbr=%s", siteURL, url.QueryEscape(tokenData), url.QueryEscape(token.Token), es.GetTrackFlowStartedByRole(isFirstAdmin, isSystemAdmin))
senderPhoto := ""
embeddedFiles := make(map[string]io.Reader)
if message != "" {
if senderProfileImage != nil {
senderPhoto = "user-avatar.png"
embeddedFiles = map[string]io.Reader{
senderPhoto: bytes.NewReader(senderProfileImage),
}
}
}
pData := postData{
SenderName: senderName,
Message: template.HTML(message),
SenderPhoto: senderPhoto,
}
data.Props["Posts"] = []postData{pData}
body, err := es.templatesContainer.RenderToString("invite_body", data)
if err != nil {
mlog.Error("Failed to send invite email successfully ", mlog.Err(err))
}
if nErr := es.SendMailWithEmbeddedFiles(invite, subject, body, embeddedFiles, "", "", "", "InviteEmailToTeamsAndChannels"); nErr != nil {
mlog.Error("Failed to send invite email successfully", mlog.Err(nErr))
if errorWhenNotSent {
inviteWithError := &model.EmailInviteWithError{
Email: invite,
Error: &model.AppError{Message: nErr.Error()},
}
invitesWithErrors = append(invitesWithErrors, inviteWithError)
}
}
}
return invitesWithErrors, nil
}
func (es *Service) NewEmailTemplateData(locale string) templates.Data {
var localT i18n.TranslateFunc
if locale != "" {
localT = i18n.GetUserTranslations(locale)
} else {
localT = i18n.T
}
organization := ""
if *es.config().EmailSettings.FeedbackOrganization != "" {
organization = localT("api.templates.email_organization") + *es.config().EmailSettings.FeedbackOrganization
}
return templates.Data{
Props: map[string]any{
"EmailInfo1": localT("api.templates.email_info1"),
"EmailInfo2": localT("api.templates.email_info2"),
"EmailInfo3": localT("api.templates.email_info3",
map[string]any{"SiteName": es.config().TeamSettings.SiteName}),
"SupportEmail": *es.config().SupportSettings.SupportEmail,
"Footer": localT("api.templates.email_footer"),
"FooterV2": localT("api.templates.email_footer_v2"),
"Organization": organization,
},
HTML: map[string]template.HTML{},
}
}
func (es *Service) SendDeactivateAccountEmail(email string, locale, siteURL string) error {
T := i18n.GetUserTranslations(locale)
serverURL := condenseSiteURL(siteURL)
subject := T("api.templates.deactivate_subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName,
"ServerURL": serverURL})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.deactivate_body.title", map[string]any{"ServerURL": serverURL})
data.Props["Info"] = T("api.templates.deactivate_body.info",
map[string]any{"SiteURL": siteURL})
data.Props["Warning"] = T("api.templates.deactivate_body.warning")
body, err := es.templatesContainer.RenderToString("deactivate_body", data)
if err != nil {
return err
}
if err := es.sendMail(email, subject, body, "DeactivateAccountEmail"); err != nil { // this needs to receive the header options
return err
}
return nil
}
func (es *Service) SendNotificationMail(to, subject, htmlBody string) error {
if !*es.config().EmailSettings.SendEmailNotifications {
return nil
}
return es.sendMail(to, subject, htmlBody, "NotificationEmail")
}
func (es *Service) sendMail(to, subject, htmlBody, category string) error {
return es.sendMailWithCC(to, subject, htmlBody, "", category)
}
func (es *Service) sendEmailWithCustomReplyTo(to, subject, htmlBody, replyToAddress, category string) error {
license := es.license()
mailConfig := es.mailServiceConfig(replyToAddress)
category = getSendGridCategory(category, license.IsCloud())
return mail.SendMailUsingConfig(to, subject, htmlBody, mailConfig, license != nil && *license.Features.Compliance, "", "", "", "", category)
}
func (es *Service) sendMailWithCC(to, subject, htmlBody, ccMail, category string) error {
license := es.license()
mailConfig := es.mailServiceConfig("")
category = getSendGridCategory(category, license.IsCloud())
return mail.SendMailUsingConfig(to, subject, htmlBody, mailConfig, license != nil && *license.Features.Compliance, "", "", "", ccMail, category)
}
func (es *Service) SendMailWithEmbeddedFilesAndCustomReplyTo(to, subject, htmlBody, replyToAddress string, embeddedFiles map[string]io.Reader, category string) error {
license := es.license()
mailConfig := es.mailServiceConfig(replyToAddress)
category = getSendGridCategory(category, license.IsCloud())
return mail.SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody, embeddedFiles, mailConfig, license != nil && *license.Features.Compliance, "", "", "", "", category)
}
func (es *Service) SendMailWithEmbeddedFiles(to, subject, htmlBody string, embeddedFiles map[string]io.Reader, messageID string, inReplyTo string, references string, category string) error {
license := es.license()
mailConfig := es.mailServiceConfig("")
category = getSendGridCategory(category, license.IsCloud())
return mail.SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody, embeddedFiles, mailConfig, license != nil && *license.Features.Compliance, messageID, inReplyTo, references, "", category)
}
func (es *Service) InvalidateVerifyEmailTokensForUser(userID string) *model.AppError {
tokens, err := es.store.Token().GetAllTokensByType(TokenTypeVerifyEmail)
if err != nil {
return model.NewAppError("InvalidateVerifyEmailTokensForUser", "api.user.invalidate_verify_email_tokens.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
var appErr *model.AppError = nil
for _, token := range tokens {
tokenExtra := struct {
UserId string
Email string
}{}
if err := json.Unmarshal([]byte(token.Extra), &tokenExtra); err != nil {
appErr = model.NewAppError("InvalidateVerifyEmailTokensForUser", "api.user.invalidate_verify_email_tokens_parse.error", nil, "", http.StatusInternalServerError).Wrap(err)
continue
}
if tokenExtra.UserId != userID {
continue
}
if err := es.store.Token().Delete(token.Token); err != nil {
appErr = model.NewAppError("InvalidateVerifyEmailTokensForUser", "api.user.invalidate_verify_email_tokens_delete.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return appErr
}
func (es *Service) CreateVerifyEmailToken(userID string, newEmail string) (*model.Token, error) {
tokenExtra := struct {
UserId string
Email string
}{
userID,
newEmail,
}
jsonData, err := json.Marshal(tokenExtra)
if err != nil {
return nil, errors.Wrap(CreateEmailTokenError, err.Error())
}
token := model.NewToken(TokenTypeVerifyEmail, string(jsonData))
if err := es.InvalidateVerifyEmailTokensForUser(userID); err != nil {
return nil, err
}
if err = es.store.Token().Save(token); err != nil {
return nil, err
}
return token, nil
}
func (es *Service) SendLicenseUpForRenewalEmail(email, name, locale, siteURL, ctaTitle, ctaLink, ctaText string, daysToExpiration int) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.license_up_for_renewal_subject")
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.license_up_for_renewal_title")
data.Props["SubTitle"] = T("api.templates.license_up_for_renewal_subtitle", map[string]any{"UserName": name, "Days": daysToExpiration})
data.Props["SubTitleTwo"] = ctaTitle
data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
data.Props["Button"] = ctaText
data.Props["ButtonURL"] = ctaLink
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["SupportEmail"] = "feedback@mattermost.com"
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
body, err := es.templatesContainer.RenderToString("license_up_for_renewal", data)
if err != nil {
return err
}
if err := es.sendMail(email, subject, body, "LicenseUpForRenewal"); err != nil {
return err
}
return nil
}
func (es *Service) SendPaymentFailedEmail(email string, locale string, failedPayment *model.FailedPayment, planName, siteURL string) (bool, error) {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.payment_failed.subject", map[string]any{"Plan": planName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.payment_failed.title")
data.Props["SubTitle1"] = T("api.templates.payment_failed.info1", map[string]any{"CardBrand": failedPayment.CardBrand, "LastFour": failedPayment.LastFour})
data.Props["SubTitle2"] = T("api.templates.payment_failed.info2")
data.Props["FailedReason"] = failedPayment.FailureMessage
data.Props["SubTitle3"] = T("api.templates.payment_failed.info3", map[string]any{"Plan": planName})
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
data.Props["Button"] = T("api.templates.delinquency_45.button")
data.Props["IncludeSecondaryActionButton"] = false
data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
data.Props["Footer"] = T("api.templates.copyright")
body, err := es.templatesContainer.RenderToString("payment_failed_body", data)
if err != nil {
return false, err
}
if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail, "PaymentFailed"); err != nil {
return false, err
}
return true, nil
}
func (es *Service) SendNoCardPaymentFailedEmail(email string, locale string, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.payment_failed_no_card.subject")
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.payment_failed_no_card.title")
data.Props["Info1"] = T("api.templates.payment_failed_no_card.info1")
data.Props["Info3"] = T("api.templates.payment_failed_no_card.info3")
data.Props["Button"] = T("api.templates.payment_failed_no_card.button")
data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
data.Props["Footer"] = T("api.templates.copyright")
body, err := es.templatesContainer.RenderToString("payment_failed_no_card_body", data)
if err != nil {
return err
}
if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail, "NoCardPaymentFailed"); err != nil {
return err
}
return nil
}
func (es *Service) SendDelinquencyEmail7(email, locale, siteURL, planName string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.payment_failed.subject", map[string]any{"Plan": planName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.delinquency_7.title")
data.Props["SubTitle1"] = T("api.templates.delinquency_7.subtitle1")
data.Props["SubTitle2"] = T("api.templates.delinquency_7.subtitle2", map[string]any{"Plan": planName})
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
data.Props["Button"] = T("api.templates.delinquency_7.button")
data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
data.Props["Footer"] = T("api.templates.copyright")
body, err := es.templatesContainer.RenderToString("cloud_7_day_arrears", data)
if err != nil {
return err
}
if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail, "Delinquency7"); err != nil {
return err
}
return nil
}
func (es *Service) SendDelinquencyEmail14(email, locale, siteURL, planName string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.delinquency_14.subject", map[string]any{"Plan": planName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.delinquency_14.title")
data.Props["SubTitle1"] = T("api.templates.delinquency_14.subtitle1")
data.Props["SubTitle2"] = T("api.templates.delinquency_14.subtitle2")
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
data.Props["Button"] = T("api.templates.delinquency_14.button")
data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
data.Props["Footer"] = T("api.templates.copyright")
body, err := es.templatesContainer.RenderToString("cloud_14_day_arrears", data)
if err != nil {
return err
}
if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail, "Delinquency14"); err != nil {
return err
}
return nil
}
func (es *Service) SendDelinquencyEmail30(email, locale, siteURL, planName string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.delinquency_30.subject", map[string]any{"Plan": planName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.delinquency_30.title")
data.Props["SubTitle1"] = T("api.templates.delinquency_30.subtitle1", map[string]any{"Plan": planName})
data.Props["SubTitle2"] = T("api.templates.delinquency_30.subtitle2")
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
data.Props["Button"] = T("api.templates.delinquency_30.button")
data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
data.Props["BulletListItems"] = []string{T("api.templates.delinquency_30.bullet.message_history"), T("api.templates.delinquency_30.bullet.files")}
data.Props["LimitsDocs"] = T("api.templates.delinquency_30.limits_documentation")
data.Props["Footer"] = T("api.templates.copyright")
body, err := es.templatesContainer.RenderToString("cloud_30_day_arrears", data)
if err != nil {
return err
}
if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail, "Delinquency30"); err != nil {
return err
}
return nil
}
func (es *Service) SendDelinquencyEmail45(email, locale, siteURL, planName, delinquencyDate string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.delinquency_45.subject", map[string]any{"Plan": planName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.delinquency_45.title")
data.Props["SubTitle1"] = T("api.templates.delinquency_45.subtitle1", map[string]any{"DelinquencyDate": delinquencyDate})
data.Props["SubTitle2"] = T("api.templates.delinquency_45.subtitle2")
data.Props["SubTitle3"] = T("api.templates.delinquency_45.subtitle3")
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
data.Props["Button"] = T("api.templates.delinquency_45.button")
data.Props["IncludeSecondaryActionButton"] = false
data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
data.Props["Footer"] = T("api.templates.copyright")
body, err := es.templatesContainer.RenderToString("cloud_45_day_arrears", data)
if err != nil {
return err
}
if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail, "Delinquency45"); err != nil {
return err
}
return nil
}
func (es *Service) SendDelinquencyEmail60(email, locale, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.delinquency_60.subject")
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.delinquency_60.title")
data.Props["SubTitle1"] = T("api.templates.delinquency_60.subtitle1")
data.Props["SubTitle2"] = T("api.templates.delinquency_60.subtitle2")
data.Props["SubTitle3"] = T("api.templates.delinquency_60.subtitle3")
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
data.Props["Button"] = T("api.templates.delinquency_60.button")
data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
data.Props["IncludeSecondaryActionButton"] = true
data.Props["SecondaryActionButtonText"] = T("api.templates.delinquency_60.downgrade_to_free")
data.Props["Footer"] = T("api.templates.copyright")
// 45 day template is the same as the 60 day one so its reused
body, err := es.templatesContainer.RenderToString("cloud_45_day_arrears", data)
if err != nil {
return err
}
if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail, "Delinquency60"); err != nil {
return err
}
return nil
}
func (es *Service) SendDelinquencyEmail75(email, locale, siteURL, planName, delinquencyDate string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.delinquency_75.subject", map[string]any{"Plan": planName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.delinquency_75.title")
data.Props["SubTitle1"] = T("api.templates.delinquency_75.subtitle1", map[string]any{"DelinquencyDate": delinquencyDate})
data.Props["SubTitle2"] = T("api.templates.delinquency_75.subtitle2", map[string]any{"Plan": planName})
data.Props["SubTitle3"] = T("api.templates.delinquency_75.subtitle3")
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
data.Props["Button"] = T("api.templates.delinquency_75.button")
data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
data.Props["IncludeSecondaryActionButton"] = true
data.Props["SecondaryActionButtonText"] = T("api.templates.delinquency_75.downgrade_to_free")
data.Props["Footer"] = T("api.templates.copyright")
// 45 day template is the same as the 75 day one so its reused
body, err := es.templatesContainer.RenderToString("cloud_45_day_arrears", data)
if err != nil {
return err
}
if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail, "Delinquency75"); err != nil {
return err
}
return nil
}
func (es *Service) SendDelinquencyEmail90(email, locale, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.delinquency_90.subject")
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.delinquency_90.title")
data.Props["SubTitle1"] = T("api.templates.delinquency_90.subtitle1", map[string]any{"SiteURL": siteURL})
data.Props["SubTitle2"] = T("api.templates.delinquency_90.subtitle2")
data.Props["SubTitle3"] = T("api.templates.delinquency_90.subtitle3")
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
data.Props["Button"] = T("api.templates.delinquency_90.button")
data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
data.Props["IncludeSecondaryActionButton"] = true
data.Props["SecondaryActionButtonText"] = T("api.templates.delinquency_90.secondary_action_button")
data.Props["Footer"] = T("api.templates.copyright")
body, err := es.templatesContainer.RenderToString("cloud_90_day_arrears", data)
if err != nil {
return err
}
if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail, "Delinquency90"); err != nil {
return err
}
return nil
}
// SendRemoveExpiredLicenseEmail formats an email and uses the email service to send the email to user with link pointing to CWS
// to renew the user license
func (es *Service) SendRemoveExpiredLicenseEmail(ctaText, ctaLink, email, locale, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.remove_expired_license.subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName})
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.remove_expired_license.body.title")
data.Props["Link"] = ctaLink
data.Props["LinkButton"] = ctaText
body, err := es.templatesContainer.RenderToString("remove_expired_license", data)
if err != nil {
return err
}
if err := es.sendMail(email, subject, body, "RemoveExpiredLicense"); err != nil {
return err
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package email
import (
"bytes"
"fmt"
"html/template"
"io"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
EmailBatchingTaskName = "Email Batching"
)
type postData struct {
SenderName string
ChannelName string
Message template.HTML
MessageURL string
SenderPhoto string
PostPhoto string
Time string
ShowChannelIcon bool
OtherChannelMembersCount int
MessageAttachments []*EmailMessageAttachment
}
func (es *Service) InitEmailBatching() {
if *es.config().EmailSettings.EnableEmailBatching {
if es.EmailBatching == nil {
es.EmailBatching = NewEmailBatchingJob(es, *es.config().EmailSettings.EmailBatchingBufferSize)
}
// note that we don't support changing EmailBatchingBufferSize without restarting the server
es.EmailBatching.Start()
}
}
func (es *Service) AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError {
if !*es.config().EmailSettings.EnableEmailBatching {
return model.NewAppError("AddNotificationEmailToBatch", "api.email_batching.add_notification_email_to_batch.disabled.app_error", nil, "", http.StatusNotImplemented)
}
if !es.EmailBatching.Add(user, post, team) {
mlog.Error("Email batching job's receiving buffer was full. Please increase the EmailBatchingBufferSize. Falling back to sending immediate mail.")
return model.NewAppError("AddNotificationEmailToBatch", "api.email_batching.add_notification_email_to_batch.channel_full.app_error", nil, "", http.StatusInternalServerError)
}
return nil
}
type batchedNotification struct {
userID string
post *model.Post
teamName string
}
type EmailBatchingJob struct {
config func() *model.Config
service *Service
newNotifications chan *batchedNotification
pendingNotifications map[string][]*batchedNotification
task *model.ScheduledTask
taskMutex sync.Mutex
}
func NewEmailBatchingJob(es *Service, bufferSize int) *EmailBatchingJob {
return &EmailBatchingJob{
config: es.config,
service: es,
newNotifications: make(chan *batchedNotification, bufferSize),
pendingNotifications: make(map[string][]*batchedNotification),
}
}
func (job *EmailBatchingJob) Start() {
mlog.Debug("Email batching job starting. Checking for pending emails periodically.", mlog.Int("interval_in_seconds", *job.config().EmailSettings.EmailBatchingInterval))
newTask := model.CreateRecurringTask(EmailBatchingTaskName, job.CheckPendingEmails, time.Duration(*job.config().EmailSettings.EmailBatchingInterval)*time.Second)
job.taskMutex.Lock()
oldTask := job.task
job.task = newTask
job.taskMutex.Unlock()
if oldTask != nil {
oldTask.Cancel()
}
}
// Stop will cancel the task properly, flushing out any pending notifications.
// Although this still won't send those notifications which are yet to be sent
// due to a user's PreferenceNameEmailInterval.
func (job *EmailBatchingJob) Stop() {
job.taskMutex.Lock()
if task := job.task; task != nil {
task.Cancel()
}
job.taskMutex.Unlock()
}
func (job *EmailBatchingJob) Add(user *model.User, post *model.Post, team *model.Team) bool {
notification := &batchedNotification{
userID: user.Id,
post: post,
teamName: team.Name,
}
select {
case job.newNotifications <- notification:
return true
default:
// return false if we couldn't queue the email notification so that we can send an immediate email
return false
}
}
func (job *EmailBatchingJob) CheckPendingEmails() {
job.handleNewNotifications()
// it's a bit weird to pass the send email function through here, but it makes it so that we can test
// without actually sending emails
job.checkPendingNotifications(time.Now(), job.service.sendBatchedEmailNotification)
mlog.Debug("Email batching job ran. Notifications might be still pending.", mlog.Int("number_of_users", len(job.pendingNotifications)))
}
func (job *EmailBatchingJob) handleNewNotifications() {
receiving := true
// read in new notifications to send
for receiving {
select {
case notification := <-job.newNotifications:
userID := notification.userID
if _, ok := job.pendingNotifications[userID]; !ok {
job.pendingNotifications[userID] = []*batchedNotification{notification}
} else {
job.pendingNotifications[userID] = append(job.pendingNotifications[userID], notification)
}
default:
receiving = false
}
}
}
func (job *EmailBatchingJob) checkPendingNotifications(now time.Time, handler func(string, []*batchedNotification)) {
for userID, notifications := range job.pendingNotifications {
// Defensive code.
if len(notifications) == 0 {
mlog.Warn("Unexpected result. Got 0 pending notifications for batched email.", mlog.String("user_id", userID))
continue
}
// get how long we need to wait to send notifications to the user
var interval int64
preference, err := job.service.store.Preference().Get(userID, model.PreferenceCategoryNotifications, model.PreferenceNameEmailInterval)
if err != nil {
// use the default batching interval if an error occurs while fetching user preferences
interval, _ = strconv.ParseInt(model.PreferenceEmailIntervalBatchingSeconds, 10, 64)
} else {
if value, err := strconv.ParseInt(preference.Value, 10, 64); err != nil {
// // use the default batching interval if an error occurs while deserializing user preferences
interval, _ = strconv.ParseInt(model.PreferenceEmailIntervalBatchingSeconds, 10, 64)
} else {
interval = value
}
}
batchStartTime := notifications[0].post.CreateAt
// Ignore if it isn't time yet to send.
if now.Sub(time.UnixMilli(batchStartTime)) <= time.Duration(interval)*time.Second {
continue
}
// If the user has viewed any channels in this team since the notification was queued, delete
// all queued notifications
inspectedTeamNames := make(map[string]string)
for _, notification := range notifications {
// at most, we'll do one check for each team that notifications were sent for
if inspectedTeamNames[notification.teamName] != "" {
continue
}
team, nErr := job.service.store.Team().GetByName(notifications[0].teamName)
if nErr != nil {
mlog.Error("Unable to find Team id for notification", mlog.Err(nErr))
continue
}
if team != nil {
inspectedTeamNames[notification.teamName] = team.Id
}
channelMembers, err := job.service.store.Channel().GetMembersForUser(inspectedTeamNames[notification.teamName], userID)
if err != nil {
mlog.Error("Unable to find ChannelMembers for user", mlog.Err(err))
continue
}
deleted := false
for _, channelMember := range channelMembers {
if channelMember.LastViewedAt >= batchStartTime {
mlog.Debug("Deleted notifications for user", mlog.String("user_id", userID))
delete(job.pendingNotifications, userID)
deleted = true
break
}
}
if deleted {
break
}
}
// The notifications might have been cleared from the above step.
// We need to check again.
if len(job.pendingNotifications[userID]) == 0 {
continue
}
handler(userID, job.pendingNotifications[userID])
delete(job.pendingNotifications, userID)
}
}
/**
* If the name is longer than i characters, replace remaining characters with ...
*/
func truncateUserNames(name string, i int) string {
runes := []rune(name)
if len(runes) > i {
newString := string(runes[:i])
return newString + "..."
}
return name
}
func (es *Service) sendBatchedEmailNotification(userID string, notifications []*batchedNotification) {
user, err := es.userService.GetUser(userID)
if err != nil {
mlog.Warn("Unable to find recipient for batched email notification")
return
}
translateFunc := i18n.GetUserTranslations(user.Locale)
displayNameFormat := *es.config().TeamSettings.TeammateNameDisplay
siteURL := *es.config().ServiceSettings.SiteURL
postsData := make([]*postData, 0 /* len */, len(notifications) /* cap */)
embeddedFiles := make(map[string]io.Reader)
emailNotificationContentsType := model.EmailNotificationContentsFull
if license := es.license(); license != nil && *license.Features.EmailNotificationContents {
emailNotificationContentsType = *es.config().EmailSettings.EmailNotificationContentsType
}
// check if user has CRT set to ON
appCRT := *es.config().ServiceSettings.CollapsedThreads
threadsEnabled := appCRT == model.CollapsedThreadsAlwaysOn
if !threadsEnabled && appCRT != model.CollapsedThreadsDisabled {
threadsEnabled = appCRT == model.CollapsedThreadsDefaultOn
// check if a participant has overridden collapsed threads settings
if preference, errCrt := es.store.Preference().Get(userID, model.PreferenceCategoryDisplaySettings, model.PreferenceNameCollapsedThreadsEnabled); errCrt == nil {
threadsEnabled = preference.Value == "on"
}
}
if emailNotificationContentsType == model.EmailNotificationContentsFull {
for i, notification := range notifications {
sender, errSender := es.userService.GetUser(notification.post.UserId)
if errSender != nil {
mlog.Warn("Unable to find sender of post for batched email notification")
}
channel, errCh := es.store.Channel().Get(notification.post.ChannelId, true)
if errCh != nil {
mlog.Warn("Unable to find channel of post for batched email notification")
}
senderProfileImage, _, errProfileImage := es.userService.GetProfileImage(sender)
if errProfileImage != nil {
mlog.Warn("Unable to get the sender user profile image.", mlog.String("user_id", sender.Id), mlog.Err(errProfileImage))
}
senderPhoto := fmt.Sprintf("user-avatar-%d.png", i)
if senderProfileImage != nil {
embeddedFiles[senderPhoto] = bytes.NewReader(senderProfileImage)
}
tm := time.Unix(notification.post.CreateAt/1000, 0)
timezone, _ := tm.Zone()
t := translateFunc("api.email_batching.send_batched_email_notification.time", map[string]any{
"Hour": tm.Hour(),
"Minute": fmt.Sprintf("%02d", tm.Minute()),
"Month": translateFunc(tm.Month().String()),
"Day": tm.Day(),
"Year": tm.Year(),
"TimeZone": timezone,
})
MessageURL := siteURL + "/" + notification.teamName + "/pl/" + notification.post.Id
channelDisplayName := channel.DisplayName
showChannelIcon := true
otherChannelMembersCount := 0
if threadsEnabled && notification.post.RootId != "" {
props := map[string]any{"channelName": channelDisplayName}
channelDisplayName = translateFunc("api.push_notification.title.collapsed_threads", props)
if channel.Type == model.ChannelTypeDirect {
channelDisplayName = translateFunc("api.push_notification.title.collapsed_threads_dm")
}
}
if channel.Type == model.ChannelTypeGroup {
otherChannelMembersCount = len(strings.Split(channelDisplayName, ",")) - 1
showChannelIcon = false
channelDisplayName = truncateUserNames(channel.DisplayName, 11)
}
postsData = append(postsData, &postData{
SenderPhoto: senderPhoto,
SenderName: truncateUserNames(sender.GetDisplayName(displayNameFormat), 22),
Time: t,
ChannelName: channelDisplayName,
Message: template.HTML(es.GetMessageForNotification(notification.post, translateFunc)),
MessageURL: MessageURL,
ShowChannelIcon: showChannelIcon,
OtherChannelMembersCount: otherChannelMembersCount,
MessageAttachments: ProcessMessageAttachments(notification.post, siteURL),
})
}
}
tm := time.Unix(notifications[0].post.CreateAt/1000, 0)
subject := translateFunc("api.email_batching.send_batched_email_notification.subject", len(notifications), map[string]any{
"SiteName": es.config().TeamSettings.SiteName,
"Year": tm.Year(),
"Month": translateFunc(tm.Month().String()),
"Day": tm.Day(),
})
data := es.NewEmailTemplateData(user.Locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = translateFunc("api.email_batching.send_batched_email_notification.title", len(notifications)-1)
data.Props["SubTitle"] = translateFunc("api.email_batching.send_batched_email_notification.subTitle")
data.Props["Button"] = translateFunc("api.email_batching.send_batched_email_notification.button")
data.Props["ButtonURL"] = siteURL
data.Props["Posts"] = postsData
data.Props["MessageButton"] = translateFunc("api.email_batching.send_batched_email_notification.messageButton")
data.Props["NotificationFooterTitle"] = translateFunc("app.notification.footer.title")
data.Props["NotificationFooterInfoLogin"] = translateFunc("app.notification.footer.infoLogin")
data.Props["NotificationFooterInfo"] = translateFunc("app.notification.footer.info")
renderedPage, renderErr := es.templatesContainer.RenderToString("messages_notification", data)
if renderErr != nil {
mlog.Error("Unable to render email", mlog.Err(renderErr))
}
if nErr := es.SendMailWithEmbeddedFiles(user.Email, subject, renderedPage, embeddedFiles, "", "", "", "BatchedEmailNotification"); nErr != nil {
mlog.Warn("Unable to send batched email notification", mlog.String("email", user.Email), mlog.Err(nErr))
}
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make email-mocks`.
package mocks
import (
io "io"
i18n "github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
mock "github.com/stretchr/testify/mock"
model "github.com/mattermost/mattermost-server/v6/model"
templates "github.com/mattermost/mattermost-server/v6/server/platform/shared/templates"
throttled "github.com/throttled/throttled"
)
// ServiceInterface is an autogenerated mock type for the ServiceInterface type
type ServiceInterface struct {
mock.Mock
}
// AddNotificationEmailToBatch provides a mock function with given fields: user, post, team
func (_m *ServiceInterface) AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError {
ret := _m.Called(user, post, team)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(*model.User, *model.Post, *model.Team) *model.AppError); ok {
r0 = rf(user, post, team)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// CreateVerifyEmailToken provides a mock function with given fields: userID, newEmail
func (_m *ServiceInterface) CreateVerifyEmailToken(userID string, newEmail string) (*model.Token, error) {
ret := _m.Called(userID, newEmail)
var r0 *model.Token
if rf, ok := ret.Get(0).(func(string, string) *model.Token); ok {
r0 = rf(userID, newEmail)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Token)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, newEmail)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMessageForNotification provides a mock function with given fields: post, translateFunc
func (_m *ServiceInterface) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string {
ret := _m.Called(post, translateFunc)
var r0 string
if rf, ok := ret.Get(0).(func(*model.Post, i18n.TranslateFunc) string); ok {
r0 = rf(post, translateFunc)
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// GetPerDayEmailRateLimiter provides a mock function with given fields:
func (_m *ServiceInterface) GetPerDayEmailRateLimiter() *throttled.GCRARateLimiter {
ret := _m.Called()
var r0 *throttled.GCRARateLimiter
if rf, ok := ret.Get(0).(func() *throttled.GCRARateLimiter); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*throttled.GCRARateLimiter)
}
}
return r0
}
// InitEmailBatching provides a mock function with given fields:
func (_m *ServiceInterface) InitEmailBatching() {
_m.Called()
}
// NewEmailTemplateData provides a mock function with given fields: locale
func (_m *ServiceInterface) NewEmailTemplateData(locale string) templates.Data {
ret := _m.Called(locale)
var r0 templates.Data
if rf, ok := ret.Get(0).(func(string) templates.Data); ok {
r0 = rf(locale)
} else {
r0 = ret.Get(0).(templates.Data)
}
return r0
}
// SendChangeUsernameEmail provides a mock function with given fields: newUsername, _a1, locale, siteURL
func (_m *ServiceInterface) SendChangeUsernameEmail(newUsername string, _a1 string, locale string, siteURL string) error {
ret := _m.Called(newUsername, _a1, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
r0 = rf(newUsername, _a1, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendCloudUpgradeConfirmationEmail provides a mock function with given fields: userEmail, name, trialEndDate, locale, siteURL, workspaceName, isYearly, embeddedFiles
func (_m *ServiceInterface) SendCloudUpgradeConfirmationEmail(userEmail string, name string, trialEndDate string, locale string, siteURL string, workspaceName string, isYearly bool, embeddedFiles map[string]io.Reader) error {
ret := _m.Called(userEmail, name, trialEndDate, locale, siteURL, workspaceName, isYearly, embeddedFiles)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string, string, string, bool, map[string]io.Reader) error); ok {
r0 = rf(userEmail, name, trialEndDate, locale, siteURL, workspaceName, isYearly, embeddedFiles)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendCloudWelcomeEmail provides a mock function with given fields: userEmail, locale, teamInviteID, workSpaceName, dns, siteURL
func (_m *ServiceInterface) SendCloudWelcomeEmail(userEmail string, locale string, teamInviteID string, workSpaceName string, dns string, siteURL string) error {
ret := _m.Called(userEmail, locale, teamInviteID, workSpaceName, dns, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string, string, string) error); ok {
r0 = rf(userEmail, locale, teamInviteID, workSpaceName, dns, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendDeactivateAccountEmail provides a mock function with given fields: _a0, locale, siteURL
func (_m *ServiceInterface) SendDeactivateAccountEmail(_a0 string, locale string, siteURL string) error {
ret := _m.Called(_a0, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = rf(_a0, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendDelinquencyEmail14 provides a mock function with given fields: _a0, locale, siteURL, planName
func (_m *ServiceInterface) SendDelinquencyEmail14(_a0 string, locale string, siteURL string, planName string) error {
ret := _m.Called(_a0, locale, siteURL, planName)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
r0 = rf(_a0, locale, siteURL, planName)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendDelinquencyEmail30 provides a mock function with given fields: _a0, locale, siteURL, planName
func (_m *ServiceInterface) SendDelinquencyEmail30(_a0 string, locale string, siteURL string, planName string) error {
ret := _m.Called(_a0, locale, siteURL, planName)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
r0 = rf(_a0, locale, siteURL, planName)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendDelinquencyEmail45 provides a mock function with given fields: _a0, locale, siteURL, planName, delinquencyDate
func (_m *ServiceInterface) SendDelinquencyEmail45(_a0 string, locale string, siteURL string, planName string, delinquencyDate string) error {
ret := _m.Called(_a0, locale, siteURL, planName, delinquencyDate)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string, string) error); ok {
r0 = rf(_a0, locale, siteURL, planName, delinquencyDate)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendDelinquencyEmail60 provides a mock function with given fields: _a0, locale, siteURL
func (_m *ServiceInterface) SendDelinquencyEmail60(_a0 string, locale string, siteURL string) error {
ret := _m.Called(_a0, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = rf(_a0, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendDelinquencyEmail7 provides a mock function with given fields: _a0, locale, siteURL, planName
func (_m *ServiceInterface) SendDelinquencyEmail7(_a0 string, locale string, siteURL string, planName string) error {
ret := _m.Called(_a0, locale, siteURL, planName)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
r0 = rf(_a0, locale, siteURL, planName)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendDelinquencyEmail75 provides a mock function with given fields: _a0, locale, siteURL, planName, delinquencyDate
func (_m *ServiceInterface) SendDelinquencyEmail75(_a0 string, locale string, siteURL string, planName string, delinquencyDate string) error {
ret := _m.Called(_a0, locale, siteURL, planName, delinquencyDate)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string, string) error); ok {
r0 = rf(_a0, locale, siteURL, planName, delinquencyDate)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendDelinquencyEmail90 provides a mock function with given fields: _a0, locale, siteURL
func (_m *ServiceInterface) SendDelinquencyEmail90(_a0 string, locale string, siteURL string) error {
ret := _m.Called(_a0, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = rf(_a0, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendEmailChangeEmail provides a mock function with given fields: oldEmail, newEmail, locale, siteURL
func (_m *ServiceInterface) SendEmailChangeEmail(oldEmail string, newEmail string, locale string, siteURL string) error {
ret := _m.Called(oldEmail, newEmail, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
r0 = rf(oldEmail, newEmail, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendEmailChangeVerifyEmail provides a mock function with given fields: newUserEmail, locale, siteURL, token
func (_m *ServiceInterface) SendEmailChangeVerifyEmail(newUserEmail string, locale string, siteURL string, token string) error {
ret := _m.Called(newUserEmail, locale, siteURL, token)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
r0 = rf(newUserEmail, locale, siteURL, token)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendGuestInviteEmails provides a mock function with given fields: team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, message, errorWhenNotSent, isSystemAdmin, isFirstAdmin
func (_m *ServiceInterface) SendGuestInviteEmails(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, message string, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) error {
ret := _m.Called(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, message, errorWhenNotSent, isSystemAdmin, isFirstAdmin)
var r0 error
if rf, ok := ret.Get(0).(func(*model.Team, []*model.Channel, string, string, []byte, []string, string, string, bool, bool, bool) error); ok {
r0 = rf(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, message, errorWhenNotSent, isSystemAdmin, isFirstAdmin)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendInviteEmails provides a mock function with given fields: team, senderName, senderUserId, invites, siteURL, reminderData, errorWhenNotSent, isSystemAdmin, isFirstAdmin
func (_m *ServiceInterface) SendInviteEmails(team *model.Team, senderName string, senderUserId string, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) error {
ret := _m.Called(team, senderName, senderUserId, invites, siteURL, reminderData, errorWhenNotSent, isSystemAdmin, isFirstAdmin)
var r0 error
if rf, ok := ret.Get(0).(func(*model.Team, string, string, []string, string, *model.TeamInviteReminderData, bool, bool, bool) error); ok {
r0 = rf(team, senderName, senderUserId, invites, siteURL, reminderData, errorWhenNotSent, isSystemAdmin, isFirstAdmin)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendInviteEmailsToTeamAndChannels provides a mock function with given fields: team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, reminderData, message, errorWhenNotSent, isSystemAdmin, isFirstAdmin
func (_m *ServiceInterface) SendInviteEmailsToTeamAndChannels(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, message string, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) ([]*model.EmailInviteWithError, error) {
ret := _m.Called(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, reminderData, message, errorWhenNotSent, isSystemAdmin, isFirstAdmin)
var r0 []*model.EmailInviteWithError
if rf, ok := ret.Get(0).(func(*model.Team, []*model.Channel, string, string, []byte, []string, string, *model.TeamInviteReminderData, string, bool, bool, bool) []*model.EmailInviteWithError); ok {
r0 = rf(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, reminderData, message, errorWhenNotSent, isSystemAdmin, isFirstAdmin)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.EmailInviteWithError)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Team, []*model.Channel, string, string, []byte, []string, string, *model.TeamInviteReminderData, string, bool, bool, bool) error); ok {
r1 = rf(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, reminderData, message, errorWhenNotSent, isSystemAdmin, isFirstAdmin)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SendLicenseUpForRenewalEmail provides a mock function with given fields: _a0, name, locale, siteURL, ctaTitle, ctaLink, ctaText, daysToExpiration
func (_m *ServiceInterface) SendLicenseUpForRenewalEmail(_a0 string, name string, locale string, siteURL string, ctaTitle string, ctaLink string, ctaText string, daysToExpiration int) error {
ret := _m.Called(_a0, name, locale, siteURL, ctaTitle, ctaLink, ctaText, daysToExpiration)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string, string, string, string, int) error); ok {
r0 = rf(_a0, name, locale, siteURL, ctaTitle, ctaLink, ctaText, daysToExpiration)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendMailWithEmbeddedFiles provides a mock function with given fields: to, subject, htmlBody, embeddedFiles, messageID, inReplyTo, references, category
func (_m *ServiceInterface) SendMailWithEmbeddedFiles(to string, subject string, htmlBody string, embeddedFiles map[string]io.Reader, messageID string, inReplyTo string, references string, category string) error {
ret := _m.Called(to, subject, htmlBody, embeddedFiles, messageID, inReplyTo, references, category)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, map[string]io.Reader, string, string, string, string) error); ok {
r0 = rf(to, subject, htmlBody, embeddedFiles, messageID, inReplyTo, references, category)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendMfaChangeEmail provides a mock function with given fields: _a0, activated, locale, siteURL
func (_m *ServiceInterface) SendMfaChangeEmail(_a0 string, activated bool, locale string, siteURL string) error {
ret := _m.Called(_a0, activated, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, bool, string, string) error); ok {
r0 = rf(_a0, activated, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendNoCardPaymentFailedEmail provides a mock function with given fields: _a0, locale, siteURL
func (_m *ServiceInterface) SendNoCardPaymentFailedEmail(_a0 string, locale string, siteURL string) error {
ret := _m.Called(_a0, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = rf(_a0, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendNotificationMail provides a mock function with given fields: to, subject, htmlBody
func (_m *ServiceInterface) SendNotificationMail(to string, subject string, htmlBody string) error {
ret := _m.Called(to, subject, htmlBody)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = rf(to, subject, htmlBody)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendPasswordChangeEmail provides a mock function with given fields: _a0, method, locale, siteURL
func (_m *ServiceInterface) SendPasswordChangeEmail(_a0 string, method string, locale string, siteURL string) error {
ret := _m.Called(_a0, method, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
r0 = rf(_a0, method, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendPasswordResetEmail provides a mock function with given fields: _a0, token, locale, siteURL
func (_m *ServiceInterface) SendPasswordResetEmail(_a0 string, token *model.Token, locale string, siteURL string) (bool, error) {
ret := _m.Called(_a0, token, locale, siteURL)
var r0 bool
if rf, ok := ret.Get(0).(func(string, *model.Token, string, string) bool); ok {
r0 = rf(_a0, token, locale, siteURL)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, *model.Token, string, string) error); ok {
r1 = rf(_a0, token, locale, siteURL)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SendPaymentFailedEmail provides a mock function with given fields: _a0, locale, failedPayment, planName, siteURL
func (_m *ServiceInterface) SendPaymentFailedEmail(_a0 string, locale string, failedPayment *model.FailedPayment, planName string, siteURL string) (bool, error) {
ret := _m.Called(_a0, locale, failedPayment, planName, siteURL)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string, *model.FailedPayment, string, string) bool); ok {
r0 = rf(_a0, locale, failedPayment, planName, siteURL)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *model.FailedPayment, string, string) error); ok {
r1 = rf(_a0, locale, failedPayment, planName, siteURL)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SendRemoveExpiredLicenseEmail provides a mock function with given fields: ctaText, ctaLink, _a2, locale, siteURL
func (_m *ServiceInterface) SendRemoveExpiredLicenseEmail(ctaText string, ctaLink string, _a2 string, locale string, siteURL string) error {
ret := _m.Called(ctaText, ctaLink, _a2, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string, string) error); ok {
r0 = rf(ctaText, ctaLink, _a2, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendSignInChangeEmail provides a mock function with given fields: _a0, method, locale, siteURL
func (_m *ServiceInterface) SendSignInChangeEmail(_a0 string, method string, locale string, siteURL string) error {
ret := _m.Called(_a0, method, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
r0 = rf(_a0, method, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendUserAccessTokenAddedEmail provides a mock function with given fields: _a0, locale, siteURL
func (_m *ServiceInterface) SendUserAccessTokenAddedEmail(_a0 string, locale string, siteURL string) error {
ret := _m.Called(_a0, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = rf(_a0, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendVerifyEmail provides a mock function with given fields: userEmail, locale, siteURL, token, redirect
func (_m *ServiceInterface) SendVerifyEmail(userEmail string, locale string, siteURL string, token string, redirect string) error {
ret := _m.Called(userEmail, locale, siteURL, token, redirect)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string, string) error); ok {
r0 = rf(userEmail, locale, siteURL, token, redirect)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendWelcomeEmail provides a mock function with given fields: userID, _a1, verified, disableWelcomeEmail, locale, siteURL, redirect
func (_m *ServiceInterface) SendWelcomeEmail(userID string, _a1 string, verified bool, disableWelcomeEmail bool, locale string, siteURL string, redirect string) error {
ret := _m.Called(userID, _a1, verified, disableWelcomeEmail, locale, siteURL, redirect)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, bool, bool, string, string, string) error); ok {
r0 = rf(userID, _a1, verified, disableWelcomeEmail, locale, siteURL, redirect)
} else {
r0 = ret.Error(0)
}
return r0
}
// Stop provides a mock function with given fields:
func (_m *ServiceInterface) Stop() {
_m.Called()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package email
import (
"html"
"html/template"
"net/url"
"path/filepath"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type FieldRow struct {
Cells []*model.SlackAttachmentField
}
type EmailMessageAttachment struct {
model.SlackAttachment
Pretext template.HTML
Text template.HTML
FieldRows []FieldRow
}
func (es *Service) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string {
if strings.TrimSpace(post.Message) != "" || len(post.FileIds) == 0 {
return post.Message
}
// extract the filenames from their paths and determine what type of files are attached
infos, err := es.store.FileInfo().GetForPost(post.Id, true, false, true)
if err != nil {
mlog.Warn("Encountered error when getting files for notification message", mlog.String("post_id", post.Id), mlog.Err(err))
}
filenames := make([]string, len(infos))
onlyImages := true
for i, info := range infos {
if escaped, err := url.QueryUnescape(filepath.Base(info.Name)); err != nil {
// this should never error since filepath was escaped using url.QueryEscape
filenames[i] = escaped
} else {
filenames[i] = info.Name
}
onlyImages = onlyImages && info.IsImage()
}
props := map[string]any{"Filenames": strings.Join(filenames, ", ")}
if onlyImages {
return translateFunc("api.post.get_message_for_notification.images_sent", len(filenames), props)
}
return translateFunc("api.post.get_message_for_notification.files_sent", len(filenames), props)
}
func ProcessMessageAttachments(post *model.Post, siteURL string) []*EmailMessageAttachment {
emailMessageAttachments := []*EmailMessageAttachment{}
for _, messageAttachment := range post.Attachments() {
emailMessageAttachment := &EmailMessageAttachment{
SlackAttachment: *messageAttachment,
Pretext: prepareTextForEmail(messageAttachment.Pretext, siteURL),
Text: prepareTextForEmail(messageAttachment.Text, siteURL),
}
stripedTitle, err := utils.StripMarkdown(emailMessageAttachment.Title)
if err != nil {
mlog.Warn("Failed parse to markdown from messageatatchment title", mlog.String("post_id", post.Id), mlog.Err(err))
stripedTitle = ""
}
emailMessageAttachment.Title = stripedTitle
shortFieldRow := FieldRow{}
for i := range messageAttachment.Fields {
// Create a new instance to avoid altering the original pointer reference
// We update field value to parse markdown.
// If we do that on the original pointer, the rendered text in mattermost
// becomes invalid as its no longer a markdown string, but rather an HTML string.
field := &model.SlackAttachmentField{
Title: messageAttachment.Fields[i].Title,
Value: messageAttachment.Fields[i].Value,
Short: messageAttachment.Fields[i].Short,
}
if stringValue, ok := field.Value.(string); ok {
field.Value = prepareTextForEmail(stringValue, siteURL)
}
if !field.Short {
if len(shortFieldRow.Cells) > 0 {
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
shortFieldRow = FieldRow{}
}
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, FieldRow{[]*model.SlackAttachmentField{field}})
} else {
shortFieldRow.Cells = append(shortFieldRow.Cells, field)
if len(shortFieldRow.Cells) == 2 {
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
shortFieldRow = FieldRow{}
}
}
}
// collect any leftover short fields
if len(shortFieldRow.Cells) > 0 {
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
shortFieldRow = FieldRow{}
}
emailMessageAttachments = append(emailMessageAttachments, emailMessageAttachment)
}
return emailMessageAttachments
}
func prepareTextForEmail(text, siteURL string) template.HTML {
escapedText := html.EscapeString(text)
markdownText, err := utils.MarkdownToHTML(escapedText, siteURL)
if err != nil {
mlog.Warn("Encountered error while converting markdown to HTML", mlog.Err(err))
return template.HTML(text)
}
return template.HTML(markdownText)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package email
import (
"io"
"net/url"
"path"
"github.com/pkg/errors"
"github.com/throttled/throttled"
"github.com/throttled/throttled/store/memstore"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/users"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/templates"
)
const (
emailRateLimitingMemstoreSize = 65536
emailRateLimitingPerHour = 20
emailRateLimitingMaxBurst = 20
TokenTypePasswordRecovery = "password_recovery"
TokenTypeVerifyEmail = "verify_email"
TokenTypeTeamInvitation = "team_invitation"
TokenTypeGuestInvitation = "guest_invitation"
TokenTypeCWSAccess = "cws_access_token"
)
func condenseSiteURL(siteURL string) string {
parsedSiteURL, _ := url.Parse(siteURL)
if parsedSiteURL.Path == "" || parsedSiteURL.Path == "/" {
return parsedSiteURL.Host
}
return path.Join(parsedSiteURL.Host, parsedSiteURL.Path)
}
type Service struct {
config func() *model.Config
license func() *model.License
userService *users.UserService
store store.Store
templatesContainer *templates.Container
perHourEmailRateLimiter *throttled.GCRARateLimiter
perDayEmailRateLimiter *throttled.GCRARateLimiter
EmailBatching *EmailBatchingJob
}
type ServiceConfig struct {
ConfigFn func() *model.Config
LicenseFn func() *model.License
TemplatesContainer *templates.Container
UserService *users.UserService
Store store.Store
}
func NewService(config ServiceConfig) (*Service, error) {
if err := config.validate(); err != nil {
return nil, err
}
service := &Service{
config: config.ConfigFn,
templatesContainer: config.TemplatesContainer,
license: config.LicenseFn,
store: config.Store,
userService: config.UserService,
}
if err := service.setUpRateLimiters(); err != nil {
return nil, err
}
service.InitEmailBatching()
return service, nil
}
func (es *Service) Stop() {
mlog.Info("Shutting down Email batching service...")
if es.EmailBatching != nil {
es.EmailBatching.Stop()
}
}
func (c *ServiceConfig) validate() error {
if c.ConfigFn == nil || c.Store == nil || c.LicenseFn == nil || c.TemplatesContainer == nil {
return errors.New("invalid service config")
}
return nil
}
func (es *Service) setUpRateLimiters() error {
store, err := memstore.New(emailRateLimitingMemstoreSize)
if err != nil {
return errors.Wrap(err, "Unable to setup email rate limiting memstore.")
}
perHourQuota := throttled.RateQuota{
MaxRate: throttled.PerHour(emailRateLimitingPerHour),
MaxBurst: emailRateLimitingMaxBurst,
}
perDayQuota := throttled.RateQuota{
MaxRate: throttled.PerDay(1),
MaxBurst: 0,
}
perHourRateLimiter, err := throttled.NewGCRARateLimiter(store, perHourQuota)
if err != nil || perHourRateLimiter == nil {
return errors.Wrap(err, "Unable to setup email rate limiting GCRA rate limiter.")
}
perDayRateLimiter, err := throttled.NewGCRARateLimiter(store, perDayQuota)
if err != nil || perDayRateLimiter == nil {
return errors.Wrap(err, "Unable to setup per day email rate limiting GCRA rate limiter.")
}
es.perHourEmailRateLimiter = perHourRateLimiter
es.perDayEmailRateLimiter = perDayRateLimiter
return nil
}
type ServiceInterface interface {
GetPerDayEmailRateLimiter() *throttled.GCRARateLimiter
NewEmailTemplateData(locale string) templates.Data
SendEmailChangeVerifyEmail(newUserEmail, locale, siteURL, token string) error
SendEmailChangeEmail(oldEmail, newEmail, locale, siteURL string) error
SendVerifyEmail(userEmail, locale, siteURL, token, redirect string) error
SendSignInChangeEmail(email, method, locale, siteURL string) error
SendWelcomeEmail(userID string, email string, verified bool, disableWelcomeEmail bool, locale, siteURL, redirect string) error
SendCloudUpgradeConfirmationEmail(userEmail, name, trialEndDate, locale, siteURL, workspaceName string, isYearly bool, embeddedFiles map[string]io.Reader) error
SendCloudWelcomeEmail(userEmail, locale, teamInviteID, workSpaceName, dns, siteURL string) error
SendPasswordChangeEmail(email, method, locale, siteURL string) error
SendUserAccessTokenAddedEmail(email, locale, siteURL string) error
SendPasswordResetEmail(email string, token *model.Token, locale, siteURL string) (bool, error)
SendMfaChangeEmail(email string, activated bool, locale, siteURL string) error
SendInviteEmails(team *model.Team, senderName string, senderUserId string, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) error
SendGuestInviteEmails(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, message string, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) error
SendInviteEmailsToTeamAndChannels(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, message string, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) ([]*model.EmailInviteWithError, error)
SendDeactivateAccountEmail(email string, locale, siteURL string) error
SendNotificationMail(to, subject, htmlBody string) error
SendMailWithEmbeddedFiles(to, subject, htmlBody string, embeddedFiles map[string]io.Reader, messageID string, inReplyTo string, references string, category string) error
SendLicenseUpForRenewalEmail(email, name, locale, siteURL, ctaTitle, ctaLink, ctaText string, daysToExpiration int) error
SendPaymentFailedEmail(email string, locale string, failedPayment *model.FailedPayment, planName, siteURL string) (bool, error)
// Cloud delinquency email sequence
SendDelinquencyEmail7(email, locale, siteURL, planName string) error
SendDelinquencyEmail14(email, locale, siteURL, planName string) error
SendDelinquencyEmail30(email, locale, siteURL, planName string) error
SendDelinquencyEmail45(email, locale, siteURL, planName, delinquencyDate string) error
SendDelinquencyEmail60(email, locale, siteURL string) error
SendDelinquencyEmail75(email, locale, siteURL, planName, delinquencyDate string) error
SendDelinquencyEmail90(email, locale, siteURL string) error
SendNoCardPaymentFailedEmail(email string, locale string, siteURL string) error
SendRemoveExpiredLicenseEmail(ctaText, ctaLink, email, locale, siteURL string) error
AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError
GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string
InitEmailBatching()
SendChangeUsernameEmail(newUsername, email, locale, siteURL string) error
CreateVerifyEmailToken(userID string, newEmail string) (*model.Token, error)
Stop()
}
func (es *Service) GetPerDayEmailRateLimiter() *throttled.GCRARateLimiter {
return es.perDayEmailRateLimiter
}
func (es *Service) GetPerHourEmailRateLimiter() *throttled.GCRARateLimiter {
return es.perHourEmailRateLimiter
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package email
import (
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mail"
)
func (es *Service) mailServiceConfig(replyToAddress string) *mail.SMTPConfig {
emailSettings := es.config().EmailSettings
hostname := utils.GetHostnameFromSiteURL(*es.config().ServiceSettings.SiteURL)
if replyToAddress == "" {
replyToAddress = *emailSettings.ReplyToAddress
}
cfg := mail.SMTPConfig{
Hostname: hostname,
ConnectionSecurity: *emailSettings.ConnectionSecurity,
SkipServerCertificateVerification: *emailSettings.SkipServerCertificateVerification,
ServerName: *emailSettings.SMTPServer,
Server: *emailSettings.SMTPServer,
Port: *emailSettings.SMTPPort,
ServerTimeout: *emailSettings.SMTPServerTimeout,
Username: *emailSettings.SMTPUsername,
Password: *emailSettings.SMTPPassword,
EnableSMTPAuth: *emailSettings.EnableSMTPAuth,
SendEmailNotifications: *emailSettings.SendEmailNotifications,
FeedbackName: *emailSettings.FeedbackName,
FeedbackEmail: *emailSettings.FeedbackEmail,
ReplyToAddress: replyToAddress,
}
return &cfg
}
func (es *Service) GetTrackFlowStartedByRole(isFirstAdmin bool, isSystemAdmin bool) string {
trackFlowStartedByRole := "su"
if isFirstAdmin {
trackFlowStartedByRole = "fa"
} else if isSystemAdmin {
trackFlowStartedByRole = "sa"
}
return trackFlowStartedByRole
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"image"
"image/color/palette"
"image/draw"
"image/gif"
_ "image/jpeg"
"io"
"mime/multipart"
"net/http"
"path"
"github.com/disintegration/imaging"
_ "golang.org/x/image/webp"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
MaxEmojiFileSize = 1 << 19 // 512 KiB
MaxEmojiWidth = 128
MaxEmojiHeight = 128
MaxEmojiOriginalWidth = 1028
MaxEmojiOriginalHeight = 1028
)
func (a *App) CreateEmoji(c request.CTX, sessionUserId string, emoji *model.Emoji, multiPartImageData *multipart.Form) (*model.Emoji, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCustomEmoji {
return nil, model.NewAppError("UploadEmojiImage", "api.emoji.disabled.app_error", nil, "", http.StatusForbidden)
}
if *a.Config().FileSettings.DriverName == "" {
return nil, model.NewAppError("GetEmoji", "api.emoji.storage.app_error", nil, "", http.StatusForbidden)
}
// wipe the emoji id so that existing emojis can't get overwritten
emoji.Id = ""
// do our best to validate the emoji before committing anything to the DB so that we don't have to clean up
// orphaned files left over when validation fails later on
emoji.PreSave()
if appErr := emoji.IsValid(); appErr != nil {
return nil, appErr
}
if emoji.CreatorId != sessionUserId {
return nil, model.NewAppError("createEmoji", "api.emoji.create.other_user.app_error", nil, "", http.StatusForbidden)
}
if existingEmoji, err := a.Srv().Store().Emoji().GetByName(context.Background(), emoji.Name, true); err == nil && existingEmoji != nil {
return nil, model.NewAppError("createEmoji", "api.emoji.create.duplicate.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
imageData := multiPartImageData.File["image"]
if len(imageData) == 0 {
return nil, model.NewAppError("Context", "api.context.invalid_body_param.app_error", map[string]any{"Name": "createEmoji"}, "", http.StatusBadRequest)
}
if appErr := a.UploadEmojiImage(c, emoji.Id, imageData[0]); appErr != nil {
return nil, appErr
}
emoji, err := a.Srv().Store().Emoji().Save(emoji)
if err != nil {
return nil, model.NewAppError("CreateEmoji", "app.emoji.create.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
message := model.NewWebSocketEvent(model.WebsocketEventEmojiAdded, "", "", "", nil, "")
emojiJSON, jsonErr := json.Marshal(emoji)
if jsonErr != nil {
return nil, model.NewAppError("CreateEmoji", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("emoji", string(emojiJSON))
a.Publish(message)
return emoji, nil
}
func (a *App) GetEmojiList(c request.CTX, page, perPage int, sort string) ([]*model.Emoji, *model.AppError) {
list, err := a.Srv().Store().Emoji().GetList(page*perPage, perPage, sort)
if err != nil {
return nil, model.NewAppError("GetEmojiList", "app.emoji.get_list.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, nil
}
func (a *App) UploadEmojiImage(c request.CTX, id string, imageData *multipart.FileHeader) *model.AppError {
if !*a.Config().ServiceSettings.EnableCustomEmoji {
return model.NewAppError("UploadEmojiImage", "api.emoji.disabled.app_error", nil, "", http.StatusForbidden)
}
if *a.Config().FileSettings.DriverName == "" {
return model.NewAppError("UploadEmojiImage", "api.emoji.storage.app_error", nil, "", http.StatusForbidden)
}
file, err := imageData.Open()
if err != nil {
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.open.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
defer file.Close()
buf := bytes.NewBuffer(nil)
io.Copy(buf, file)
// make sure the file is an image and is within the required dimensions
config, _, err := image.DecodeConfig(bytes.NewReader(buf.Bytes()))
if err != nil {
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.image.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if config.Width > MaxEmojiOriginalWidth || config.Height > MaxEmojiOriginalHeight {
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.too_large.app_error", map[string]any{
"MaxWidth": MaxEmojiOriginalWidth,
"MaxHeight": MaxEmojiOriginalHeight,
}, "", http.StatusBadRequest)
}
if config.Width > MaxEmojiWidth || config.Height > MaxEmojiHeight {
data := buf.Bytes()
newbuf := bytes.NewBuffer(nil)
info, err := model.GetInfoForBytes(imageData.Filename, bytes.NewReader(data), len(data))
if err != nil {
return err
}
if info.MimeType == "image/gif" {
gif_data, err := gif.DecodeAll(bytes.NewReader(data))
if err != nil {
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_decode_error", nil, "", http.StatusBadRequest).Wrap(err)
}
resized_gif := resizeEmojiGif(gif_data)
if err := gif.EncodeAll(newbuf, resized_gif); err != nil {
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_encode_error", nil, "", http.StatusBadRequest).Wrap(err)
}
buf = newbuf
} else {
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.decode_error", nil, "", http.StatusBadRequest).Wrap(err)
}
resizedImg := resizeEmoji(img, config.Width, config.Height)
if err := a.ch.imgEncoder.EncodePNG(newbuf, resizedImg); err != nil {
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.encode_error", nil, "", http.StatusBadRequest).Wrap(err)
}
buf = newbuf
}
}
_, appErr := a.WriteFile(buf, getEmojiImagePath(id))
return appErr
}
func (a *App) DeleteEmoji(c request.CTX, emoji *model.Emoji) *model.AppError {
if err := a.Srv().Store().Emoji().Delete(emoji, model.GetMillis()); err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return model.NewAppError("DeleteEmoji", "app.emoji.delete.no_results", nil, "id="+emoji.Id, http.StatusNotFound).Wrap(err)
default:
return model.NewAppError("DeleteEmoji", "app.emoji.delete.app_error", nil, "id="+emoji.Id, http.StatusInternalServerError).Wrap(err)
}
}
a.deleteEmojiImage(emoji.Id)
a.deleteReactionsForEmoji(emoji.Name)
return nil
}
func (a *App) GetEmoji(c request.CTX, emojiId string) (*model.Emoji, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCustomEmoji {
return nil, model.NewAppError("GetEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusForbidden)
}
if *a.Config().FileSettings.DriverName == "" {
return nil, model.NewAppError("GetEmoji", "api.emoji.storage.app_error", nil, "", http.StatusForbidden)
}
emoji, err := a.Srv().Store().Emoji().Get(context.Background(), emojiId, true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return emoji, model.NewAppError("GetEmoji", "app.emoji.get.no_result", nil, "", http.StatusNotFound).Wrap(err)
default:
return emoji, model.NewAppError("GetEmoji", "app.emoji.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return emoji, nil
}
func (a *App) GetEmojiByName(c request.CTX, emojiName string) (*model.Emoji, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCustomEmoji {
return nil, model.NewAppError("GetEmojiByName", "api.emoji.disabled.app_error", nil, "", http.StatusForbidden)
}
if *a.Config().FileSettings.DriverName == "" {
return nil, model.NewAppError("GetEmojiByName", "api.emoji.storage.app_error", nil, "", http.StatusForbidden)
}
emoji, err := a.Srv().Store().Emoji().GetByName(context.Background(), emojiName, true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return emoji, model.NewAppError("GetEmojiByName", "app.emoji.get_by_name.no_result", nil, "", http.StatusNotFound).Wrap(err)
default:
return emoji, model.NewAppError("GetEmojiByName", "app.emoji.get_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return emoji, nil
}
func (a *App) GetMultipleEmojiByName(c request.CTX, names []string) ([]*model.Emoji, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCustomEmoji {
return nil, model.NewAppError("GetMultipleEmojiByName", "api.emoji.disabled.app_error", nil, "", http.StatusForbidden)
}
emoji, err := a.Srv().Store().Emoji().GetMultipleByName(names)
if err != nil {
return nil, model.NewAppError("GetMultipleEmojiByName", "app.emoji.get_by_name.app_error", nil, fmt.Sprintf("names=%v, %v", names, err.Error()), http.StatusInternalServerError)
}
return emoji, nil
}
func (a *App) GetEmojiImage(c request.CTX, emojiId string) ([]byte, string, *model.AppError) {
_, storeErr := a.Srv().Store().Emoji().Get(context.Background(), emojiId, true)
if storeErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(storeErr, &nfErr):
return nil, "", model.NewAppError("GetEmojiImage", "app.emoji.get.no_result", nil, "", http.StatusNotFound).Wrap(storeErr)
default:
return nil, "", model.NewAppError("GetEmojiImage", "app.emoji.get.app_error", nil, "", http.StatusInternalServerError).Wrap(storeErr)
}
}
img, appErr := a.ReadFile(getEmojiImagePath(emojiId))
if appErr != nil {
return nil, "", model.NewAppError("getEmojiImage", "api.emoji.get_image.read.app_error", nil, "", http.StatusNotFound).Wrap(appErr)
}
_, imageType, err := image.DecodeConfig(bytes.NewReader(img))
if err != nil {
return nil, "", model.NewAppError("getEmojiImage", "api.emoji.get_image.decode.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return img, imageType, nil
}
func (a *App) SearchEmoji(c request.CTX, name string, prefixOnly bool, limit int) ([]*model.Emoji, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCustomEmoji {
return nil, model.NewAppError("SearchEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusForbidden)
}
list, err := a.Srv().Store().Emoji().Search(name, prefixOnly, limit)
if err != nil {
return nil, model.NewAppError("SearchEmoji", "app.emoji.get_by_name.app_error", nil, "name="+name+", "+err.Error(), http.StatusInternalServerError)
}
return list, nil
}
// GetEmojiStaticURL returns a relative static URL for system default emojis,
// and the API route for custom ones. Errors if not found or if custom and deleted.
func (a *App) GetEmojiStaticURL(c request.CTX, emojiName string) (string, *model.AppError) {
subPath, _ := utils.GetSubpathFromConfig(a.Config())
if id, found := model.GetSystemEmojiId(emojiName); found {
return path.Join(subPath, "/static/emoji", id+".png"), nil
}
emoji, err := a.Srv().Store().Emoji().GetByName(context.Background(), emojiName, true)
if err == nil {
return path.Join(subPath, "/api/v4/emoji", emoji.Id, "image"), nil
}
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return "", model.NewAppError("GetEmojiStaticURL", "app.emoji.get_by_name.no_result", nil, "", http.StatusNotFound).Wrap(err)
default:
return "", model.NewAppError("GetEmojiStaticURL", "app.emoji.get_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
func resizeEmojiGif(gifImg *gif.GIF) *gif.GIF {
// Create a new RGBA image to hold the incremental frames.
firstFrame := gifImg.Image[0].Bounds()
b := image.Rect(0, 0, firstFrame.Dx(), firstFrame.Dy())
img := image.NewRGBA(b)
resizedImage := image.Image(nil)
// Resize each frame.
for index, frame := range gifImg.Image {
bounds := frame.Bounds()
draw.Draw(img, bounds, frame, bounds.Min, draw.Over)
resizedImage = resizeEmoji(img, firstFrame.Dx(), firstFrame.Dy())
gifImg.Image[index] = imageToPaletted(resizedImage)
}
// Set new gif width and height
gifImg.Config.Width = resizedImage.Bounds().Dx()
gifImg.Config.Height = resizedImage.Bounds().Dy()
return gifImg
}
func getEmojiImagePath(id string) string {
return "emoji/" + id + "/image"
}
func resizeEmoji(img image.Image, width int, height int) image.Image {
emojiWidth := float64(width)
emojiHeight := float64(height)
if emojiHeight <= MaxEmojiHeight && emojiWidth <= MaxEmojiWidth {
return img
}
return imaging.Fit(img, MaxEmojiWidth, MaxEmojiHeight, imaging.Lanczos)
}
func imageToPaletted(img image.Image) *image.Paletted {
b := img.Bounds()
pm := image.NewPaletted(b, palette.Plan9)
draw.FloydSteinberg.Draw(pm, b, img, image.Point{})
return pm
}
func (a *App) deleteEmojiImage(id string) {
if err := a.MoveFile(getEmojiImagePath(id), "emoji/"+id+"/image_deleted"); err != nil {
mlog.Warn("Failed to rename image when deleting emoji", mlog.String("emoji_id", id))
}
}
func (a *App) deleteReactionsForEmoji(emojiName string) {
if err := a.Srv().Store().Reaction().DeleteAllWithEmojiName(emojiName); err != nil {
mlog.Warn("Unable to delete reactions when deleting emoji", mlog.String("emoji_name", emojiName), mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
ejobs "github.com/mattermost/mattermost-server/v6/server/channels/einterfaces/jobs"
)
var accountMigrationInterface func(*App) einterfaces.AccountMigrationInterface
func RegisterAccountMigrationInterface(f func(*App) einterfaces.AccountMigrationInterface) {
accountMigrationInterface = f
}
var complianceInterface func(*App) einterfaces.ComplianceInterface
func RegisterComplianceInterface(f func(*App) einterfaces.ComplianceInterface) {
complianceInterface = f
}
var dataRetentionInterface func(*App) einterfaces.DataRetentionInterface
func RegisterDataRetentionInterface(f func(*App) einterfaces.DataRetentionInterface) {
dataRetentionInterface = f
}
var jobsDataRetentionJobInterface func(*Server) ejobs.DataRetentionJobInterface
func RegisterJobsDataRetentionJobInterface(f func(*Server) ejobs.DataRetentionJobInterface) {
jobsDataRetentionJobInterface = f
}
var jobsMessageExportJobInterface func(*Server) ejobs.MessageExportJobInterface
func RegisterJobsMessageExportJobInterface(f func(*Server) ejobs.MessageExportJobInterface) {
jobsMessageExportJobInterface = f
}
var jobsElasticsearchAggregatorInterface func(*Server) ejobs.ElasticsearchAggregatorInterface
func RegisterJobsElasticsearchAggregatorInterface(f func(*Server) ejobs.ElasticsearchAggregatorInterface) {
jobsElasticsearchAggregatorInterface = f
}
var jobsElasticsearchIndexerInterface func(*Server) ejobs.IndexerJobInterface
func RegisterJobsElasticsearchIndexerInterface(f func(*Server) ejobs.IndexerJobInterface) {
jobsElasticsearchIndexerInterface = f
}
var jobsLdapSyncInterface func(*App) ejobs.LdapSyncInterface
func RegisterJobsLdapSyncInterface(f func(*App) ejobs.LdapSyncInterface) {
jobsLdapSyncInterface = f
}
var ldapInterface func(*App) einterfaces.LdapInterface
func RegisterLdapInterface(f func(*App) einterfaces.LdapInterface) {
ldapInterface = f
}
var messageExportInterface func(*App) einterfaces.MessageExportInterface
func RegisterMessageExportInterface(f func(*App) einterfaces.MessageExportInterface) {
messageExportInterface = f
}
var cloudInterface func(*Server) einterfaces.CloudInterface
func RegisterCloudInterface(f func(*Server) einterfaces.CloudInterface) {
cloudInterface = f
}
var samlInterfaceNew func(*App) einterfaces.SamlInterface
func RegisterNewSamlInterface(f func(*App) einterfaces.SamlInterface) {
samlInterfaceNew = f
}
var notificationInterface func(*App) einterfaces.NotificationInterface
func RegisterNotificationInterface(f func(*App) einterfaces.NotificationInterface) {
notificationInterface = f
}
func (s *Server) initEnterprise() {
if cloudInterface != nil {
s.Cloud = cloudInterface(s)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
OneHourMillis = 60 * 60 * 1000
)
// NotifySessionsExpired is called periodically from the job server to notify any mobile sessions that have expired.
func (a *App) NotifySessionsExpired() error {
if !a.canSendPushNotifications() {
return nil
}
// Get all mobile sessions that expired within the last hour.
sessions, err := a.ch.srv.Store().Session().GetSessionsExpired(OneHourMillis, true, true)
if err != nil {
return model.NewAppError("NotifySessionsExpired", "app.session.analytics_session_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
msg := &model.PushNotification{
Version: model.PushMessageV2,
Type: model.PushTypeSession,
}
for _, session := range sessions {
tmpMessage := msg.DeepCopy()
tmpMessage.SetDeviceIdAndPlatform(session.DeviceId)
tmpMessage.AckId = model.NewId()
tmpMessage.Message = a.getSessionExpiredPushMessage(session)
errPush := a.sendToPushProxy(tmpMessage, session)
if errPush != nil {
a.NotificationsLog().Error("Notification error",
mlog.String("ackId", tmpMessage.AckId),
mlog.String("type", tmpMessage.Type),
mlog.String("userId", session.UserId),
mlog.String("deviceId", tmpMessage.DeviceId),
mlog.String("status", errPush.Error()),
)
continue
}
a.NotificationsLog().Info("Notification sent",
mlog.String("ackId", tmpMessage.AckId),
mlog.String("type", tmpMessage.Type),
mlog.String("userId", session.UserId),
mlog.String("deviceId", tmpMessage.DeviceId),
mlog.String("status", model.PushSendSuccess),
)
if a.Metrics() != nil {
a.Metrics().IncrementPostSentPush()
}
err = a.ch.srv.Store().Session().UpdateExpiredNotify(session.Id, true)
if err != nil {
mlog.Error("Failed to update ExpiredNotify flag", mlog.String("sessionid", session.Id), mlog.Err(err))
}
}
return nil
}
func (a *App) getSessionExpiredPushMessage(session *model.Session) string {
locale := model.DefaultLocale
user, err := a.GetUser(session.UserId)
if err == nil {
locale = user.Locale
}
T := i18n.GetUserTranslations(locale)
siteName := *a.Config().TeamSettings.SiteName
props := map[string]any{"siteName": siteName, "hoursCount": *a.Config().ServiceSettings.SessionLengthMobileInHours}
return T("api.push_notifications.session.expired", props)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"archive/zip"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/imports"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// We use this map to identify the exportable preferences.
// Here we link the preference category and name, to the name of the relevant field in the import struct.
var exportablePreferences = map[imports.ComparablePreference]string{{
Category: model.PreferenceCategoryTheme,
Name: "",
}: "Theme", {
Category: model.PreferenceCategoryAdvancedSettings,
Name: "feature_enabled_markdown_preview",
}: "UseMarkdownPreview", {
Category: model.PreferenceCategoryAdvancedSettings,
Name: "formatting",
}: "UseFormatting", {
Category: model.PreferenceCategorySidebarSettings,
Name: "show_unread_section",
}: "ShowUnreadSection", {
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameUseMilitaryTime,
}: "UseMilitaryTime", {
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameCollapseSetting,
}: "CollapsePreviews", {
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameMessageDisplay,
}: "MessageDisplay", {
Category: model.PreferenceCategoryDisplaySettings,
Name: "channel_display_mode",
}: "CollapseConsecutive", {
Category: model.PreferenceCategoryDisplaySettings,
Name: "collapse_consecutive_messages",
}: "ColorizeUsernames", {
Category: model.PreferenceCategoryDisplaySettings,
Name: "colorize_usernames",
}: "ChannelDisplayMode", {
Category: model.PreferenceCategoryTutorialSteps,
Name: "",
}: "TutorialStep", {
Category: model.PreferenceCategoryNotifications,
Name: model.PreferenceNameEmailInterval,
}: "EmailInterval",
}
func (a *App) BulkExport(ctx request.CTX, writer io.Writer, outPath string, job *model.Job, opts model.BulkExportOpts) *model.AppError {
var zipWr *zip.Writer
if opts.CreateArchive {
var err error
zipWr = zip.NewWriter(writer)
defer zipWr.Close()
writer, err = zipWr.Create("import.jsonl")
if err != nil {
return model.NewAppError("BulkExport", "app.export.zip_create.error",
nil, "err="+err.Error(), http.StatusInternalServerError)
}
}
if job != nil && job.Data == nil {
job.Data = make(model.StringMap)
}
ctx.Logger().Info("Bulk export: exporting version")
if err := a.exportVersion(writer); err != nil {
return err
}
ctx.Logger().Info("Bulk export: exporting teams")
teamNames, err := a.exportAllTeams(ctx, job, writer)
if err != nil {
return err
}
ctx.Logger().Info("Bulk export: exporting channels")
if err = a.exportAllChannels(ctx, job, writer, teamNames); err != nil {
return err
}
ctx.Logger().Info("Bulk export: exporting users")
if err = a.exportAllUsers(ctx, job, writer); err != nil {
return err
}
ctx.Logger().Info("Bulk export: exporting posts")
attachments, err := a.exportAllPosts(ctx, job, writer, opts.IncludeAttachments)
if err != nil {
return err
}
ctx.Logger().Info("Bulk export: exporting emoji")
emojiPaths, err := a.exportCustomEmoji(ctx, job, writer, outPath, "exported_emoji", !opts.CreateArchive)
if err != nil {
return err
}
ctx.Logger().Info("Bulk export: exporting direct channels")
if err = a.exportAllDirectChannels(ctx, job, writer); err != nil {
return err
}
ctx.Logger().Info("Bulk export: exporting direct posts")
directAttachments, err := a.exportAllDirectPosts(ctx, job, writer, opts.IncludeAttachments)
if err != nil {
return err
}
if opts.IncludeAttachments {
ctx.Logger().Info("Bulk export: exporting file attachments")
for _, attachment := range attachments {
if err := a.exportFile(outPath, *attachment.Path, zipWr); err != nil {
return err
}
}
for _, attachment := range directAttachments {
if err := a.exportFile(outPath, *attachment.Path, zipWr); err != nil {
return err
}
}
for _, emojiPath := range emojiPaths {
if err := a.exportFile(outPath, emojiPath, zipWr); err != nil {
return err
}
}
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "attachments_exported", len(attachments)+len(directAttachments)+len(emojiPaths))
}
return nil
}
func (a *App) exportWriteLine(w io.Writer, line *imports.LineImportData) *model.AppError {
b, err := json.Marshal(line)
if err != nil {
return model.NewAppError("BulkExport", "app.export.export_write_line.json_marshall.error", nil, "", http.StatusBadRequest).Wrap(err)
}
if _, err := w.Write(append(b, '\n')); err != nil {
return model.NewAppError("BulkExport", "app.export.export_write_line.io_writer.error", nil, "", http.StatusBadRequest).Wrap(err)
}
return nil
}
func (a *App) exportVersion(writer io.Writer) *model.AppError {
version := 1
info := &imports.VersionInfoImportData{
Generator: "mattermost-server",
Version: fmt.Sprintf("%s (%s, enterprise: %s)", model.CurrentVersion, model.BuildHash, model.BuildEnterpriseReady),
Created: time.Now().Format(time.RFC3339Nano),
}
versionLine := &imports.LineImportData{
Type: "version",
Version: &version,
Info: info,
}
return a.exportWriteLine(writer, versionLine)
}
func (a *App) exportAllTeams(ctx request.CTX, job *model.Job, writer io.Writer) (map[string]bool, *model.AppError) {
afterId := strings.Repeat("0", 26)
teamNames := make(map[string]bool)
cnt := 0
for {
teams, err := a.Srv().Store().Team().GetAllForExportAfter(1000, afterId)
if err != nil {
return nil, model.NewAppError("exportAllTeams", "app.team.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(teams) == 0 {
break
}
cnt += len(teams)
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "teams_exported", cnt)
for _, team := range teams {
afterId = team.Id
// Skip deleted.
if team.DeleteAt != 0 {
continue
}
teamNames[team.Name] = true
teamLine := ImportLineFromTeam(team)
if err := a.exportWriteLine(writer, teamLine); err != nil {
return nil, err
}
}
}
return teamNames, nil
}
func (a *App) exportAllChannels(ctx request.CTX, job *model.Job, writer io.Writer, teamNames map[string]bool) *model.AppError {
afterId := strings.Repeat("0", 26)
cnt := 0
for {
channels, err := a.Srv().Store().Channel().GetAllChannelsForExportAfter(1000, afterId)
if err != nil {
return model.NewAppError("exportAllChannels", "app.channel.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(channels) == 0 {
break
}
cnt += len(channels)
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "channels_exported", cnt)
for _, channel := range channels {
afterId = channel.Id
// Skip deleted.
if channel.DeleteAt != 0 {
continue
}
// Skip channels on deleted teams.
if ok := teamNames[channel.TeamName]; !ok {
continue
}
channelLine := ImportLineFromChannel(channel)
if err := a.exportWriteLine(writer, channelLine); err != nil {
return err
}
}
}
return nil
}
func (a *App) exportAllUsers(ctx request.CTX, job *model.Job, writer io.Writer) *model.AppError {
afterId := strings.Repeat("0", 26)
cnt := 0
for {
users, err := a.Srv().Store().User().GetAllAfter(1000, afterId)
if err != nil {
return model.NewAppError("exportAllUsers", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(users) == 0 {
break
}
cnt += len(users)
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "users_exported", cnt)
for _, user := range users {
afterId = user.Id
// Gathering here the exportable preferences to pass them on to ImportLineFromUser
exportedPrefs := make(map[string]*string)
allPrefs, err := a.GetPreferencesForUser(user.Id)
if err != nil {
return err
}
for _, pref := range allPrefs {
// We need to manage the special cases
// Here we manage Tutorial steps
if pref.Category == model.PreferenceCategoryTutorialSteps {
pref.Name = ""
// Then the email interval
} else if pref.Category == model.PreferenceCategoryNotifications && pref.Name == model.PreferenceNameEmailInterval {
switch pref.Value {
case model.PreferenceEmailIntervalNoBatchingSeconds:
pref.Value = model.PreferenceEmailIntervalImmediately
case model.PreferenceEmailIntervalFifteenAsSeconds:
pref.Value = model.PreferenceEmailIntervalFifteen
case model.PreferenceEmailIntervalHourAsSeconds:
pref.Value = model.PreferenceEmailIntervalHour
case "0":
pref.Value = ""
}
}
id, ok := exportablePreferences[imports.ComparablePreference{
Category: pref.Category,
Name: pref.Name,
}]
if ok {
prefPtr := pref.Value
if prefPtr != "" {
exportedPrefs[id] = &prefPtr
} else {
exportedPrefs[id] = nil
}
}
}
userLine := ImportLineFromUser(user, exportedPrefs)
userLine.User.NotifyProps = a.buildUserNotifyProps(user.NotifyProps)
// Do the Team Memberships.
members, err := a.buildUserTeamAndChannelMemberships(user.Id)
if err != nil {
return err
}
userLine.User.Teams = members
if err := a.exportWriteLine(writer, userLine); err != nil {
return err
}
}
}
return nil
}
func (a *App) buildUserTeamAndChannelMemberships(userID string) (*[]imports.UserTeamImportData, *model.AppError) {
var memberships []imports.UserTeamImportData
members, err := a.Srv().Store().Team().GetTeamMembersForExport(userID)
if err != nil {
return nil, model.NewAppError("buildUserTeamAndChannelMemberships", "app.team.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, member := range members {
// Skip deleted.
if member.DeleteAt != 0 {
continue
}
memberData := ImportUserTeamDataFromTeamMember(member)
// Do the Channel Memberships.
channelMembers, err := a.buildUserChannelMemberships(userID, member.TeamId)
if err != nil {
return nil, err
}
// Get the user theme
themePreference, nErr := a.Srv().Store().Preference().Get(member.UserId, model.PreferenceCategoryTheme, member.TeamId)
if nErr == nil {
memberData.Theme = &themePreference.Value
}
memberData.Channels = channelMembers
memberships = append(memberships, *memberData)
}
return &memberships, nil
}
func (a *App) buildUserChannelMemberships(userID string, teamID string) (*[]imports.UserChannelImportData, *model.AppError) {
members, nErr := a.Srv().Store().Channel().GetChannelMembersForExport(userID, teamID)
if nErr != nil {
return nil, model.NewAppError("buildUserChannelMemberships", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
category := model.PreferenceCategoryFavoriteChannel
preferences, err := a.GetPreferenceByCategoryForUser(userID, category)
if err != nil && err.StatusCode != http.StatusNotFound {
return nil, err
}
memberships := make([]imports.UserChannelImportData, len(members))
for i, member := range members {
memberships[i] = *ImportUserChannelDataFromChannelMemberAndPreferences(member, &preferences)
}
return &memberships, nil
}
func (a *App) buildUserNotifyProps(notifyProps model.StringMap) *imports.UserNotifyPropsImportData {
getProp := func(key string) *string {
if v, ok := notifyProps[key]; ok {
return &v
}
return nil
}
return &imports.UserNotifyPropsImportData{
Desktop: getProp(model.DesktopNotifyProp),
DesktopSound: getProp(model.DesktopSoundNotifyProp),
Email: getProp(model.EmailNotifyProp),
Mobile: getProp(model.PushNotifyProp),
MobilePushStatus: getProp(model.PushStatusNotifyProp),
ChannelTrigger: getProp(model.ChannelMentionsNotifyProp),
CommentsTrigger: getProp(model.CommentsNotifyProp),
MentionKeys: getProp(model.MentionKeysNotifyProp),
}
}
func (a *App) exportAllPosts(ctx request.CTX, job *model.Job, writer io.Writer, withAttachments bool) ([]imports.AttachmentImportData, *model.AppError) {
var attachments []imports.AttachmentImportData
afterId := strings.Repeat("0", 26)
var postProcessCount uint64
logCheckpoint := time.Now()
cnt := 0
for {
if time.Since(logCheckpoint) > 5*time.Minute {
ctx.Logger().Debug(fmt.Sprintf("Bulk Export: processed %d posts", postProcessCount))
logCheckpoint = time.Now()
}
posts, nErr := a.Srv().Store().Post().GetParentsForExportAfter(1000, afterId)
if nErr != nil {
return nil, model.NewAppError("exportAllPosts", "app.post.get_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
if len(posts) == 0 {
return attachments, nil
}
cnt += len(posts)
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "posts_exported", cnt)
for _, post := range posts {
afterId = post.Id
postProcessCount++
// Skip deleted.
if post.DeleteAt != 0 {
continue
}
postLine := ImportLineForPost(post)
replies, replyAttachments, err := a.buildPostReplies(ctx, post.Id, withAttachments)
if err != nil {
return nil, err
}
if withAttachments && len(replyAttachments) > 0 {
attachments = append(attachments, replyAttachments...)
}
postLine.Post.Replies = &replies
postLine.Post.Reactions = &[]imports.ReactionImportData{}
if post.HasReactions {
postLine.Post.Reactions, err = a.BuildPostReactions(ctx, post.Id)
if err != nil {
return nil, err
}
}
if len(post.FileIds) > 0 {
postAttachments, err := a.buildPostAttachments(post.Id)
if err != nil {
return nil, err
}
postLine.Post.Attachments = &postAttachments
if withAttachments && len(postAttachments) > 0 {
attachments = append(attachments, postAttachments...)
}
}
if err := a.exportWriteLine(writer, postLine); err != nil {
return nil, err
}
}
}
}
func (a *App) buildPostReplies(ctx request.CTX, postID string, withAttachments bool) ([]imports.ReplyImportData, []imports.AttachmentImportData, *model.AppError) {
var replies []imports.ReplyImportData
var attachments []imports.AttachmentImportData
replyPosts, nErr := a.Srv().Store().Post().GetRepliesForExport(postID)
if nErr != nil {
return nil, nil, model.NewAppError("buildPostReplies", "app.post.get_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
for _, reply := range replyPosts {
replyImportObject := ImportReplyFromPost(reply)
if reply.HasReactions {
var appErr *model.AppError
replyImportObject.Reactions, appErr = a.BuildPostReactions(ctx, reply.Id)
if appErr != nil {
return nil, nil, appErr
}
}
if len(reply.FileIds) > 0 {
postAttachments, appErr := a.buildPostAttachments(reply.Id)
if appErr != nil {
return nil, nil, appErr
}
replyImportObject.Attachments = &postAttachments
if withAttachments && len(postAttachments) > 0 {
attachments = append(attachments, postAttachments...)
}
}
replies = append(replies, *replyImportObject)
}
return replies, attachments, nil
}
func (a *App) BuildPostReactions(ctx request.CTX, postID string) (*[]ReactionImportData, *model.AppError) {
var reactionsOfPost []imports.ReactionImportData
reactions, nErr := a.Srv().Store().Reaction().GetForPost(postID, true)
if nErr != nil {
return nil, model.NewAppError("BuildPostReactions", "app.reaction.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
for _, reaction := range reactions {
user, err := a.Srv().Store().User().Get(context.Background(), reaction.UserId)
if err != nil {
var nfErr *store.ErrNotFound
if errors.As(err, &nfErr) { // this is a valid case, the user that reacted might've been deleted by now
ctx.Logger().Info("Skipping reactions by user since the entity doesn't exist anymore", mlog.String("user_id", reaction.UserId))
continue
}
return nil, model.NewAppError("BuildPostReactions", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
reactionsOfPost = append(reactionsOfPost, *ImportReactionFromPost(user, reaction))
}
return &reactionsOfPost, nil
}
func (a *App) buildPostAttachments(postID string) ([]imports.AttachmentImportData, *model.AppError) {
infos, nErr := a.Srv().Store().FileInfo().GetForPost(postID, false, false, false)
if nErr != nil {
return nil, model.NewAppError("buildPostAttachments", "app.file_info.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
attachments := make([]imports.AttachmentImportData, 0, len(infos))
for _, info := range infos {
attachments = append(attachments, imports.AttachmentImportData{Path: &info.Path})
}
return attachments, nil
}
func (a *App) exportCustomEmoji(c request.CTX, job *model.Job, writer io.Writer, outPath, exportDir string, exportFiles bool) ([]string, *model.AppError) {
var emojiPaths []string
pageNumber := 0
cnt := 0
for {
customEmojiList, err := a.GetEmojiList(c, pageNumber, 100, model.EmojiSortByName)
if err != nil {
return nil, err
}
if len(customEmojiList) == 0 {
break
}
cnt += len(customEmojiList)
updateJobProgress(c.Logger(), a.Srv().Store(), job, "emojis_exported", cnt)
pageNumber++
emojiPath := filepath.Join(*a.Config().FileSettings.Directory, "emoji")
pathToDir := filepath.Join(outPath, exportDir)
if exportFiles {
if _, err := os.Stat(pathToDir); os.IsNotExist(err) {
os.Mkdir(pathToDir, os.ModePerm)
}
}
for _, emoji := range customEmojiList {
emojiImagePath := filepath.Join(emojiPath, emoji.Id, "image")
filePath := filepath.Join(exportDir, emoji.Id, "image")
if exportFiles {
err := a.copyEmojiImages(emoji.Id, emojiImagePath, pathToDir)
if err != nil {
return nil, model.NewAppError("BulkExport", "app.export.export_custom_emoji.copy_emoji_images.error", nil, "err="+err.Error(), http.StatusBadRequest)
}
} else {
filePath = filepath.Join("emoji", emoji.Id, "image")
emojiPaths = append(emojiPaths, filePath)
}
emojiImportObject := ImportLineFromEmoji(emoji, filePath)
if err := a.exportWriteLine(writer, emojiImportObject); err != nil {
return nil, err
}
}
}
return emojiPaths, nil
}
// Copies emoji files from 'data/emoji' dir to 'exported_emoji' dir
func (a *App) copyEmojiImages(emojiId string, emojiImagePath string, pathToDir string) error {
fromPath, err := os.Open(emojiImagePath)
if fromPath == nil || err != nil {
return errors.New("Error reading " + emojiImagePath + "file")
}
defer fromPath.Close()
emojiDir := pathToDir + "/" + emojiId
if _, err = os.Stat(emojiDir); err != nil {
if !os.IsNotExist(err) {
return errors.Wrapf(err, "Error fetching file info of emoji directory %v", emojiDir)
}
if err = os.Mkdir(emojiDir, os.ModePerm); err != nil {
return errors.Wrapf(err, "Error creating emoji directory %v", emojiDir)
}
}
toPath, err := os.OpenFile(emojiDir+"/image", os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
return errors.New("Error creating the image file " + err.Error())
}
defer toPath.Close()
_, err = io.Copy(toPath, fromPath)
if err != nil {
return errors.New("Error copying emojis " + err.Error())
}
return nil
}
func (a *App) exportAllDirectChannels(ctx request.CTX, job *model.Job, writer io.Writer) *model.AppError {
afterId := strings.Repeat("0", 26)
cnt := 0
for {
channels, err := a.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, afterId)
if err != nil {
return model.NewAppError("exportAllDirectChannels", "app.channel.get_all_direct.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(channels) == 0 {
break
}
cnt += len(channels)
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "direct_channels_exported", cnt)
for _, channel := range channels {
afterId = channel.Id
// Skip if there are no active members in the channel
if len(*channel.Members) == 0 {
continue
}
// Skip deleted.
if channel.DeleteAt != 0 {
continue
}
favoritedBy, err := a.buildFavoritedByList(channel.Id)
if err != nil {
return err
}
channelLine := ImportLineFromDirectChannel(channel, favoritedBy)
if err := a.exportWriteLine(writer, channelLine); err != nil {
return err
}
}
}
return nil
}
func (a *App) buildFavoritedByList(channelID string) ([]string, *model.AppError) {
prefs, err := a.Srv().Store().Preference().GetCategoryAndName(model.PreferenceCategoryFavoriteChannel, channelID)
if err != nil {
return nil, model.NewAppError("buildFavoritedByList", "app.preference.get_category.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
userIDs := make([]string, 0, len(prefs))
for _, pref := range prefs {
if pref.Value != "true" {
continue
}
user, err := a.Srv().Store().User().Get(context.Background(), pref.UserId)
if err != nil {
return nil, model.NewAppError("buildFavoritedByList", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
userIDs = append(userIDs, user.Username)
}
return userIDs, nil
}
func (a *App) exportAllDirectPosts(ctx request.CTX, job *model.Job, writer io.Writer, withAttachments bool) ([]imports.AttachmentImportData, *model.AppError) {
var attachments []imports.AttachmentImportData
afterId := strings.Repeat("0", 26)
var postProcessCount uint64
logCheckpoint := time.Now()
cnt := 0
for {
if time.Since(logCheckpoint) > 5*time.Minute {
ctx.Logger().Debug(fmt.Sprintf("Bulk Export: processed %d direct posts", postProcessCount))
logCheckpoint = time.Now()
}
posts, err := a.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, afterId)
if err != nil {
return nil, model.NewAppError("exportAllDirectPosts", "app.post.get_direct_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(posts) == 0 {
break
}
cnt += len(posts)
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "direct_posts_exported", cnt)
for _, post := range posts {
afterId = post.Id
postProcessCount++
// Skip deleted.
if post.DeleteAt != 0 {
continue
}
// Handle attachments.
var postAttachments []imports.AttachmentImportData
var err *model.AppError
if len(post.FileIds) > 0 {
postAttachments, err = a.buildPostAttachments(post.Id)
if err != nil {
return nil, err
}
if withAttachments && len(postAttachments) > 0 {
attachments = append(attachments, postAttachments...)
}
}
// Do the Replies.
replies, replyAttachments, err := a.buildPostReplies(ctx, post.Id, withAttachments)
if err != nil {
return nil, err
}
if withAttachments && len(replyAttachments) > 0 {
attachments = append(attachments, replyAttachments...)
}
postLine := ImportLineForDirectPost(post)
postLine.DirectPost.Replies = &replies
if len(postAttachments) > 0 {
postLine.DirectPost.Attachments = &postAttachments
}
if err := a.exportWriteLine(writer, postLine); err != nil {
return nil, err
}
}
}
return attachments, nil
}
func (a *App) exportFile(outPath, filePath string, zipWr *zip.Writer) *model.AppError {
var wr io.Writer
var err error
rd, appErr := a.FileReader(filePath)
if appErr != nil {
return appErr
}
defer rd.Close()
if zipWr != nil {
wr, err = zipWr.CreateHeader(&zip.FileHeader{
Name: filepath.Join(model.ExportDataDir, filePath),
Method: zip.Store,
})
if err != nil {
return model.NewAppError("exportFileAttachment", "app.export.export_attachment.zip_create_header.error",
nil, "err="+err.Error(), http.StatusInternalServerError)
}
} else {
filePath = filepath.Join(outPath, model.ExportDataDir, filePath)
if err = os.MkdirAll(filepath.Dir(filePath), 0700); err != nil {
return model.NewAppError("exportFileAttachment", "app.export.export_attachment.mkdirall.error",
nil, "err="+err.Error(), http.StatusInternalServerError)
}
wr, err = os.Create(filePath)
if err != nil {
return model.NewAppError("exportFileAttachment", "app.export.export_attachment.create_file.error",
nil, "err="+err.Error(), http.StatusInternalServerError)
}
defer wr.(*os.File).Close()
}
if _, err := io.Copy(wr, rd); err != nil {
return model.NewAppError("exportFileAttachment", "app.export.export_attachment.copy_file.error",
nil, "err="+err.Error(), http.StatusInternalServerError)
}
return nil
}
func (a *App) ListExports() ([]string, *model.AppError) {
exports, appErr := a.ListDirectory(*a.Config().ExportSettings.Directory)
if appErr != nil {
return nil, appErr
}
results := make([]string, len(exports))
for i := range exports {
results[i] = filepath.Base(exports[i])
}
return results, nil
}
func (a *App) DeleteExport(name string) *model.AppError {
filePath := filepath.Join(*a.Config().ExportSettings.Directory, name)
if ok, err := a.FileExists(filePath); err != nil {
return err
} else if !ok {
return nil
}
return a.RemoveFile(filePath)
}
func updateJobProgress(logger mlog.LoggerIFace, store store.Store, job *model.Job, key string, value int) {
if job != nil {
job.Data[key] = strconv.Itoa(value)
if _, err2 := store.Job().UpdateOptimistically(job, model.JobStatusInProgress); err2 != nil {
logger.Warn("Failed to update job status", mlog.Err(err2))
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/imports"
)
func ImportLineFromTeam(team *model.TeamForExport) *imports.LineImportData {
return &imports.LineImportData{
Type: "team",
Team: &imports.TeamImportData{
Name: &team.Name,
DisplayName: &team.DisplayName,
Type: &team.Type,
Description: &team.Description,
AllowOpenInvite: &team.AllowOpenInvite,
Scheme: team.SchemeName,
},
}
}
func ImportLineFromChannel(channel *model.ChannelForExport) *imports.LineImportData {
return &imports.LineImportData{
Type: "channel",
Channel: &imports.ChannelImportData{
Team: &channel.TeamName,
Name: &channel.Name,
DisplayName: &channel.DisplayName,
Type: &channel.Type,
Header: &channel.Header,
Purpose: &channel.Purpose,
Scheme: channel.SchemeName,
},
}
}
func ImportLineFromDirectChannel(channel *model.DirectChannelForExport, favoritedBy []string) *imports.LineImportData {
channelMembers := *channel.Members
if len(channelMembers) == 1 {
channelMembers = []string{channelMembers[0], channelMembers[0]}
}
line := &imports.LineImportData{
Type: "direct_channel",
DirectChannel: &imports.DirectChannelImportData{
Header: &channel.Header,
Members: &channelMembers,
},
}
if len(favoritedBy) != 0 {
line.DirectChannel.FavoritedBy = &favoritedBy
}
return line
}
func ImportLineFromUser(user *model.User, exportedPrefs map[string]*string) *imports.LineImportData {
// Bulk Importer doesn't accept "empty string" for AuthService.
var authService *string
if user.AuthService != "" {
authService = &user.AuthService
}
return &imports.LineImportData{
Type: "user",
User: &imports.UserImportData{
Username: &user.Username,
Email: &user.Email,
AuthService: authService,
AuthData: user.AuthData,
Nickname: &user.Nickname,
FirstName: &user.FirstName,
LastName: &user.LastName,
Position: &user.Position,
Roles: &user.Roles,
Locale: &user.Locale,
UseMarkdownPreview: exportedPrefs["UseMarkdownPreview"],
UseFormatting: exportedPrefs["UseFormatting"],
ShowUnreadSection: exportedPrefs["ShowUnreadSection"],
Theme: exportedPrefs["Theme"],
UseMilitaryTime: exportedPrefs["UseMilitaryTime"],
CollapsePreviews: exportedPrefs["CollapsePreviews"],
MessageDisplay: exportedPrefs["MessageDisplay"],
ColorizeUsernames: exportedPrefs["ColorizeUsernames"],
ChannelDisplayMode: exportedPrefs["ChannelDisplayMode"],
TutorialStep: exportedPrefs["TutorialStep"],
EmailInterval: exportedPrefs["EmailInterval"],
DeleteAt: &user.DeleteAt,
},
}
}
func ImportUserTeamDataFromTeamMember(member *model.TeamMemberForExport) *imports.UserTeamImportData {
rolesList := strings.Fields(member.Roles)
if member.SchemeAdmin {
rolesList = append(rolesList, model.TeamAdminRoleId)
}
if member.SchemeUser {
rolesList = append(rolesList, model.TeamUserRoleId)
}
if member.SchemeGuest {
rolesList = append(rolesList, model.TeamGuestRoleId)
}
roles := strings.Join(rolesList, " ")
return &imports.UserTeamImportData{
Name: &member.TeamName,
Roles: &roles,
}
}
func ImportUserChannelDataFromChannelMemberAndPreferences(member *model.ChannelMemberForExport, preferences *model.Preferences) *imports.UserChannelImportData {
rolesList := strings.Fields(member.Roles)
if member.SchemeAdmin {
rolesList = append(rolesList, model.ChannelAdminRoleId)
}
if member.SchemeUser {
rolesList = append(rolesList, model.ChannelUserRoleId)
}
if member.SchemeGuest {
rolesList = append(rolesList, model.ChannelGuestRoleId)
}
props := member.NotifyProps
notifyProps := imports.UserChannelNotifyPropsImportData{}
desktop, exist := props[model.DesktopNotifyProp]
if exist {
notifyProps.Desktop = &desktop
}
mobile, exist := props[model.PushNotifyProp]
if exist {
notifyProps.Mobile = &mobile
}
markUnread, exist := props[model.MarkUnreadNotifyProp]
if exist {
notifyProps.MarkUnread = &markUnread
}
favorite := false
for _, preference := range *preferences {
if member.ChannelId == preference.Name {
favorite = true
}
}
roles := strings.Join(rolesList, " ")
return &imports.UserChannelImportData{
Name: &member.ChannelName,
Roles: &roles,
NotifyProps: ¬ifyProps,
Favorite: &favorite,
MentionCount: &member.MentionCount,
MentionCountRoot: &member.MentionCountRoot,
UrgentMentionCount: &member.UrgentMentionCount,
MsgCount: &member.MsgCount,
MsgCountRoot: &member.MsgCountRoot,
LastViewedAt: &member.LastViewedAt,
}
}
func ImportLineForPost(post *model.PostForExport) *imports.LineImportData {
return &imports.LineImportData{
Type: "post",
Post: &imports.PostImportData{
Team: &post.TeamName,
Channel: &post.ChannelName,
User: &post.Username,
Type: &post.Type,
Message: &post.Message,
Props: &post.Props,
CreateAt: &post.CreateAt,
EditAt: &post.EditAt,
},
}
}
func ImportLineForDirectPost(post *model.DirectPostForExport) *imports.LineImportData {
channelMembers := *post.ChannelMembers
if len(channelMembers) == 1 {
channelMembers = []string{channelMembers[0], channelMembers[0]}
}
return &imports.LineImportData{
Type: "direct_post",
DirectPost: &imports.DirectPostImportData{
ChannelMembers: &channelMembers,
User: &post.User,
Type: &post.Type,
Message: &post.Message,
Props: &post.Props,
CreateAt: &post.CreateAt,
EditAt: &post.EditAt,
},
}
}
func ImportReplyFromPost(post *model.ReplyForExport) *imports.ReplyImportData {
return &imports.ReplyImportData{
User: &post.Username,
Type: &post.Type,
Message: &post.Message,
CreateAt: &post.CreateAt,
EditAt: &post.EditAt,
}
}
func ImportReactionFromPost(user *model.User, reaction *model.Reaction) *imports.ReactionImportData {
return &imports.ReactionImportData{
User: &user.Username,
EmojiName: &reaction.EmojiName,
CreateAt: &reaction.CreateAt,
}
}
func ImportLineFromEmoji(emoji *model.Emoji, filePath string) *imports.LineImportData {
return &imports.LineImportData{
Type: "emoji",
Emoji: &imports.EmojiImportData{
Name: &emoji.Name,
Image: &filePath,
},
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"archive/tar"
"compress/gzip"
"io"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// extractTarGz takes in an io.Reader containing the bytes for a .tar.gz file and
// a destination string to extract to.
func extractTarGz(gzipStream io.Reader, dst string) error {
if dst == "" {
return errors.New("no destination path provided")
}
uncompressedStream, err := gzip.NewReader(gzipStream)
if err != nil {
return errors.Wrap(err, "failed to initialize gzip reader")
}
defer uncompressedStream.Close()
tarReader := tar.NewReader(uncompressedStream)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
} else if err != nil {
return errors.Wrap(err, "failed to read next file from archive")
}
// Preemptively check type flag to avoid reporting a misleading error in
// trying to sanitize the header name.
switch header.Typeflag {
case tar.TypeDir:
case tar.TypeReg:
default:
mlog.Warn("skipping unsupported header type on extracting tar file", mlog.String("header_type", string(header.Typeflag)), mlog.String("header_name", header.Name))
continue
}
// filepath.HasPrefix is deprecated, so we just use strings.HasPrefix to ensure
// the target path remains rooted at dst and has no `../` escaping outside.
path := filepath.Join(dst, header.Name)
if !strings.HasPrefix(path, dst) {
return errors.Errorf("failed to sanitize path %s", header.Name)
}
switch header.Typeflag {
case tar.TypeDir:
if err := os.Mkdir(path, 0744); err != nil && !os.IsExist(err) {
return err
}
case tar.TypeReg:
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0744); err != nil {
return err
}
copyFile := func() error {
outFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
return err
}
defer outFile.Close()
if _, err := io.Copy(outFile, tarReader); err != nil {
return err
}
return nil
}
if err := copyFile(); err != nil {
return err
}
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package featureflag
import (
"math"
"reflect"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/splitio/go-client/v6/splitio/client"
"github.com/splitio/go-client/v6/splitio/conf"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SyncParams struct {
ServerID string
SplitKey string
SyncIntervalSeconds int
Log *mlog.Logger
Attributes map[string]any
}
type Synchronizer struct {
SyncParams
client *client.SplitClient
stop chan struct{}
stopped chan struct{}
}
var featureNames = getStructFields(model.FeatureFlags{})
func NewSynchronizer(params SyncParams) (*Synchronizer, error) {
cfg := conf.Default()
if params.Log != nil {
cfg.Logger = &splitLogger{wrappedLog: params.Log.With(mlog.String("service", "split"))}
} else {
cfg.LoggerConfig.LogLevel = math.MinInt32
}
factory, err := client.NewSplitFactory(params.SplitKey, cfg)
if err != nil {
return nil, errors.Wrap(err, "unable to create split factory")
}
return &Synchronizer{
SyncParams: params,
client: factory.Client(),
stop: make(chan struct{}),
stopped: make(chan struct{}),
}, nil
}
// EnsureReady blocks until the synchronizer is ready to update feature flag values
func (f *Synchronizer) EnsureReady() error {
if err := f.client.BlockUntilReady(10); err != nil {
return errors.Wrap(err, "split.io client could not initialize")
}
return nil
}
func (f *Synchronizer) UpdateFeatureFlagValues(base model.FeatureFlags) model.FeatureFlags {
featuresMap := f.client.Treatments(f.ServerID, featureNames, f.Attributes)
ffm := featureFlagsFromMap(featuresMap, base)
return ffm
}
func (f *Synchronizer) Close() {
f.client.Destroy()
}
// featureFlagsFromMap sets the feature flags from a map[string]string.
// It starts with baseFeatureFlags and only sets values that are
// given by the upstream management system.
// Makes the assumption that all feature flags are strings or booleans.
// Strings are converted to booleans by considering case insensitive "on" or any value considered by strconv.ParseBool as true and any other value as false.
func featureFlagsFromMap(featuresMap map[string]string, baseFeatureFlags model.FeatureFlags) model.FeatureFlags {
refStruct := reflect.ValueOf(&baseFeatureFlags).Elem()
for fieldName, fieldValue := range featuresMap {
refField := refStruct.FieldByName(fieldName)
// "control" is returned by split.io if the treatment is not found, in this case we should use the default value.
if !refField.IsValid() || !refField.CanSet() || fieldValue == "control" {
continue
}
switch refField.Type().Kind() {
case reflect.Bool:
parsedBoolValue, _ := strconv.ParseBool(fieldValue)
refField.Set(reflect.ValueOf(strings.ToLower(fieldValue) == "on" || parsedBoolValue))
default:
refField.Set(reflect.ValueOf(fieldValue))
}
}
return baseFeatureFlags
}
func getStructFields(s any) []string {
structType := reflect.TypeOf(s)
fieldNames := make([]string, 0, structType.NumField())
for i := 0; i < structType.NumField(); i++ {
fieldNames = append(fieldNames, structType.Field(i).Name)
}
return fieldNames
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package featureflag
import (
"fmt"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type splitLogger struct {
wrappedLog *mlog.Logger
}
func (s *splitLogger) Error(msg ...any) {
s.wrappedLog.Error(fmt.Sprint(msg...))
}
func (s *splitLogger) Warning(msg ...any) {
s.wrappedLog.Warn(fmt.Sprint(msg...))
}
// Ignoring more verbose messages from split
func (s *splitLogger) Info(msg ...any) {
//s.wrappedLog.Info(fmt.Sprint(msg...))
}
func (s *splitLogger) Debug(msg ...any) {
//s.wrappedLog.Debug(fmt.Sprint(msg...))
}
func (s *splitLogger) Verbose(msg ...any) {
//s.wrappedLog.Info(fmt.Sprint(msg...))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"archive/zip"
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"fmt"
"image"
"io"
"math"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/imaging"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/services/docextractor"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/pkg/errors"
)
const (
imageThumbnailWidth = 120
imageThumbnailHeight = 100
imagePreviewWidth = 1920
miniPreviewImageWidth = 16
miniPreviewImageHeight = 16
jpegEncQuality = 90
maxUploadInitialBufferSize = 1024 * 1024 // 1MB
maxContentExtractionSize = 1024 * 1024 // 1MB
)
// Ensure fileInfo service wrapper implements `product.FileInfoStoreService`
var _ product.FileInfoStoreService = (*fileInfoWrapper)(nil)
// fileInfoWrapper implements `product.FileInfoStoreService` for use by products.
type fileInfoWrapper struct {
srv *Server
}
func (f *fileInfoWrapper) GetFileInfo(fileID string) (*model.FileInfo, *model.AppError) {
return f.srv.getFileInfo(fileID)
}
func (a *App) FileBackend() filestore.FileBackend {
return a.ch.filestore
}
func (a *App) CheckMandatoryS3Fields(settings *model.FileSettings) *model.AppError {
fileBackendSettings := settings.ToFileBackendSettings(false, false)
err := fileBackendSettings.CheckMandatoryS3Fields()
if err != nil {
return model.NewAppError("CheckMandatoryS3Fields", "api.admin.test_s3.missing_s3_bucket", nil, "", http.StatusBadRequest).Wrap(err)
}
return nil
}
func connectionTestErrorToAppError(connTestErr error) *model.AppError {
switch err := connTestErr.(type) {
case *filestore.S3FileBackendAuthError:
return model.NewAppError("TestConnection", "api.file.test_connection_s3_auth.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
case *filestore.S3FileBackendNoBucketError:
return model.NewAppError("TestConnection", "api.file.test_connection_s3_bucket_does_not_exist.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
default:
return model.NewAppError("TestConnection", "api.file.test_connection.app_error", nil, "", http.StatusInternalServerError).Wrap(connTestErr)
}
}
func (a *App) TestFileStoreConnection() *model.AppError {
nErr := a.FileBackend().TestConnection()
if nErr != nil {
return connectionTestErrorToAppError(nErr)
}
return nil
}
func (a *App) TestFileStoreConnectionWithConfig(cfg *model.FileSettings) *model.AppError {
license := a.Srv().License()
insecure := a.Config().ServiceSettings.EnableInsecureOutgoingConnections
backend, err := filestore.NewFileBackend(cfg.ToFileBackendSettings(license != nil && *license.Features.Compliance, insecure != nil && *insecure))
if err != nil {
return model.NewAppError("FileBackend", "api.file.no_driver.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
nErr := backend.TestConnection()
if nErr != nil {
return connectionTestErrorToAppError(nErr)
}
return nil
}
func (a *App) ReadFile(path string) ([]byte, *model.AppError) {
return a.ch.srv.ReadFile(path)
}
func (s *Server) fileReader(path string) (filestore.ReadCloseSeeker, *model.AppError) {
result, nErr := s.FileBackend().Reader(path)
if nErr != nil {
return nil, model.NewAppError("FileReader", "api.file.file_reader.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return result, nil
}
// Caller must close the first return value
func (a *App) FileReader(path string) (filestore.ReadCloseSeeker, *model.AppError) {
return a.Srv().fileReader(path)
}
func (a *App) FileExists(path string) (bool, *model.AppError) {
return a.Srv().fileExists(path)
}
func (s *Server) fileExists(path string) (bool, *model.AppError) {
result, nErr := s.FileBackend().FileExists(path)
if nErr != nil {
return false, model.NewAppError("FileExists", "api.file.file_exists.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return result, nil
}
func (a *App) FileSize(path string) (int64, *model.AppError) {
size, nErr := a.FileBackend().FileSize(path)
if nErr != nil {
return 0, model.NewAppError("FileSize", "api.file.file_size.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return size, nil
}
func (a *App) FileModTime(path string) (time.Time, *model.AppError) {
modTime, nErr := a.FileBackend().FileModTime(path)
if nErr != nil {
return time.Time{}, model.NewAppError("FileModTime", "api.file.file_mod_time.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return modTime, nil
}
func (a *App) MoveFile(oldPath, newPath string) *model.AppError {
nErr := a.FileBackend().MoveFile(oldPath, newPath)
if nErr != nil {
return model.NewAppError("MoveFile", "api.file.move_file.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return nil
}
func (a *App) WriteFileContext(ctx context.Context, fr io.Reader, path string) (int64, *model.AppError) {
return a.Srv().writeFileContext(ctx, fr, path)
}
func (a *App) WriteFile(fr io.Reader, path string) (int64, *model.AppError) {
return a.Srv().writeFile(fr, path)
}
func (s *Server) writeFile(fr io.Reader, path string) (int64, *model.AppError) {
result, nErr := s.FileBackend().WriteFile(fr, path)
if nErr != nil {
return result, model.NewAppError("WriteFile", "api.file.write_file.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return result, nil
}
func (s *Server) writeFileContext(ctx context.Context, fr io.Reader, path string) (int64, *model.AppError) {
// Check if we can provide a custom context, otherwise just use the default method.
written, err := filestore.TryWriteFileContext(s.FileBackend(), ctx, fr, path)
if err != nil {
return written, model.NewAppError("WriteFile", "api.file.write_file.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return written, nil
}
func (a *App) AppendFile(fr io.Reader, path string) (int64, *model.AppError) {
result, nErr := a.FileBackend().AppendFile(fr, path)
if nErr != nil {
return result, model.NewAppError("AppendFile", "api.file.append_file.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return result, nil
}
func (a *App) RemoveFile(path string) *model.AppError {
return a.Srv().removeFile(path)
}
func (s *Server) removeFile(path string) *model.AppError {
nErr := s.FileBackend().RemoveFile(path)
if nErr != nil {
return model.NewAppError("RemoveFile", "api.file.remove_file.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return nil
}
func (a *App) ListDirectory(path string) ([]string, *model.AppError) {
return a.Srv().listDirectory(path, false)
}
func (a *App) ListDirectoryRecursively(path string) ([]string, *model.AppError) {
return a.Srv().listDirectory(path, true)
}
func (s *Server) listDirectory(path string, recursion bool) ([]string, *model.AppError) {
backend := s.FileBackend()
var paths []string
var nErr error
if recursion {
paths, nErr = backend.ListDirectoryRecursively(path)
} else {
paths, nErr = backend.ListDirectory(path)
}
if nErr != nil {
return nil, model.NewAppError("ListDirectory", "api.file.list_directory.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return paths, nil
}
func (a *App) RemoveDirectory(path string) *model.AppError {
nErr := a.FileBackend().RemoveDirectory(path)
if nErr != nil {
return model.NewAppError("RemoveDirectory", "api.file.remove_directory.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return nil
}
func (a *App) getInfoForFilename(post *model.Post, teamID, channelID, userID, oldId, filename string) *model.FileInfo {
name, _ := url.QueryUnescape(filename)
pathPrefix := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/", teamID, channelID, userID, oldId)
path := pathPrefix + name
// Open the file and populate the fields of the FileInfo
data, err := a.ReadFile(path)
if err != nil {
mlog.Error(
"File not found when migrating post to use FileInfos",
mlog.String("post_id", post.Id),
mlog.String("filename", filename),
mlog.String("path", path),
mlog.Err(err),
)
return nil
}
info, err := model.GetInfoForBytes(name, bytes.NewReader(data), len(data))
if err != nil {
mlog.Warn(
"Unable to fully decode file info when migrating post to use FileInfos",
mlog.String("post_id", post.Id),
mlog.String("filename", filename),
mlog.Err(err),
)
}
// Generate a new ID because with the old system, you could very rarely get multiple posts referencing the same file
info.Id = model.NewId()
info.CreatorId = post.UserId
info.PostId = post.Id
info.ChannelId = post.ChannelId
info.CreateAt = post.CreateAt
info.UpdateAt = post.UpdateAt
info.Path = path
if info.IsImage() && !info.IsSvg() {
nameWithoutExtension := name[:strings.LastIndex(name, ".")]
info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview." + getFileExtFromMimeType(info.MimeType)
info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb." + getFileExtFromMimeType(info.MimeType)
}
return info
}
func (a *App) findTeamIdForFilename(post *model.Post, id, filename string) string {
name, _ := url.QueryUnescape(filename)
// This post is in a direct channel so we need to figure out what team the files are stored under.
teams, err := a.Srv().Store().Team().GetTeamsByUserId(post.UserId)
if err != nil {
mlog.Error("Unable to get teams when migrating post to use FileInfo", mlog.Err(err), mlog.String("post_id", post.Id))
return ""
}
if len(teams) == 1 {
// The user has only one team so the post must've been sent from it
return teams[0].Id
}
for _, team := range teams {
path := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/%s", team.Id, post.ChannelId, post.UserId, id, name)
if ok, err := a.FileExists(path); ok && err == nil {
// Found the team that this file was posted from
return team.Id
}
}
return ""
}
var fileMigrationLock sync.Mutex
var oldFilenameMatchExp *regexp.Regexp = regexp.MustCompile(`^\/([a-z\d]{26})\/([a-z\d]{26})\/([a-z\d]{26})\/([^\/]+)$`)
// Parse the path from the Filename of the form /{channelID}/{userID}/{uid}/{nameWithExtension}
func parseOldFilenames(filenames []string, channelID, userID string) [][]string {
parsed := [][]string{}
for _, filename := range filenames {
matches := oldFilenameMatchExp.FindStringSubmatch(filename)
if len(matches) != 5 {
mlog.Error("Failed to parse old Filename", mlog.String("filename", filename))
continue
}
if matches[1] != channelID {
mlog.Error("ChannelId in Filename does not match", mlog.String("channel_id", channelID), mlog.String("matched", matches[1]))
} else if matches[2] != userID {
mlog.Error("UserId in Filename does not match", mlog.String("user_id", userID), mlog.String("matched", matches[2]))
} else {
parsed = append(parsed, matches[1:])
}
}
return parsed
}
// Creates and stores FileInfos for a post created before the FileInfos table existed.
func (a *App) MigrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo {
if len(post.Filenames) == 0 {
mlog.Warn("Unable to migrate post to use FileInfos with an empty Filenames field", mlog.String("post_id", post.Id))
return []*model.FileInfo{}
}
channel, errCh := a.Srv().Store().Channel().Get(post.ChannelId, true)
// There's a weird bug that rarely happens where a post ends up with duplicate Filenames so remove those
filenames := utils.RemoveDuplicatesFromStringArray(post.Filenames)
if errCh != nil {
mlog.Error(
"Unable to get channel when migrating post to use FileInfos",
mlog.String("post_id", post.Id),
mlog.String("channel_id", post.ChannelId),
mlog.Err(errCh),
)
return []*model.FileInfo{}
}
// Parse and validate filenames before further processing
parsedFilenames := parseOldFilenames(filenames, post.ChannelId, post.UserId)
if len(parsedFilenames) == 0 {
mlog.Error("Unable to parse filenames")
return []*model.FileInfo{}
}
// Find the team that was used to make this post since its part of the file path that isn't saved in the Filename
var teamID string
if channel.TeamId == "" {
// This post was made in a cross-team DM channel, so we need to find where its files were saved
teamID = a.findTeamIdForFilename(post, parsedFilenames[0][2], parsedFilenames[0][3])
} else {
teamID = channel.TeamId
}
// Create FileInfo objects for this post
infos := make([]*model.FileInfo, 0, len(filenames))
if teamID == "" {
mlog.Error(
"Unable to find team id for files when migrating post to use FileInfos",
mlog.String("filenames", strings.Join(filenames, ",")),
mlog.String("post_id", post.Id),
)
} else {
for _, parsed := range parsedFilenames {
info := a.getInfoForFilename(post, teamID, parsed[0], parsed[1], parsed[2], parsed[3])
if info == nil {
continue
}
infos = append(infos, info)
}
}
// Lock to prevent only one migration thread from trying to update the post at once, preventing duplicate FileInfos from being created
fileMigrationLock.Lock()
defer fileMigrationLock.Unlock()
result, nErr := a.Srv().Store().Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", a.Config().GetSanitizeOptions())
if nErr != nil {
mlog.Error("Unable to get post when migrating post to use FileInfos", mlog.Err(nErr), mlog.String("post_id", post.Id))
return []*model.FileInfo{}
}
if newPost := result.Posts[post.Id]; len(newPost.Filenames) != len(post.Filenames) {
// Another thread has already created FileInfos for this post, so just return those
var fileInfos []*model.FileInfo
fileInfos, nErr = a.Srv().Store().FileInfo().GetForPost(post.Id, true, false, false)
if nErr != nil {
mlog.Error("Unable to get FileInfos for migrated post", mlog.Err(nErr), mlog.String("post_id", post.Id))
return []*model.FileInfo{}
}
mlog.Debug("Post already migrated to use FileInfos", mlog.String("post_id", post.Id))
return fileInfos
}
mlog.Debug("Migrating post to use FileInfos", mlog.String("post_id", post.Id))
savedInfos := make([]*model.FileInfo, 0, len(infos))
fileIDs := make([]string, 0, len(filenames))
for _, info := range infos {
if _, nErr = a.Srv().Store().FileInfo().Save(info); nErr != nil {
mlog.Error(
"Unable to save file info when migrating post to use FileInfos",
mlog.String("post_id", post.Id),
mlog.String("file_info_id", info.Id),
mlog.String("file_info_path", info.Path),
mlog.Err(nErr),
)
continue
}
savedInfos = append(savedInfos, info)
fileIDs = append(fileIDs, info.Id)
}
// Copy and save the updated post
newPost := post.Clone()
newPost.Filenames = []string{}
newPost.FileIds = fileIDs
// Update Posts to clear Filenames and set FileIds
if _, nErr = a.Srv().Store().Post().Update(newPost, post); nErr != nil {
mlog.Error(
"Unable to save migrated post when migrating to use FileInfos",
mlog.String("new_file_ids", strings.Join(newPost.FileIds, ",")),
mlog.String("old_filenames", strings.Join(post.Filenames, ",")),
mlog.String("post_id", post.Id),
mlog.Err(nErr),
)
return []*model.FileInfo{}
}
return savedInfos
}
func (a *App) GeneratePublicLink(siteURL string, info *model.FileInfo) string {
hash := GeneratePublicLinkHash(info.Id, *a.Config().FileSettings.PublicLinkSalt)
return fmt.Sprintf("%s/files/%v/public?h=%s", siteURL, info.Id, hash)
}
func GeneratePublicLinkHash(fileID, salt string) string {
hash := sha256.New()
hash.Write([]byte(salt))
hash.Write([]byte(fileID))
return base64.RawURLEncoding.EncodeToString(hash.Sum(nil))
}
// UploadFile uploads a single file in form of a completely constructed byte array for a channel.
func (a *App) UploadFile(c request.CTX, data []byte, channelID string, filename string) (*model.FileInfo, *model.AppError) {
_, err := a.GetChannel(c, channelID)
if err != nil && channelID != "" {
return nil, model.NewAppError("UploadFile", "api.file.upload_file.incorrect_channelId.app_error",
map[string]any{"channelId": channelID}, "", http.StatusBadRequest)
}
info, _, appError := a.DoUploadFileExpectModification(c, time.Now(), "noteam", channelID, "nouser", filename, data)
if appError != nil {
return nil, appError
}
if info.PreviewPath != "" || info.ThumbnailPath != "" {
previewPathList := []string{info.PreviewPath}
thumbnailPathList := []string{info.ThumbnailPath}
imageDataList := [][]byte{data}
a.HandleImages(previewPathList, thumbnailPathList, imageDataList)
}
return info, nil
}
func (a *App) DoUploadFile(c request.CTX, now time.Time, rawTeamId string, rawChannelId string, rawUserId string, rawFilename string, data []byte) (*model.FileInfo, *model.AppError) {
info, _, err := a.DoUploadFileExpectModification(c, now, rawTeamId, rawChannelId, rawUserId, rawFilename, data)
return info, err
}
func UploadFileSetTeamId(teamID string) func(t *UploadFileTask) {
return func(t *UploadFileTask) {
t.TeamId = filepath.Base(teamID)
}
}
func UploadFileSetUserId(userID string) func(t *UploadFileTask) {
return func(t *UploadFileTask) {
t.UserId = filepath.Base(userID)
}
}
func UploadFileSetTimestamp(timestamp time.Time) func(t *UploadFileTask) {
return func(t *UploadFileTask) {
t.Timestamp = timestamp
}
}
func UploadFileSetContentLength(contentLength int64) func(t *UploadFileTask) {
return func(t *UploadFileTask) {
t.ContentLength = contentLength
}
}
func UploadFileSetClientId(clientId string) func(t *UploadFileTask) {
return func(t *UploadFileTask) {
t.ClientId = clientId
}
}
func UploadFileSetRaw() func(t *UploadFileTask) {
return func(t *UploadFileTask) {
t.Raw = true
}
}
type UploadFileTask struct {
// File name.
Name string
ChannelId string
TeamId string
UserId string
// Time stamp to use when creating the file.
Timestamp time.Time
// The value of the Content-Length http header, when available.
ContentLength int64
// The file data stream.
Input io.Reader
// An optional, client-assigned Id field.
ClientId string
// If Raw, do not execute special processing for images, just upload
// the file. Plugins are still invoked.
Raw bool
//=============================================================
// Internal state
buf *bytes.Buffer
limit int64
limitedInput io.Reader
teeInput io.Reader
fileinfo *model.FileInfo
maxFileSize int64
maxImageRes int64
// Cached image data that (may) get initialized in preprocessImage and
// is used in postprocessImage
decoded image.Image
imageType string
imageOrientation int
// Testing: overridable dependency functions
pluginsEnvironment *plugin.Environment
writeFile func(io.Reader, string) (int64, *model.AppError)
saveToDatabase func(*model.FileInfo) (*model.FileInfo, error)
imgDecoder *imaging.Decoder
imgEncoder *imaging.Encoder
}
func (t *UploadFileTask) init(a *App) {
t.buf = &bytes.Buffer{}
if t.ContentLength > 0 {
t.limit = t.ContentLength
} else {
t.limit = t.maxFileSize
}
if t.ContentLength > 0 && t.ContentLength < maxUploadInitialBufferSize {
t.buf.Grow(int(t.ContentLength))
} else {
t.buf.Grow(maxUploadInitialBufferSize)
}
t.fileinfo = model.NewInfo(filepath.Base(t.Name))
t.fileinfo.Id = model.NewId()
t.fileinfo.CreatorId = t.UserId
t.fileinfo.CreateAt = t.Timestamp.UnixNano() / int64(time.Millisecond)
t.fileinfo.Path = t.pathPrefix() + t.Name
t.limitedInput = &io.LimitedReader{
R: t.Input,
N: t.limit + 1,
}
t.teeInput = io.TeeReader(t.limitedInput, t.buf)
t.pluginsEnvironment = a.GetPluginsEnvironment()
t.writeFile = a.WriteFile
t.saveToDatabase = a.Srv().Store().FileInfo().Save
}
// UploadFileX uploads a single file as specified in t. It applies the upload
// constraints, executes plugins and image processing logic as needed. It
// returns a filled-out FileInfo and an optional error. A plugin may reject the
// upload, returning a rejection error. In this case FileInfo would have
// contained the last "good" FileInfo before the execution of that plugin.
func (a *App) UploadFileX(c *request.Context, channelID, name string, input io.Reader,
opts ...func(*UploadFileTask)) (*model.FileInfo, *model.AppError) {
t := &UploadFileTask{
ChannelId: filepath.Base(channelID),
Name: filepath.Base(name),
Input: input,
maxFileSize: *a.Config().FileSettings.MaxFileSize,
maxImageRes: *a.Config().FileSettings.MaxImageResolution,
imgDecoder: a.ch.imgDecoder,
imgEncoder: a.ch.imgEncoder,
}
for _, o := range opts {
o(t)
}
if *a.Config().FileSettings.DriverName == "" {
return nil, t.newAppError("api.file.upload_file.storage.app_error", http.StatusNotImplemented)
}
if t.ContentLength > t.maxFileSize {
return nil, t.newAppError("api.file.upload_file.too_large_detailed.app_error", http.StatusRequestEntityTooLarge, "Length", t.ContentLength, "Limit", t.maxFileSize)
}
t.init(a)
var aerr *model.AppError
if !t.Raw && t.fileinfo.IsImage() {
aerr = t.preprocessImage()
if aerr != nil {
return t.fileinfo, aerr
}
}
written, aerr := t.writeFile(io.MultiReader(t.buf, t.limitedInput), t.fileinfo.Path)
if aerr != nil {
return nil, aerr
}
if written > t.maxFileSize {
if fileErr := a.RemoveFile(t.fileinfo.Path); fileErr != nil {
mlog.Error("Failed to remove file", mlog.Err(fileErr))
}
return nil, t.newAppError("api.file.upload_file.too_large_detailed.app_error", http.StatusRequestEntityTooLarge, "Length", t.ContentLength, "Limit", t.maxFileSize)
}
t.fileinfo.Size = written
file, aerr := a.FileReader(t.fileinfo.Path)
if aerr != nil {
return nil, aerr
}
defer file.Close()
aerr = a.runPluginsHook(c, t.fileinfo, file)
if aerr != nil {
return nil, aerr
}
if !t.Raw && t.fileinfo.IsImage() {
file, aerr = a.FileReader(t.fileinfo.Path)
if aerr != nil {
return nil, aerr
}
defer file.Close()
t.postprocessImage(file)
}
if _, err := t.saveToDatabase(t.fileinfo); err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("UploadFileX", "app.file_info.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if *a.Config().FileSettings.ExtractContent {
infoCopy := *t.fileinfo
a.Srv().GoBuffered(func() {
err := a.ExtractContentFromFileInfo(&infoCopy)
if err != nil {
mlog.Error("Failed to extract file content", mlog.Err(err), mlog.String("fileInfoId", infoCopy.Id))
}
})
}
return t.fileinfo, nil
}
func (t *UploadFileTask) preprocessImage() *model.AppError {
// If SVG, attempt to extract dimensions and then return
if t.fileinfo.IsSvg() {
svgInfo, err := imaging.ParseSVG(t.teeInput)
if err != nil {
mlog.Warn("Failed to parse SVG", mlog.Err(err))
}
if svgInfo.Width > 0 && svgInfo.Height > 0 {
t.fileinfo.Width = svgInfo.Width
t.fileinfo.Height = svgInfo.Height
}
t.fileinfo.HasPreviewImage = false
return nil
}
// If we fail to decode, return "as is".
w, h, err := imaging.GetDimensions(t.teeInput)
if err != nil {
return nil
}
t.fileinfo.Width = w
t.fileinfo.Height = h
if err = checkImageResolutionLimit(w, h, t.maxImageRes); err != nil {
return t.newAppError("api.file.upload_file.large_image_detailed.app_error", http.StatusBadRequest)
}
t.fileinfo.HasPreviewImage = true
nameWithoutExtension := t.Name[:strings.LastIndex(t.Name, ".")]
t.fileinfo.PreviewPath = t.pathPrefix() + nameWithoutExtension + "_preview." + getFileExtFromMimeType(t.fileinfo.MimeType)
t.fileinfo.ThumbnailPath = t.pathPrefix() + nameWithoutExtension + "_thumb." + getFileExtFromMimeType(t.fileinfo.MimeType)
// check the image orientation with goexif; consume the bytes we
// already have first, then keep Tee-ing from input.
// TODO: try to reuse exif's .Raw buffer rather than Tee-ing
if t.imageOrientation, err = imaging.GetImageOrientation(io.MultiReader(bytes.NewReader(t.buf.Bytes()), t.teeInput)); err == nil &&
(t.imageOrientation == imaging.RotatedCWMirrored ||
t.imageOrientation == imaging.RotatedCCW ||
t.imageOrientation == imaging.RotatedCCWMirrored ||
t.imageOrientation == imaging.RotatedCW) {
t.fileinfo.Width, t.fileinfo.Height = t.fileinfo.Height, t.fileinfo.Width
}
// For animated GIFs disable the preview; since we have to Decode gifs
// anyway, cache the decoded image for later.
if t.fileinfo.MimeType == "image/gif" {
image, format, err := t.imgDecoder.Decode(io.MultiReader(bytes.NewReader(t.buf.Bytes()), t.teeInput))
if err == nil && image != nil {
t.fileinfo.HasPreviewImage = false
t.decoded = image
t.imageType = format
}
}
return nil
}
func (t *UploadFileTask) postprocessImage(file io.Reader) {
// don't try to process SVG files
if t.fileinfo.IsSvg() {
return
}
decoded, imgType := t.decoded, t.imageType
if decoded == nil {
var err error
var release func()
decoded, imgType, release, err = t.imgDecoder.DecodeMemBounded(file)
if err != nil {
mlog.Error("Unable to decode image", mlog.Err(err))
return
}
defer release()
}
decoded = imaging.MakeImageUpright(decoded, t.imageOrientation)
if decoded == nil {
return
}
writeImage := func(img image.Image, path string) {
r, w := io.Pipe()
go func() {
var err error
// It's okay to access imgType in a separate goroutine,
// because imgType is only written once and never written again.
if imgType == "png" {
err = t.imgEncoder.EncodePNG(w, img)
} else {
err = t.imgEncoder.EncodeJPEG(w, img, jpegEncQuality)
}
if err != nil {
mlog.Error("Unable to encode image as jpeg", mlog.String("path", path), mlog.Err(err))
w.CloseWithError(err)
} else {
w.Close()
}
}()
_, aerr := t.writeFile(r, path)
if aerr != nil {
mlog.Error("Unable to upload", mlog.String("path", path), mlog.Err(aerr))
r.CloseWithError(aerr) // always returns nil
return
}
}
var wg sync.WaitGroup
wg.Add(3)
// Generating thumbnail and preview regardless of HasPreviewImage value.
// This is needed on mobile in case of animated GIFs.
go func() {
defer wg.Done()
writeImage(imaging.GenerateThumbnail(decoded, imageThumbnailWidth, imageThumbnailHeight), t.fileinfo.ThumbnailPath)
}()
go func() {
defer wg.Done()
writeImage(imaging.GeneratePreview(decoded, imagePreviewWidth), t.fileinfo.PreviewPath)
}()
go func() {
defer wg.Done()
if t.fileinfo.MiniPreview == nil {
if miniPreview, err := imaging.GenerateMiniPreviewImage(decoded,
miniPreviewImageWidth, miniPreviewImageHeight, jpegEncQuality); err != nil {
mlog.Info("Unable to generate mini preview image", mlog.Err(err))
} else {
t.fileinfo.MiniPreview = &miniPreview
}
}
}()
wg.Wait()
}
func (t UploadFileTask) pathPrefix() string {
return t.Timestamp.Format("20060102") +
"/teams/" + t.TeamId +
"/channels/" + t.ChannelId +
"/users/" + t.UserId +
"/" + t.fileinfo.Id + "/"
}
func (t UploadFileTask) newAppError(id string, httpStatus int, extra ...any) *model.AppError {
params := map[string]any{
"Name": t.Name,
"Filename": t.Name,
"ChannelId": t.ChannelId,
"TeamId": t.TeamId,
"UserId": t.UserId,
"ContentLength": t.ContentLength,
"ClientId": t.ClientId,
}
if t.fileinfo != nil {
params["Width"] = t.fileinfo.Width
params["Height"] = t.fileinfo.Height
}
for i := 0; i+1 < len(extra); i += 2 {
params[fmt.Sprintf("%v", extra[i])] = extra[i+1]
}
return model.NewAppError("uploadFileTask", id, params, "", httpStatus)
}
func (a *App) DoUploadFileExpectModification(c request.CTX, now time.Time, rawTeamId string, rawChannelId string, rawUserId string, rawFilename string, data []byte) (*model.FileInfo, []byte, *model.AppError) {
filename := filepath.Base(rawFilename)
teamID := filepath.Base(rawTeamId)
channelID := filepath.Base(rawChannelId)
userID := filepath.Base(rawUserId)
info, err := model.GetInfoForBytes(filename, bytes.NewReader(data), len(data))
if err != nil {
err.StatusCode = http.StatusBadRequest
return nil, data, err
}
if orientation, err := imaging.GetImageOrientation(bytes.NewReader(data)); err == nil &&
(orientation == imaging.RotatedCWMirrored ||
orientation == imaging.RotatedCCW ||
orientation == imaging.RotatedCCWMirrored ||
orientation == imaging.RotatedCW) {
info.Width, info.Height = info.Height, info.Width
}
info.Id = model.NewId()
info.CreatorId = userID
info.CreateAt = now.UnixNano() / int64(time.Millisecond)
pathPrefix := now.Format("20060102") + "/teams/" + teamID + "/channels/" + channelID + "/users/" + userID + "/" + info.Id + "/"
info.Path = pathPrefix + filename
if info.IsImage() && !info.IsSvg() {
if limitErr := checkImageResolutionLimit(info.Width, info.Height, *a.Config().FileSettings.MaxImageResolution); limitErr != nil {
err := model.NewAppError("uploadFile", "api.file.upload_file.large_image.app_error", map[string]any{"Filename": filename}, "", http.StatusBadRequest).Wrap(limitErr)
return nil, data, err
}
nameWithoutExtension := filename[:strings.LastIndex(filename, ".")]
info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview." + getFileExtFromMimeType(info.MimeType)
info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb." + getFileExtFromMimeType(info.MimeType)
}
var rejectionError *model.AppError
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
var newBytes bytes.Buffer
replacementInfo, rejectionReason := hooks.FileWillBeUploaded(pluginContext, info, bytes.NewReader(data), &newBytes)
if rejectionReason != "" {
rejectionError = model.NewAppError("DoUploadFile", "File rejected by plugin. "+rejectionReason, nil, "", http.StatusBadRequest)
return false
}
if replacementInfo != nil {
info = replacementInfo
}
if newBytes.Len() != 0 {
data = newBytes.Bytes()
info.Size = int64(len(data))
}
return true
}, plugin.FileWillBeUploadedID)
if rejectionError != nil {
return nil, data, rejectionError
}
if _, err := a.WriteFile(bytes.NewReader(data), info.Path); err != nil {
return nil, data, err
}
if _, err := a.Srv().Store().FileInfo().Save(info); err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, data, appErr
default:
return nil, data, model.NewAppError("DoUploadFileExpectModification", "app.file_info.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if *a.Config().FileSettings.ExtractContent {
infoCopy := *info
a.Srv().GoBuffered(func() {
err := a.ExtractContentFromFileInfo(&infoCopy)
if err != nil {
mlog.Error("Failed to extract file content", mlog.Err(err), mlog.String("fileInfoId", infoCopy.Id))
}
})
}
return info, data, nil
}
func (a *App) HandleImages(previewPathList []string, thumbnailPathList []string, fileData [][]byte) {
wg := new(sync.WaitGroup)
for i := range fileData {
img, imgType, release, err := prepareImage(a.ch.imgDecoder, bytes.NewReader(fileData[i]))
if err != nil {
mlog.Debug("Failed to prepare image", mlog.Err(err))
continue
}
wg.Add(2)
go func(img image.Image, imgType, path string) {
defer wg.Done()
a.generateThumbnailImage(img, imgType, path)
}(img, imgType, thumbnailPathList[i])
go func(img image.Image, imgType, path string) {
defer wg.Done()
a.generatePreviewImage(img, imgType, path)
}(img, imgType, previewPathList[i])
wg.Wait()
release()
}
}
func prepareImage(imgDecoder *imaging.Decoder, imgData io.ReadSeeker) (img image.Image, imgType string, release func(), err error) {
// Decode image bytes into Image object
img, imgType, release, err = imgDecoder.DecodeMemBounded(imgData)
if err != nil {
return nil, "", nil, fmt.Errorf("prepareImage: failed to decode image: %w", err)
}
imgData.Seek(0, io.SeekStart)
// Flip the image to be upright
orientation, err := imaging.GetImageOrientation(imgData)
if err != nil {
mlog.Debug("GetImageOrientation failed", mlog.Err(err))
}
img = imaging.MakeImageUpright(img, orientation)
return img, imgType, release, nil
}
func (a *App) generateThumbnailImage(img image.Image, imgType, thumbnailPath string) {
var buf bytes.Buffer
thumb := imaging.GenerateThumbnail(img, imageThumbnailWidth, imageThumbnailHeight)
if imgType == "png" {
if err := a.ch.imgEncoder.EncodePNG(&buf, thumb); err != nil {
mlog.Error("Unable to encode image as png", mlog.String("path", thumbnailPath), mlog.Err(err))
return
}
} else {
if err := a.ch.imgEncoder.EncodeJPEG(&buf, thumb, jpegEncQuality); err != nil {
mlog.Error("Unable to encode image as jpeg", mlog.String("path", thumbnailPath), mlog.Err(err))
return
}
}
if _, err := a.WriteFile(&buf, thumbnailPath); err != nil {
mlog.Error("Unable to upload thumbnail", mlog.String("path", thumbnailPath), mlog.Err(err))
return
}
}
func (a *App) generatePreviewImage(img image.Image, imgType, previewPath string) {
var buf bytes.Buffer
preview := imaging.GeneratePreview(img, imagePreviewWidth)
if imgType == "png" {
if err := a.ch.imgEncoder.EncodePNG(&buf, preview); err != nil {
mlog.Error("Unable to encode image as preview png", mlog.Err(err), mlog.String("path", previewPath))
return
}
} else {
if err := a.ch.imgEncoder.EncodeJPEG(&buf, preview, jpegEncQuality); err != nil {
mlog.Error("Unable to encode image as preview jpg", mlog.Err(err), mlog.String("path", previewPath))
return
}
}
if _, err := a.WriteFile(&buf, previewPath); err != nil {
mlog.Error("Unable to upload preview", mlog.Err(err), mlog.String("path", previewPath))
return
}
}
// generateMiniPreview updates mini preview if needed
// will save fileinfo with the preview added
func (a *App) generateMiniPreview(fi *model.FileInfo) {
if fi.IsImage() && !fi.IsSvg() && fi.MiniPreview == nil {
file, appErr := a.FileReader(fi.Path)
if appErr != nil {
mlog.Debug("error reading image file", mlog.Err(appErr))
return
}
defer file.Close()
img, _, release, err := prepareImage(a.ch.imgDecoder, file)
if err != nil {
mlog.Debug("generateMiniPreview: prepareImage failed", mlog.Err(err),
mlog.String("fileinfo_id", fi.Id), mlog.String("channel_id", fi.ChannelId),
mlog.String("creator_id", fi.CreatorId))
return
}
defer release()
var miniPreview []byte
if miniPreview, err = imaging.GenerateMiniPreviewImage(img,
miniPreviewImageWidth, miniPreviewImageHeight, jpegEncQuality); err != nil {
mlog.Info("Unable to generate mini preview image", mlog.Err(err))
} else {
fi.MiniPreview = &miniPreview
}
if _, err = a.Srv().Store().FileInfo().Upsert(fi); err != nil {
mlog.Debug("creating mini preview failed", mlog.Err(err))
} else {
a.Srv().Store().FileInfo().InvalidateFileInfosForPostCache(fi.PostId, false)
}
}
}
func (a *App) generateMiniPreviewForInfos(fileInfos []*model.FileInfo) {
wg := new(sync.WaitGroup)
wg.Add(len(fileInfos))
for _, fileInfo := range fileInfos {
go func(fi *model.FileInfo) {
defer wg.Done()
a.generateMiniPreview(fi)
}(fileInfo)
}
wg.Wait()
}
func (s *Server) getFileInfo(fileID string) (*model.FileInfo, *model.AppError) {
fileInfo, err := s.Store().FileInfo().Get(fileID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetFileInfo", "app.file_info.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetFileInfo", "app.file_info.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return fileInfo, nil
}
func (a *App) GetFileInfo(fileID string) (*model.FileInfo, *model.AppError) {
fileInfo, appErr := a.Srv().getFileInfo(fileID)
if appErr != nil {
return nil, appErr
}
firstInaccessibleFileTime, appErr := a.isInaccessibleFile(fileInfo)
if appErr != nil {
return nil, appErr
}
if firstInaccessibleFileTime > 0 {
return nil, model.NewAppError("GetFileInfo", "app.file.cloud.get.app_error", nil, "", http.StatusForbidden)
}
a.generateMiniPreview(fileInfo)
return fileInfo, appErr
}
func (a *App) getFileInfoIgnoreCloudLimit(fileID string) (*model.FileInfo, *model.AppError) {
fileInfo, appErr := a.Srv().getFileInfo(fileID)
if appErr == nil {
a.generateMiniPreview(fileInfo)
}
return fileInfo, appErr
}
func (a *App) GetFileInfos(page, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, *model.AppError) {
fileInfos, err := a.Srv().Store().FileInfo().GetWithOptions(page, perPage, opt)
if err != nil {
var invErr *store.ErrInvalidInput
var ltErr *store.ErrLimitExceeded
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("GetFileInfos", "app.file_info.get_with_options.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, <Err):
return nil, model.NewAppError("GetFileInfos", "app.file_info.get_with_options.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("GetFileInfos", "app.file_info.get_with_options.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
filterOptions := filterFileOptions{}
if opt != nil && (opt.SortBy == "" || opt.SortBy == model.FileinfoSortByCreated) {
filterOptions.assumeSortedCreatedAt = true
}
fileInfos, _, appErr := a.getFilteredAccessibleFiles(fileInfos, filterOptions)
if appErr != nil {
return nil, appErr
}
a.generateMiniPreviewForInfos(fileInfos)
return fileInfos, nil
}
func (a *App) GetFile(fileID string) ([]byte, *model.AppError) {
info, err := a.GetFileInfo(fileID)
if err != nil {
return nil, err
}
data, err := a.ReadFile(info.Path)
if err != nil {
return nil, err
}
return data, nil
}
func (a *App) getFileIgnoreCloudLimit(fileID string) ([]byte, *model.AppError) {
info, err := a.getFileInfoIgnoreCloudLimit(fileID)
if err != nil {
return nil, err
}
data, err := a.ReadFile(info.Path)
if err != nil {
return nil, err
}
return data, nil
}
func (a *App) CopyFileInfos(userID string, fileIDs []string) ([]string, *model.AppError) {
var newFileIds []string
now := model.GetMillis()
for _, fileID := range fileIDs {
fileInfo, err := a.Srv().Store().FileInfo().Get(fileID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("CopyFileInfos", "app.file_info.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("CopyFileInfos", "app.file_info.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
fileInfo.Id = model.NewId()
fileInfo.CreatorId = userID
fileInfo.CreateAt = now
fileInfo.UpdateAt = now
fileInfo.PostId = ""
fileInfo.ChannelId = ""
if _, err := a.Srv().Store().FileInfo().Save(fileInfo); err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CopyFileInfos", "app.file_info.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
newFileIds = append(newFileIds, fileInfo.Id)
}
return newFileIds, nil
}
// This function zip's up all the files in fileDatas array and then saves it to the directory specified with the specified zip file name
// Ensure the zip file name ends with a .zip
func (a *App) CreateZipFileAndAddFiles(fileBackend filestore.FileBackend, fileDatas []model.FileData, zipFileName, directory string) error {
// Create Zip File (temporarily stored on disk)
conglomerateZipFile, err := os.Create(zipFileName)
if err != nil {
return err
}
defer os.Remove(zipFileName)
// Create a new zip archive.
zipFileWriter := zip.NewWriter(conglomerateZipFile)
// Populate Zip file with File Datas array
err = populateZipfile(zipFileWriter, fileDatas)
if err != nil {
return err
}
conglomerateZipFile.Seek(0, 0)
_, err = fileBackend.WriteFile(conglomerateZipFile, path.Join(directory, zipFileName))
if err != nil {
return err
}
return nil
}
// This is a implementation of Go's example of writing files to zip (with slight modification)
// https://golang.org/src/archive/zip/example_test.go
func populateZipfile(w *zip.Writer, fileDatas []model.FileData) error {
defer w.Close()
for _, fd := range fileDatas {
f, err := w.Create(fd.Filename)
if err != nil {
return err
}
_, err = f.Write(fd.Body)
if err != nil {
return err
}
}
return nil
}
func (a *App) SearchFilesInTeamForUser(c *request.Context, terms string, userId string, teamId string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int, modifier string) (*model.FileInfoList, *model.AppError) {
paramsList := model.ParseSearchParams(strings.TrimSpace(terms), timeZoneOffset)
includeDeleted := includeDeletedChannels && *a.Config().TeamSettings.ExperimentalViewArchivedChannels
if !*a.Config().ServiceSettings.EnableFileSearch {
return nil, model.NewAppError("SearchFilesInTeamForUser", "store.sql_file_info.search.disabled", nil, fmt.Sprintf("teamId=%v userId=%v", teamId, userId), http.StatusNotImplemented)
}
finalParamsList := []*model.SearchParams{}
for _, params := range paramsList {
params.Modifier = modifier
params.OrTerms = isOrSearch
params.IncludeDeletedChannels = includeDeleted
// Don't allow users to search for "*"
if params.Terms != "*" {
// Convert channel names to channel IDs
params.InChannels = a.convertChannelNamesToChannelIds(c, params.InChannels, userId, teamId, includeDeletedChannels)
params.ExcludedChannels = a.convertChannelNamesToChannelIds(c, params.ExcludedChannels, userId, teamId, includeDeletedChannels)
// Convert usernames to user IDs
params.FromUsers = a.convertUserNameToUserIds(params.FromUsers)
params.ExcludedUsers = a.convertUserNameToUserIds(params.ExcludedUsers)
finalParamsList = append(finalParamsList, params)
}
}
// If the processed search params are empty, return empty search results.
if len(finalParamsList) == 0 {
return model.NewFileInfoList(), nil
}
fileInfoSearchResults, nErr := a.Srv().Store().FileInfo().Search(finalParamsList, userId, teamId, page, perPage)
if nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("SearchFilesInTeamForUser", "app.post.search.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return fileInfoSearchResults, a.filterInaccessibleFiles(fileInfoSearchResults, filterFileOptions{assumeSortedCreatedAt: true})
}
func (a *App) ExtractContentFromFileInfo(fileInfo *model.FileInfo) error {
// We don't process images.
if fileInfo.IsImage() {
return nil
}
file, aerr := a.FileReader(fileInfo.Path)
if aerr != nil {
return errors.Wrap(aerr, "failed to open file for extract file content")
}
defer file.Close()
text, err := docextractor.Extract(fileInfo.Name, file, docextractor.ExtractSettings{
ArchiveRecursion: *a.Config().FileSettings.ArchiveRecursion,
})
if err != nil {
return errors.Wrap(err, "failed to extract file content")
}
if text != "" {
if len(text) > maxContentExtractionSize {
text = text[0:maxContentExtractionSize]
}
if storeErr := a.Srv().Store().FileInfo().SetContent(fileInfo.Id, text); storeErr != nil {
return errors.Wrap(storeErr, "failed to save the extracted file content")
}
reloadFileInfo, storeErr := a.Srv().Store().FileInfo().Get(fileInfo.Id)
if storeErr != nil {
mlog.Warn("Failed to invalidate the fileInfo cache.", mlog.Err(storeErr), mlog.String("file_info_id", fileInfo.Id))
} else {
a.Srv().Store().FileInfo().InvalidateFileInfosForPostCache(reloadFileInfo.PostId, false)
}
}
return nil
}
// GetLastAccessibleFileTime returns CreateAt time(from cache) of the last accessible post as per the cloud limit
func (a *App) GetLastAccessibleFileTime() (int64, *model.AppError) {
license := a.Srv().License()
if !license.IsCloud() {
return 0, nil
}
system, err := a.Srv().Store().System().GetByName(model.SystemLastAccessibleFileTime)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
// All files are accessible
return 0, nil
default:
return 0, model.NewAppError("GetLastAccessibleFileTime", "app.system.get_by_name.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
lastAccessibleFileTime, err := strconv.ParseInt(system.Value, 10, 64)
if err != nil {
return 0, model.NewAppError("GetLastAccessibleFileTime", "common.parse_error_int64", map[string]interface{}{"Value": system.Value}, err.Error(), http.StatusInternalServerError)
}
return lastAccessibleFileTime, nil
}
// ComputeLastAccessibleFileTime updates cache with CreateAt time of the last accessible file as per the cloud plan's limit.
// Use GetLastAccessibleFileTime() to access the result.
func (a *App) ComputeLastAccessibleFileTime() error {
limit, appErr := a.getCloudFilesSizeLimit()
if appErr != nil {
return appErr
}
if limit == 0 {
// All files are accessible - we must check if a previous value was set so we can clear it
systemValue, err := a.Srv().Store().System().GetByName(model.SystemLastAccessibleFileTime)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
// All files are already accessible
return nil
default:
return model.NewAppError("ComputeLastAccessibleFileTime", "app.system.get_by_name.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
if systemValue != nil {
// Previous value was set, so we must clear it
if _, err := a.Srv().Store().System().PermanentDeleteByName(model.SystemLastAccessibleFileTime); err != nil {
return model.NewAppError("ComputeLastAccessibleFileTime", "app.system.permanent_delete_by_name.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return nil
}
createdAt, err := a.Srv().GetStore().FileInfo().GetUptoNSizeFileTime(limit)
if err != nil {
var nfErr *store.ErrNotFound
if !errors.As(err, &nfErr) {
return model.NewAppError("ComputeLastAccessibleFileTime", "app.last_accessible_file.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
// Update Cache
err = a.Srv().Store().System().SaveOrUpdate(&model.System{
Name: model.SystemLastAccessibleFileTime,
Value: strconv.FormatInt(createdAt, 10),
})
if err != nil {
return model.NewAppError("ComputeLastAccessibleFileTime", "app.system.save.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
}
// getCloudFilesSizeLimit returns size in bytes
func (a *App) getCloudFilesSizeLimit() (int64, *model.AppError) {
license := a.Srv().License()
if license == nil || !license.IsCloud() {
return 0, nil
}
// limits is in bits
limits, err := a.Cloud().GetCloudLimits("")
if err != nil {
return 0, model.NewAppError("getCloudFilesSizeLimit", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if limits == nil || limits.Files == nil || limits.Files.TotalStorage == nil {
// Cloud limit is not applicable
return 0, nil
}
return int64(math.Ceil(float64(*limits.Files.TotalStorage) / 8)), nil
}
func getFileExtFromMimeType(mimeType string) string {
if mimeType == "image/png" {
return "png"
}
return "jpg"
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
)
// removeInaccessibleContentFromFilesSlice removes content from the files beyond the cloud plan's limit
// and also returns the firstInaccessibleFileTime
func (a *App) removeInaccessibleContentFromFilesSlice(files []*model.FileInfo) (int64, *model.AppError) {
if len(files) == 0 {
return 0, nil
}
lastAccessibleFileTime, appErr := a.GetLastAccessibleFileTime()
if appErr != nil {
return 0, model.NewAppError("removeInaccessibleFileListContent", "app.last_accessible_file.app_error", nil, appErr.Error(), http.StatusInternalServerError)
}
if lastAccessibleFileTime == 0 {
// No need to remove content, all files are accessible
return 0, nil
}
var firstInaccessibleFileTime int64 = 0
for _, file := range files {
if createAt := file.CreateAt; createAt < lastAccessibleFileTime {
file.MakeContentInaccessible()
if createAt > firstInaccessibleFileTime {
firstInaccessibleFileTime = createAt
}
}
}
return firstInaccessibleFileTime, nil
}
// filterInaccessibleFiles filters out the files, past the cloud limit
func (a *App) filterInaccessibleFiles(fileList *model.FileInfoList, options filterFileOptions) *model.AppError {
if fileList == nil || fileList.FileInfos == nil || len(fileList.FileInfos) == 0 {
return nil
}
lastAccessibleFileTime, appErr := a.GetLastAccessibleFileTime()
if appErr != nil {
return model.NewAppError("filterInaccessibleFiles", "app.last_accessible_file.app_error", nil, appErr.Error(), http.StatusInternalServerError)
}
if lastAccessibleFileTime == 0 {
// No need to filter, all files are accessible
return nil
}
if len(fileList.FileInfos) == len(fileList.Order) && options.assumeSortedCreatedAt {
lenFiles := len(fileList.FileInfos)
getCreateAt := func(i int) int64 { return fileList.FileInfos[fileList.Order[i]].CreateAt }
bounds := getTimeSortedPostAccessibleBounds(lastAccessibleFileTime, lenFiles, getCreateAt)
if bounds.allAccessible(lenFiles) {
return nil
}
if bounds.noAccessible() {
if lenFiles > 0 {
firstFileCreatedAt := fileList.FileInfos[fileList.Order[0]].CreateAt
lastFileCreatedAt := fileList.FileInfos[fileList.Order[lenFiles-1]].CreateAt
fileList.FirstInaccessibleFileTime = max(firstFileCreatedAt, lastFileCreatedAt)
}
fileList.FileInfos = map[string]*model.FileInfo{}
fileList.Order = []string{}
return nil
}
startInaccessibleIndex, endInaccessibleIndex := bounds.getInaccessibleRange(len(fileList.Order))
startInaccessibleCreatedAt := fileList.FileInfos[fileList.Order[startInaccessibleIndex]].CreateAt
endInaccessibleCreatedAt := fileList.FileInfos[fileList.Order[endInaccessibleIndex]].CreateAt
fileList.FirstInaccessibleFileTime = max(startInaccessibleCreatedAt, endInaccessibleCreatedAt)
files := fileList.FileInfos
order := fileList.Order
accessibleCount := bounds.end - bounds.start + 1
inaccessibleCount := lenFiles - accessibleCount
// Linearly cover shorter route to traverse files map
if inaccessibleCount < accessibleCount {
for i := 0; i < bounds.start; i++ {
delete(files, order[i])
}
for i := bounds.end + 1; i < lenFiles; i++ {
delete(files, order[i])
}
} else {
accessibleFiles := make(map[string]*model.FileInfo, accessibleCount)
for i := bounds.start; i <= bounds.end; i++ {
accessibleFiles[order[i]] = files[order[i]]
}
fileList.FileInfos = accessibleFiles
}
fileList.Order = fileList.Order[bounds.start : bounds.end+1]
} else {
linearFilterFileList(fileList, lastAccessibleFileTime)
}
return nil
}
// isInaccessibleFile indicates if the file is past the cloud plan's limit.
func (a *App) isInaccessibleFile(file *model.FileInfo) (int64, *model.AppError) {
if file == nil {
return 0, nil
}
fl := &model.FileInfoList{
Order: []string{file.Id},
FileInfos: map[string]*model.FileInfo{file.Id: file},
}
appErr := a.filterInaccessibleFiles(fl, filterFileOptions{assumeSortedCreatedAt: true})
return fl.FirstInaccessibleFileTime, appErr
}
// getFilteredAccessibleFiles returns accessible files filtered as per the cloud plan's limit and also indicates if there were any inaccessible files
func (a *App) getFilteredAccessibleFiles(files []*model.FileInfo, options filterFileOptions) ([]*model.FileInfo, int64, *model.AppError) {
if len(files) == 0 {
return files, 0, nil
}
filteredFiles := []*model.FileInfo{}
lastAccessibleFileTime, appErr := a.GetLastAccessibleFileTime()
if appErr != nil {
return filteredFiles, 0, model.NewAppError("getFilteredAccessibleFiles", "app.last_accessible_file.app_error", nil, appErr.Error(), http.StatusInternalServerError)
} else if lastAccessibleFileTime == 0 {
// No need to filter, all files are accessible
return files, 0, nil
}
if options.assumeSortedCreatedAt {
lenFiles := len(files)
getCreateAt := func(i int) int64 { return files[i].CreateAt }
bounds := getTimeSortedPostAccessibleBounds(lastAccessibleFileTime, lenFiles, getCreateAt)
if bounds.allAccessible(lenFiles) {
return files, 0, nil
}
if bounds.noAccessible() {
var firstInaccessibleFileTime int64 = 0
if lenFiles > 0 {
firstFileCreatedAt := files[0].CreateAt
lastFileCreatedAt := files[len(files)-1].CreateAt
firstInaccessibleFileTime = max(firstFileCreatedAt, lastFileCreatedAt)
}
return filteredFiles, firstInaccessibleFileTime, nil
}
startInaccessibleIndex, endInaccessibleIndex := bounds.getInaccessibleRange(len(files))
firstFileCreatedAt := files[startInaccessibleIndex].CreateAt
lastFileCreatedAt := files[endInaccessibleIndex].CreateAt
firstInaccessibleFileTime := max(firstFileCreatedAt, lastFileCreatedAt)
filteredFiles = files[bounds.start : bounds.end+1]
return filteredFiles, firstInaccessibleFileTime, nil
}
filteredFiles, firstInaccessibleFileTime := linearFilterFilesSlice(files, lastAccessibleFileTime)
return filteredFiles, firstInaccessibleFileTime, nil
}
type filterFileOptions struct {
assumeSortedCreatedAt bool
}
// linearFilterFileList make no assumptions about ordering, go through files one by one
// this is the slower fallback that is still safe
// if we can not assume files are ordered by CreatedAt
func linearFilterFileList(fileList *model.FileInfoList, earliestAccessibleTime int64) {
files := fileList.FileInfos
order := fileList.Order
n := 0
for i, fileID := range order {
if createAt := files[fileID].CreateAt; createAt >= earliestAccessibleTime {
order[n] = order[i]
n++
} else {
if createAt > fileList.FirstInaccessibleFileTime {
fileList.FirstInaccessibleFileTime = createAt
}
delete(files, fileID)
}
}
fileList.Order = order[:n]
}
// linearFilterFilesSlice make no assumptions about ordering, go through files one by one
// this is the slower fallback that is still safe
// if we can not assume files are ordered by CreatedAt
func linearFilterFilesSlice(files []*model.FileInfo, earliestAccessibleTime int64) ([]*model.FileInfo, int64) {
var firstInaccessibleFileTime int64 = 0
n := 0
for i := range files {
if createAt := files[i].CreateAt; createAt >= earliestAccessibleTime {
files[n] = files[i]
n++
} else {
if createAt > firstInaccessibleFileTime {
firstInaccessibleFileTime = createAt
}
}
}
return files[:n], firstInaccessibleFileTime
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func (a *App) GetGroup(id string, opts *model.GetGroupOpts, viewRestrictions *model.ViewUsersRestrictions) (*model.Group, *model.AppError) {
group, err := a.Srv().Store().Group().Get(id)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetGroup", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetGroup", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if opts != nil && opts.IncludeMemberCount {
memberCount, err := a.Srv().Store().Group().GetMemberCountWithRestrictions(id, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetGroup", "app.member_count", nil, "", http.StatusInternalServerError).Wrap(err)
}
group.MemberCount = model.NewInt(int(memberCount))
}
return group, nil
}
func (a *App) GetGroupByName(name string, opts model.GroupSearchOpts) (*model.Group, *model.AppError) {
group, err := a.Srv().Store().Group().GetByName(name, opts)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetGroupByName", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetGroupByName", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return group, nil
}
func (a *App) GetGroupByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, *model.AppError) {
group, err := a.Srv().Store().Group().GetByRemoteID(remoteID, groupSource)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetGroupByRemoteID", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetGroupByRemoteID", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return group, nil
}
func (a *App) GetGroupsBySource(groupSource model.GroupSource) ([]*model.Group, *model.AppError) {
groups, err := a.Srv().Store().Group().GetAllBySource(groupSource)
if err != nil {
return nil, model.NewAppError("GetGroupsBySource", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groups, nil
}
func (a *App) GetGroupsByUserId(userID string) ([]*model.Group, *model.AppError) {
groups, err := a.Srv().Store().Group().GetByUser(userID)
if err != nil {
return nil, model.NewAppError("GetGroupsByUserId", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groups, nil
}
func (a *App) CreateGroup(group *model.Group) (*model.Group, *model.AppError) {
if err := a.isUniqueToUsernames(group.GetName()); err != nil {
err.Where = "CreateGroup"
return nil, err
}
group, err := a.Srv().Store().Group().Create(group)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateGroup", "app.group.id.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("CreateGroup", "app.insert_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return group, nil
}
func (a *App) isUniqueToUsernames(val string) *model.AppError {
if val == "" {
return nil
}
var notFoundErr *store.ErrNotFound
user, err := a.Srv().Store().User().GetByUsername(val)
if err != nil && !errors.As(err, ¬FoundErr) {
return model.NewAppError("isUniqueToUsernames", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
if user != nil {
return model.NewAppError("isUniqueToUsernames", "app.group.username_conflict", map[string]interface{}{"Username": val}, "", http.StatusBadRequest)
}
return nil
}
func (a *App) CreateGroupWithUserIds(group *model.GroupWithUserIds) (*model.Group, *model.AppError) {
if appErr := a.isUniqueToUsernames(group.GetName()); appErr != nil {
appErr.Where = "CreateGroupWithUserIds"
return nil, appErr
}
newGroup, err := a.Srv().Store().Group().CreateWithUserIds(group)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
var dupKey *store.ErrUniqueConstraint
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateGroupWithUserIds", "app.group.id.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &dupKey):
return nil, model.NewAppError("CreateGroupWithUserIds", "app.custom_group.unique_name", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("CreateGroupWithUserIds", "app.insert_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
messageWs := model.NewWebSocketEvent(model.WebsocketEventReceivedGroup, "", "", "", nil, "")
count, err := a.Srv().Store().Group().GetMemberCount(newGroup.Id)
if err != nil {
return nil, model.NewAppError("CreateGroupWithUserIds", "app.group.id.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
group.MemberCount = model.NewInt(int(count))
groupJSON, jsonErr := json.Marshal(newGroup)
if jsonErr != nil {
return nil, model.NewAppError("CreateGroupWithUserIds", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
messageWs.Add("group", string(groupJSON))
a.Publish(messageWs)
return newGroup, nil
}
func (a *App) UpdateGroup(group *model.Group) (*model.Group, *model.AppError) {
if appErr := a.isUniqueToUsernames(group.GetName()); appErr != nil {
appErr.Where = "UpdateGroup"
return nil, appErr
}
updatedGroup, err := a.Srv().Store().Group().Update(group)
if err != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
var dupKey *store.ErrUniqueConstraint
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &nfErr):
return nil, model.NewAppError("UpdateGroup", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &dupKey):
return nil, model.NewAppError("CreateGroup", "app.custom_group.unique_name", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpdateGroup", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
count, err := a.Srv().Store().Group().GetMemberCount(updatedGroup.Id)
if err != nil {
return nil, model.NewAppError("UpdateGroup", "app.group.id.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
updatedGroup.MemberCount = model.NewInt(int(count))
messageWs := model.NewWebSocketEvent(model.WebsocketEventReceivedGroup, "", "", "", nil, "")
groupJSON, err := json.Marshal(updatedGroup)
if err != nil {
return nil, model.NewAppError("UpdateGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
messageWs.Add("group", string(groupJSON))
a.Publish(messageWs)
return updatedGroup, nil
}
func (a *App) DeleteGroup(groupID string) (*model.Group, *model.AppError) {
deletedGroup, err := a.Srv().Store().Group().Delete(groupID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("DeleteGroup", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("DeleteGroup", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return deletedGroup, nil
}
func (a *App) RestoreGroup(groupID string) (*model.Group, *model.AppError) {
restoredGroup, err := a.Srv().Store().Group().Restore(groupID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("RestoreGroup", "app.group.no_rows", nil, nfErr.Error(), http.StatusNotFound)
default:
return nil, model.NewAppError("RestoreGroup", "app.update_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return restoredGroup, nil
}
func (a *App) GetGroupMemberCount(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, *model.AppError) {
count, err := a.Srv().Store().Group().GetMemberCountWithRestrictions(groupID, viewRestrictions)
if err != nil {
return 0, model.NewAppError("GetGroupMemberCount", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return count, nil
}
func (a *App) GetGroupMemberUsers(groupID string) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store().Group().GetMemberUsers(groupID)
if err != nil {
return nil, model.NewAppError("GetGroupMemberUsers", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetGroupMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, int, *model.AppError) {
members, err := a.Srv().Store().Group().GetMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, teammateNameDisplay)
if err != nil {
return nil, 0, model.NewAppError("GetGroupMemberUsersPage", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
count, appErr := a.GetGroupMemberCount(groupID, viewRestrictions)
if appErr != nil {
return nil, 0, appErr
}
return a.sanitizeProfiles(members, false), int(count), nil
}
func (a *App) GetGroupMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, int, *model.AppError) {
return a.GetGroupMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, model.ShowUsername)
}
func (a *App) GetUsersNotInGroupPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
members, err := a.Srv().Store().Group().GetNonMemberUsersPage(groupID, page, perPage, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetUsersNotInGroupPage", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return a.sanitizeProfiles(members, false), nil
}
func (a *App) UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) {
groupMember, err := a.Srv().Store().Group().UpsertMember(groupID, userID)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("UpsertGroupMember", "app.group.uniqueness_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpsertGroupMember", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if appErr := a.publishGroupMemberEvent(model.WebsocketEventGroupMemberAdd, groupMember); appErr != nil {
return nil, appErr
}
return groupMember, nil
}
func (a *App) DeleteGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) {
groupMember, err := a.Srv().Store().Group().DeleteMember(groupID, userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("DeleteGroupMember", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("DeleteGroupMember", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if appErr := a.publishGroupMemberEvent(model.WebsocketEventGroupMemberDelete, groupMember); appErr != nil {
return nil, appErr
}
return groupMember, nil
}
func (a *App) UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) {
gs, err := a.Srv().Store().Group().GetGroupSyncable(groupSyncable.GroupId, groupSyncable.SyncableId, groupSyncable.Type)
var notFoundErr *store.ErrNotFound
if err != nil && !errors.As(err, ¬FoundErr) {
return nil, model.NewAppError("UpsertGroupSyncable", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// reject the syncable creation if the group isn't already associated to the parent team
if groupSyncable.Type == model.GroupSyncableTypeChannel {
channel, nErr := a.Srv().Store().Channel().Get(groupSyncable.SyncableId, true)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("UpsertGroupSyncable", "app.channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("UpsertGroupSyncable", "app.channel.get.find.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
var team *model.Team
team, nErr = a.Srv().Store().Team().Get(channel.TeamId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("UpsertGroupSyncable", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("UpsertGroupSyncable", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if team.IsGroupConstrained() {
var teamGroups []*model.GroupWithSchemeAdmin
teamGroups, err = a.Srv().Store().Group().GetGroupsByTeam(channel.TeamId, model.GroupSearchOpts{})
if err != nil {
return nil, model.NewAppError("UpsertGroupSyncable", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
var permittedGroup bool
for _, teamGroup := range teamGroups {
if teamGroup.Group.Id == groupSyncable.GroupId {
permittedGroup = true
break
}
}
if !permittedGroup {
return nil, model.NewAppError("UpsertGroupSyncable", "group_not_associated_to_synced_team", nil, "", http.StatusBadRequest)
}
} else {
_, appErr := a.UpsertGroupSyncable(model.NewGroupTeam(groupSyncable.GroupId, team.Id, groupSyncable.AutoAdd))
if appErr != nil {
return nil, appErr
}
}
}
if gs == nil {
gs, err = a.Srv().Store().Group().CreateGroupSyncable(groupSyncable)
if err != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &nfErr):
return nil, model.NewAppError("UpsertGroupSyncable", "store.sql_channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("UpsertGroupSyncable", "app.insert_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
} else {
gs, err = a.Srv().Store().Group().UpdateGroupSyncable(groupSyncable)
if err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("UpsertGroupSyncable", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
var messageWs *model.WebSocketEvent
if gs.Type == model.GroupSyncableTypeTeam {
messageWs = model.NewWebSocketEvent(model.WebsocketEventReceivedGroupAssociatedToTeam, gs.SyncableId, "", "", nil, "")
} else {
messageWs = model.NewWebSocketEvent(model.WebsocketEventReceivedGroupAssociatedToChannel, "", gs.SyncableId, "", nil, "")
}
messageWs.Add("group_id", gs.GroupId)
a.Publish(messageWs)
return gs, nil
}
func (a *App) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) {
group, err := a.Srv().Store().Group().GetGroupSyncable(groupID, syncableID, syncableType)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetGroupSyncable", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetGroupSyncable", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return group, nil
}
func (a *App) GetGroupSyncables(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, *model.AppError) {
groups, err := a.Srv().Store().Group().GetAllGroupSyncablesByGroupId(groupID, syncableType)
if err != nil {
return nil, model.NewAppError("GetGroupSyncables", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groups, nil
}
func (a *App) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) {
if groupSyncable.DeleteAt == 0 {
// updating a *deleted* GroupSyncable, so no need to ensure the GroupTeam is present (as done in the upsert)
gs, err := a.Srv().Store().Group().UpdateGroupSyncable(groupSyncable)
if err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("UpdateGroupSyncable", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return gs, nil
}
// do an upsert to ensure that there's an associated GroupTeam
gs, err := a.UpsertGroupSyncable(groupSyncable)
if err != nil {
return nil, err
}
return gs, nil
}
func (a *App) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) {
gs, err := a.Srv().Store().Group().DeleteGroupSyncable(groupID, syncableID, syncableType)
if err != nil {
var invErr *store.ErrInvalidInput
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("DeleteGroupSyncable", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &invErr):
return nil, model.NewAppError("DeleteGroupSyncable", "app.group.group_syncable_already_deleted", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("DeleteGroupSyncable", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// if a GroupTeam is being deleted delete all associated GroupChannels
if gs.Type == model.GroupSyncableTypeTeam {
allGroupChannels, err := a.Srv().Store().Group().GetAllGroupSyncablesByGroupId(gs.GroupId, model.GroupSyncableTypeChannel)
if err != nil {
return nil, model.NewAppError("DeleteGroupSyncable", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, groupChannel := range allGroupChannels {
_, err = a.Srv().Store().Group().DeleteGroupSyncable(groupChannel.GroupId, groupChannel.SyncableId, groupChannel.Type)
if err != nil {
var invErr *store.ErrInvalidInput
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("DeleteGroupSyncable", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &invErr):
return nil, model.NewAppError("DeleteGroupSyncable", "app.group.group_syncable_already_deleted", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("DeleteGroupSyncable", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
}
var messageWs *model.WebSocketEvent
if gs.Type == model.GroupSyncableTypeTeam {
messageWs = model.NewWebSocketEvent(model.WebsocketEventReceivedGroupNotAssociatedToTeam, gs.SyncableId, "", "", nil, "")
} else {
messageWs = model.NewWebSocketEvent(model.WebsocketEventReceivedGroupNotAssociatedToChannel, "", gs.SyncableId, "", nil, "")
}
messageWs.Add("group_id", gs.GroupId)
a.Publish(messageWs)
return gs, nil
}
// TeamMembersToAdd returns a slice of UserTeamIDPair that need newly created memberships
// based on the groups configurations. The returned list can be optionally scoped to a single given team.
//
// Typically since will be the last successful group sync time.
// If includeRemovedMembers is true, then team members who left or were removed from the team will
// be included; otherwise, they will be excluded.
func (a *App) TeamMembersToAdd(since int64, teamID *string, includeRemovedMembers bool) ([]*model.UserTeamIDPair, *model.AppError) {
userTeams, err := a.Srv().Store().Group().TeamMembersToAdd(since, teamID, includeRemovedMembers)
if err != nil {
return nil, model.NewAppError("TeamMembersToAdd", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return userTeams, nil
}
// ChannelMembersToAdd returns a slice of UserChannelIDPair that need newly created memberships
// based on the groups configurations. The returned list can be optionally scoped to a single given channel.
//
// Typically since will be the last successful group sync time.
// If includeRemovedMembers is true, then channel members who left or were removed from the channel will
// be included; otherwise, they will be excluded.
func (a *App) ChannelMembersToAdd(since int64, channelID *string, includeRemovedMembers bool) ([]*model.UserChannelIDPair, *model.AppError) {
userChannels, err := a.Srv().Store().Group().ChannelMembersToAdd(since, channelID, includeRemovedMembers)
if err != nil {
return nil, model.NewAppError("ChannelMembersToAdd", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return userChannels, nil
}
func (a *App) TeamMembersToRemove(teamID *string) ([]*model.TeamMember, *model.AppError) {
teamMembers, err := a.Srv().Store().Group().TeamMembersToRemove(teamID)
if err != nil {
return nil, model.NewAppError("TeamMembersToRemove", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teamMembers, nil
}
func (a *App) ChannelMembersToRemove(teamID *string) ([]*model.ChannelMember, *model.AppError) {
channelMembers, err := a.Srv().Store().Group().ChannelMembersToRemove(teamID)
if err != nil {
return nil, model.NewAppError("ChannelMembersToRemove", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelMembers, nil
}
func (a *App) GetGroupsByChannel(channelID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.AppError) {
groups, err := a.Srv().Store().Group().GetGroupsByChannel(channelID, opts)
if err != nil {
return nil, 0, model.NewAppError("GetGroupsByChannel", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
count, err := a.Srv().Store().Group().CountGroupsByChannel(channelID, opts)
if err != nil {
return nil, 0, model.NewAppError("GetGroupsByChannel", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groups, int(count), nil
}
// GetGroupsByTeam returns the paged list and the total count of group associated to the given team.
func (a *App) GetGroupsByTeam(teamID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.AppError) {
groups, err := a.Srv().Store().Group().GetGroupsByTeam(teamID, opts)
if err != nil {
return nil, 0, model.NewAppError("GetGroupsByTeam", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
count, err := a.Srv().Store().Group().CountGroupsByTeam(teamID, opts)
if err != nil {
return nil, 0, model.NewAppError("GetGroupsByTeam", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groups, int(count), nil
}
func (a *App) GetGroupsAssociatedToChannelsByTeam(teamID string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, *model.AppError) {
groupsAssociatedByChannelId, err := a.Srv().Store().Group().GetGroupsAssociatedToChannelsByTeam(teamID, opts)
if err != nil {
return nil, model.NewAppError("GetGroupsAssociatedToChannelsByTeam", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groupsAssociatedByChannelId, nil
}
func (a *App) GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError) {
groups, err := a.Srv().Store().Group().GetGroups(page, perPage, opts, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetGroups", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groups, nil
}
// TeamMembersMinusGroupMembers returns the set of users on the given team minus the set of users in the given
// groups.
//
// The result can be used, for example, to determine the set of users who would be removed from a team if the team
// were group-constrained with the given groups.
func (a *App) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, int64, *model.AppError) {
users, err := a.Srv().Store().Group().TeamMembersMinusGroupMembers(teamID, groupIDs, page, perPage)
if err != nil {
return nil, 0, model.NewAppError("TeamMembersMinusGroupMembers", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, u := range users {
a.SanitizeProfile(&u.User, false)
}
// parse all group ids of all users
allUsersGroupIDMap := map[string]bool{}
for _, user := range users {
for _, groupID := range user.GetGroupIDs() {
allUsersGroupIDMap[groupID] = true
}
}
// create a slice of distinct group ids
var allUsersGroupIDSlice []string
for key := range allUsersGroupIDMap {
allUsersGroupIDSlice = append(allUsersGroupIDSlice, key)
}
// retrieve groups from DB
groups, appErr := a.GetGroupsByIDs(allUsersGroupIDSlice)
if appErr != nil {
return nil, 0, appErr
}
// map groups by id
groupMap := map[string]*model.Group{}
for _, group := range groups {
groupMap[group.Id] = group
}
// populate each instance's groups field
for _, user := range users {
user.Groups = []*model.Group{}
for _, groupID := range user.GetGroupIDs() {
group, ok := groupMap[groupID]
if ok {
user.Groups = append(user.Groups, group)
}
}
}
totalCount, err := a.Srv().Store().Group().CountTeamMembersMinusGroupMembers(teamID, groupIDs)
if err != nil {
return nil, 0, model.NewAppError("TeamMembersMinusGroupMembers", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, totalCount, nil
}
func (a *App) GetGroupsByIDs(groupIDs []string) ([]*model.Group, *model.AppError) {
groups, err := a.Srv().Store().Group().GetByIDs(groupIDs)
if err != nil {
return nil, model.NewAppError("GetGroupsByIDs", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groups, nil
}
// ChannelMembersMinusGroupMembers returns the set of users in the given channel minus the set of users in the given
// groups.
//
// The result can be used, for example, to determine the set of users who would be removed from a channel if the
// channel were group-constrained with the given groups.
func (a *App) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, int64, *model.AppError) {
users, err := a.Srv().Store().Group().ChannelMembersMinusGroupMembers(channelID, groupIDs, page, perPage)
if err != nil {
return nil, 0, model.NewAppError("ChannelMembersMinusGroupMembers", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, u := range users {
a.SanitizeProfile(&u.User, false)
}
// parse all group ids of all users
allUsersGroupIDMap := map[string]bool{}
for _, user := range users {
for _, groupID := range user.GetGroupIDs() {
allUsersGroupIDMap[groupID] = true
}
}
// create a slice of distinct group ids
var allUsersGroupIDSlice []string
for key := range allUsersGroupIDMap {
allUsersGroupIDSlice = append(allUsersGroupIDSlice, key)
}
// retrieve groups from DB
groups, appErr := a.GetGroupsByIDs(allUsersGroupIDSlice)
if appErr != nil {
return nil, 0, appErr
}
// map groups by id
groupMap := map[string]*model.Group{}
for _, group := range groups {
groupMap[group.Id] = group
}
// populate each instance's groups field
for _, user := range users {
user.Groups = []*model.Group{}
for _, groupID := range user.GetGroupIDs() {
group, ok := groupMap[groupID]
if ok {
user.Groups = append(user.Groups, group)
}
}
}
totalCount, err := a.Srv().Store().Group().CountChannelMembersMinusGroupMembers(channelID, groupIDs)
if err != nil {
return nil, 0, model.NewAppError("ChannelMembersMinusGroupMembers", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, totalCount, nil
}
// UserIsInAdminRoleGroup returns true at least one of the user's groups are configured to set the members as
// admins in the given syncable.
func (a *App) UserIsInAdminRoleGroup(userID, syncableID string, syncableType model.GroupSyncableType) (bool, *model.AppError) {
groupIDs, err := a.Srv().Store().Group().AdminRoleGroupsForSyncableMember(userID, syncableID, syncableType)
if err != nil {
return false, model.NewAppError("UserIsInAdminRoleGroup", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(groupIDs) == 0 {
return false, nil
}
return true, nil
}
func (a *App) UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) {
members, err := a.Srv().Store().Group().UpsertMembers(groupID, userIDs)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("UpsertGroupMembers", "app.group.uniqueness_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpsertGroupMembers", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
for _, groupMember := range members {
if appErr := a.publishGroupMemberEvent(model.WebsocketEventGroupMemberAdd, groupMember); appErr != nil {
return nil, appErr
}
}
return members, nil
}
func (a *App) DeleteGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) {
members, err := a.Srv().Store().Group().DeleteMembers(groupID, userIDs)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("DeleteGroupMember", "app.group.uniqueness_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("DeleteGroupMember", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
for _, groupMember := range members {
if appErr := a.publishGroupMemberEvent(model.WebsocketEventGroupMemberDelete, groupMember); appErr != nil {
return nil, appErr
}
}
return members, nil
}
func (a *App) publishGroupMemberEvent(eventName string, groupMember *model.GroupMember) *model.AppError {
messageWs := model.NewWebSocketEvent(eventName, "", "", groupMember.UserId, nil, "")
groupMemberJSON, jsonErr := json.Marshal(groupMember)
if jsonErr != nil {
return model.NewAppError("publishGroupMemberEvent", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
messageWs.Add("group_member", string(groupMemberJSON))
a.Publish(messageWs)
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"github.com/mattermost/mattermost-server/v6/model"
)
func (a *App) NotifySelfHostedSignupProgress(progress string, userId string) {
// this is an event only the relevant admin should receive.
// If there is no progress, there is nothing to report.
// If there is no userId, we do not want to mistakenly broadcast to all users.
if progress == "" || userId == "" {
return
}
message := model.NewWebSocketEvent(model.WebsocketEventHostedCustomerSignupProgressUpdated, "", "", userId, nil, "")
message.Add("progress", progress)
a.Srv().Platform().Publish(message)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"fmt"
"io"
"github.com/mattermost/mattermost-server/v6/server/channels/app/imaging"
)
func checkImageResolutionLimit(w, h int, maxRes int64) error {
// This casting is done to prevent overflow on 32 bit systems (not needed
// in 64 bits systems because images can't have more than 32 bits height or
// width)
imageRes := int64(w) * int64(h)
if imageRes > maxRes {
return fmt.Errorf("image resolution is too high: %d, max allowed is %d", imageRes, maxRes)
}
return nil
}
func checkImageLimits(imageData io.Reader, maxRes int64) error {
w, h, err := imaging.GetDimensions(imageData)
if err != nil {
return fmt.Errorf("failed to get image dimensions: %w", err)
}
return checkImageResolutionLimit(w, h, maxRes)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"errors"
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"io"
"sync"
_ "github.com/oov/psd"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
)
// DecoderOptions holds configuration options for an image decoder.
type DecoderOptions struct {
// The level of concurrency for the decoder. This defines a limit on the
// number of concurrently running encoding goroutines.
ConcurrencyLevel int
}
func (o *DecoderOptions) validate() error {
if o.ConcurrencyLevel < 0 {
return errors.New("ConcurrencyLevel must be non-negative")
}
return nil
}
// Decoder holds the necessary state to decode images.
// This is safe to be used from multiple goroutines.
type Decoder struct {
sem chan struct{}
opts DecoderOptions
}
// NewDecoder creates and returns a new image decoder with the given options.
func NewDecoder(opts DecoderOptions) (*Decoder, error) {
var d Decoder
if err := opts.validate(); err != nil {
return nil, fmt.Errorf("imaging: error validating decoder options: %w", err)
}
if opts.ConcurrencyLevel > 0 {
d.sem = make(chan struct{}, opts.ConcurrencyLevel)
}
d.opts = opts
return &d, nil
}
// Decode decodes the given encoded data and returns the decoded image.
func (d *Decoder) Decode(rd io.Reader) (img image.Image, format string, err error) {
if d.opts.ConcurrencyLevel != 0 {
d.sem <- struct{}{}
defer func() { <-d.sem }()
}
img, format, err = image.Decode(rd)
if err != nil {
return nil, "", fmt.Errorf("imaging: failed to decode image: %w", err)
}
return img, format, nil
}
// DecodeMemBounded works similarly to Decode but also returns a release function that
// must be called when access to the raw image is not needed anymore.
// This sets the raw image data pointer to nil in an attempt to help the GC to re-use the underlying data as soon as possible.
func (d *Decoder) DecodeMemBounded(rd io.Reader) (img image.Image, format string, releaseFunc func(), err error) {
if d.opts.ConcurrencyLevel != 0 {
d.sem <- struct{}{}
defer func() {
if err != nil {
<-d.sem
}
}()
}
img, format, err = image.Decode(rd)
if err != nil {
return nil, "", nil, fmt.Errorf("imaging: failed to decode image: %w", err)
}
var once sync.Once
releaseFunc = func() {
if d.opts.ConcurrencyLevel == 0 {
return
}
once.Do(func() {
if img != nil {
releaseImageData(img)
}
<-d.sem
})
}
return img, format, releaseFunc, nil
}
// DecodeConfig returns the image config for the given data.
func (d *Decoder) DecodeConfig(rd io.Reader) (image.Config, string, error) {
img, format, err := image.DecodeConfig(rd)
if err != nil {
return image.Config{}, "", fmt.Errorf("imaging: failed to decode image config: %w", err)
}
return img, format, nil
}
// GetDimensions returns the dimensions for the given encoded image data.
func GetDimensions(imageData io.Reader) (int, int, error) {
cfg, _, err := image.DecodeConfig(imageData)
if seeker, ok := imageData.(io.ReadSeeker); ok {
defer seeker.Seek(0, 0)
}
return cfg.Width, cfg.Height, err
}
// This is only needed to try and simplify GC work.
func releaseImageData(img image.Image) {
switch raw := img.(type) {
case *image.Alpha:
raw.Pix = nil
case *image.Alpha16:
raw.Pix = nil
case *image.Gray:
raw.Pix = nil
case *image.Gray16:
raw.Pix = nil
case *image.NRGBA:
raw.Pix = nil
case *image.NRGBA64:
raw.Pix = nil
case *image.Paletted:
raw.Pix = nil
case *image.RGBA:
raw.Pix = nil
case *image.RGBA64:
raw.Pix = nil
default:
return
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"errors"
"fmt"
"image"
"io"
"image/jpeg"
"image/png"
)
// EncoderOptions holds configuration options for an image encoder.
type EncoderOptions struct {
// The level of concurrency for the encoder. This defines a limit on the
// number of concurrently running encoding goroutines.
ConcurrencyLevel int
}
func (o *EncoderOptions) validate() error {
if o.ConcurrencyLevel < 0 {
return errors.New("ConcurrencyLevel must be non-negative")
}
return nil
}
// Decoder holds the necessary state to encode images.
// This is safe to be used from multiple goroutines.
type Encoder struct {
sem chan struct{}
opts EncoderOptions
pngEncoder *png.Encoder
}
// NewEncoder creates and returns a new image encoder with the given options.
func NewEncoder(opts EncoderOptions) (*Encoder, error) {
var e Encoder
if err := opts.validate(); err != nil {
return nil, fmt.Errorf("imaging: error validating encoder options: %w", err)
}
if opts.ConcurrencyLevel > 0 {
e.sem = make(chan struct{}, opts.ConcurrencyLevel)
}
e.opts = opts
e.pngEncoder = &png.Encoder{
CompressionLevel: png.BestCompression,
}
return &e, nil
}
// EncodeJPEG encodes the given image in JPEG format and writes the data to
// the passed writer.
func (e *Encoder) EncodeJPEG(wr io.Writer, img image.Image, quality int) error {
if e.opts.ConcurrencyLevel > 0 {
e.sem <- struct{}{}
defer func() {
<-e.sem
}()
}
var encOpts jpeg.Options
encOpts.Quality = quality
if err := jpeg.Encode(wr, img, &encOpts); err != nil {
return fmt.Errorf("imaging: failed to encode jpeg: %w", err)
}
return nil
}
// EncodePNG encodes the given image in PNG format and writes the data to
// the passed writer.
func (e *Encoder) EncodePNG(wr io.Writer, img image.Image) error {
if e.opts.ConcurrencyLevel > 0 {
e.sem <- struct{}{}
defer func() {
<-e.sem
}()
}
if err := e.pngEncoder.Encode(wr, img); err != nil {
return fmt.Errorf("imaging: failed to encode png: %w", err)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"fmt"
"image"
"io"
"github.com/disintegration/imaging"
"github.com/rwcarlsen/goexif/exif"
)
const (
/*
EXIF Image Orientations
1 2 3 4 5 6 7 8
888888 888888 88 88 8888888888 88 88 8888888888
88 88 88 88 88 88 88 88 88 88 88 88
8888 8888 8888 8888 88 8888888888 8888888888 88
88 88 88 88
88 88 888888 888888
*/
Upright = iota + 1
UprightMirrored
UpsideDown
UpsideDownMirrored
RotatedCWMirrored
RotatedCCW
RotatedCCWMirrored
RotatedCW
)
// MakeImageUpright changes the orientation of the given image.
func MakeImageUpright(img image.Image, orientation int) image.Image {
switch orientation {
case UprightMirrored:
return imaging.FlipH(img)
case UpsideDown:
return imaging.Rotate180(img)
case UpsideDownMirrored:
return imaging.FlipV(img)
case RotatedCWMirrored:
return imaging.Transpose(img)
case RotatedCCW:
return imaging.Rotate270(img)
case RotatedCCWMirrored:
return imaging.Transverse(img)
case RotatedCW:
return imaging.Rotate90(img)
default:
return img
}
}
// GetImageOrientation reads the input data and returns the EXIF encoded
// image orientation.
func GetImageOrientation(input io.Reader) (int, error) {
exifData, err := exif.Decode(input)
if err != nil {
return Upright, fmt.Errorf("failed to decode exif data: %w", err)
}
tag, err := exifData.Get("Orientation")
if err != nil {
return Upright, fmt.Errorf("failed to get orientation field from exif data: %w", err)
}
orientation, err := tag.Int(0)
if err != nil {
return Upright, fmt.Errorf("failed to get value from exif tag: %w", err)
}
return orientation, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"bytes"
"fmt"
"image"
"image/jpeg"
"github.com/disintegration/imaging"
)
// GeneratePreview generates the preview for the given image.
func GeneratePreview(img image.Image, width int) image.Image {
preview := img
w := img.Bounds().Dx()
if w > width {
preview = imaging.Resize(img, width, 0, imaging.Lanczos)
}
return preview
}
// GenerateThumbnail generates the thumbnail for the given image.
func GenerateThumbnail(img image.Image, width, height int) image.Image {
thumb := img
w := img.Bounds().Dx()
h := img.Bounds().Dy()
expectedRatio := float64(height) / float64(width)
if h > height || w > width {
ratio := float64(h) / float64(w)
if ratio < expectedRatio {
// we pre-calculate the thumbnail's width to make sure we are not upscaling.
targetWidth := int(float64(height) * float64(w) / float64(h))
if targetWidth <= w {
thumb = imaging.Resize(img, 0, height, imaging.Lanczos)
} else {
thumb = imaging.Resize(img, width, 0, imaging.Lanczos)
}
} else {
// we pre-calculate the thumbnail's height to make sure we are not upscaling.
targetHeight := int(float64(width) * float64(h) / float64(w))
if targetHeight <= h {
thumb = imaging.Resize(img, width, 0, imaging.Lanczos)
} else {
thumb = imaging.Resize(img, 0, height, imaging.Lanczos)
}
}
}
return thumb
}
// GenerateMiniPreviewImage generates the mini preview for the given image.
func GenerateMiniPreviewImage(img image.Image, w, h, q int) ([]byte, error) {
var buf bytes.Buffer
preview := imaging.Resize(img, w, h, imaging.Lanczos)
if err := jpeg.Encode(&buf, preview, &jpeg.Options{Quality: q}); err != nil {
return nil, fmt.Errorf("failed to encode image to JPEG format: %w", err)
}
return buf.Bytes(), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"encoding/xml"
"fmt"
"io"
"strings"
"github.com/pkg/errors"
)
// SVGInfo holds information for a SVG image.
type SVGInfo struct {
Width int
Height int
}
// ParseSVG returns information for the given SVG input data.
func ParseSVG(svgReader io.Reader) (SVGInfo, error) {
svgInfo := SVGInfo{
Width: 0,
Height: 0,
}
decoder := xml.NewDecoder(svgReader)
for {
token, err := decoder.Token()
if err != nil {
return svgInfo, err
}
switch t := token.(type) {
case xml.StartElement:
for _, attr := range t.Attr {
if attr.Name.Local == "viewBox" {
values := strings.Fields(attr.Value)
if len(values) == 4 {
width := 0
_, widthErr := fmt.Sscan(values[2], &width)
height := 0
_, heightErr := fmt.Sscan(values[3], &height)
if widthErr != nil || heightErr != nil {
return svgInfo, err
}
svgInfo.Width = width
svgInfo.Height = height
return svgInfo, nil
}
}
if attr.Name.Local == "width" {
width := 0
_, err := fmt.Sscan(attr.Value, &width)
if err != nil {
return svgInfo, err
}
svgInfo.Width = width
}
if attr.Name.Local == "height" {
height := 0
_, err := fmt.Sscan(attr.Value, &height)
if err != nil {
return svgInfo, err
}
svgInfo.Height = height
}
}
if svgInfo.Width == 0 || svgInfo.Height == 0 {
return svgInfo, errors.New("unable to extract SVG dimensions")
}
return svgInfo, nil
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imaging
import (
"image"
"image/color"
"github.com/disintegration/imaging"
)
type rawImg interface {
Set(x, y int, c color.Color)
Opaque() bool
}
func isFullyTransparent(c color.Color) bool {
// TODO: This can be optimized by checking the color type and
// only extract the needed alpha value.
_, _, _, a := c.RGBA()
return a == 0
}
// FillImageTransparency fills in-place all the fully transparent pixels of the
// input image with the given color.
func FillImageTransparency(img image.Image, c color.Color) {
var i rawImg
bounds := img.Bounds()
fillFunc := func() {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
if isFullyTransparent(img.At(x, y)) {
i.Set(x, y, c)
}
}
}
}
switch raw := img.(type) {
case *image.Alpha:
i = raw
case *image.Alpha16:
i = raw
case *image.Gray:
i = raw
case *image.Gray16:
i = raw
case *image.NRGBA:
i = raw
col := color.NRGBAModel.Convert(c).(color.NRGBA)
fillFunc = func() {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
i := raw.PixOffset(x, y)
if raw.Pix[i+3] == 0x00 {
raw.Pix[i] = col.R
raw.Pix[i+1] = col.G
raw.Pix[i+2] = col.B
raw.Pix[i+3] = col.A
}
}
}
}
case *image.NRGBA64:
i = raw
col := color.NRGBA64Model.Convert(c).(color.NRGBA64)
fillFunc = func() {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
i := raw.PixOffset(x, y)
a := uint16(raw.Pix[i+6])<<8 | uint16(raw.Pix[i+7])
if a == 0 {
raw.Pix[i] = uint8(col.R >> 8)
raw.Pix[i+1] = uint8(col.R)
raw.Pix[i+2] = uint8(col.G >> 8)
raw.Pix[i+3] = uint8(col.G)
raw.Pix[i+4] = uint8(col.B >> 8)
raw.Pix[i+5] = uint8(col.B)
raw.Pix[i+6] = uint8(col.A >> 8)
raw.Pix[i+7] = uint8(col.A)
}
}
}
}
case *image.Paletted:
i = raw
fillFunc = func() {
for i := range raw.Palette {
if isFullyTransparent(raw.Palette[i]) {
raw.Palette[i] = c
}
}
}
case *image.RGBA:
i = raw
col := color.RGBAModel.Convert(c).(color.RGBA)
fillFunc = func() {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
i := raw.PixOffset(x, y)
if raw.Pix[i+3] == 0x00 {
raw.Pix[i] = col.R
raw.Pix[i+1] = col.G
raw.Pix[i+2] = col.B
raw.Pix[i+3] = col.A
}
}
}
}
case *image.RGBA64:
i = raw
col := color.RGBA64Model.Convert(c).(color.RGBA64)
fillFunc = func() {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
i := raw.PixOffset(x, y)
a := uint16(raw.Pix[i+6])<<8 | uint16(raw.Pix[i+7])
if a == 0 {
raw.Pix[i] = uint8(col.R >> 8)
raw.Pix[i+1] = uint8(col.R)
raw.Pix[i+2] = uint8(col.G >> 8)
raw.Pix[i+3] = uint8(col.G)
raw.Pix[i+4] = uint8(col.B >> 8)
raw.Pix[i+5] = uint8(col.B)
raw.Pix[i+6] = uint8(col.A >> 8)
raw.Pix[i+7] = uint8(col.A)
}
}
}
}
default:
return
}
if !i.Opaque() {
fillFunc()
}
}
// FillCenter creates an image with the specified dimensions and fills it with
// the centered and scaled source image.
func FillCenter(img image.Image, w, h int) *image.NRGBA {
return imaging.Fill(img, w, h, imaging.Center, imaging.Lanczos)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"archive/zip"
"bufio"
"encoding/json"
"fmt"
"io"
"net/http"
"path/filepath"
"strings"
"sync"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/imports"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type ReactionImportData = imports.ReactionImportData // part of the app interface
const (
importMultiplePostsThreshold = 1000
maxScanTokenSize = 16 * 1024 * 1024 // Need to set a higher limit than default because some customers cross the limit. See MM-22314
statusUpdateAfterLines = 8192
)
func stopOnError(c request.CTX, err imports.LineImportWorkerError) bool {
switch err.Error.Id {
case "api.file.upload_file.large_image.app_error":
c.Logger().Warn("Large image import error", mlog.Err(err.Error))
return false
case "app.import.validate_direct_channel_import_data.members_too_few.error", "app.import.validate_direct_channel_import_data.members_too_many.error":
c.Logger().Warn("Invalid direct channel import data", mlog.Err(err.Error))
return false
default:
return true
}
}
func processAttachmentPaths(c request.CTX, files *[]imports.AttachmentImportData, basePath string, filesMap map[string]*zip.File) error {
if files == nil {
return nil
}
var ok bool
for i, f := range *files {
if f.Path != nil {
path := filepath.Join(basePath, *f.Path)
*f.Path = path
if len(filesMap) > 0 {
if (*files)[i].Data, ok = filesMap[path]; !ok {
return fmt.Errorf("attachment %q not found in map", path)
}
}
}
}
return nil
}
func processAttachments(c request.CTX, line *imports.LineImportData, basePath string, filesMap map[string]*zip.File) error {
var ok bool
switch line.Type {
case "post", "direct_post":
var replies []imports.ReplyImportData
if line.Type == "direct_post" {
if err := processAttachmentPaths(c, line.DirectPost.Attachments, basePath, filesMap); err != nil {
return err
}
if line.DirectPost.Replies != nil {
replies = *line.DirectPost.Replies
}
} else {
if err := processAttachmentPaths(c, line.Post.Attachments, basePath, filesMap); err != nil {
return err
}
if line.Post.Replies != nil {
replies = *line.Post.Replies
}
}
for _, reply := range replies {
if err := processAttachmentPaths(c, reply.Attachments, basePath, filesMap); err != nil {
return err
}
}
case "user":
if line.User.ProfileImage != nil {
path := filepath.Join(basePath, *line.User.ProfileImage)
*line.User.ProfileImage = path
if len(filesMap) > 0 {
if line.User.ProfileImageData, ok = filesMap[path]; !ok {
return fmt.Errorf("attachment %q not found in map", path)
}
}
}
case "emoji":
if line.Emoji.Image != nil {
path := filepath.Join(basePath, *line.Emoji.Image)
*line.Emoji.Image = path
if len(filesMap) > 0 {
if line.Emoji.Data, ok = filesMap[path]; !ok {
return fmt.Errorf("attachment %q not found in map", path)
}
}
}
}
return nil
}
func (a *App) bulkImportWorker(c request.CTX, dryRun bool, wg *sync.WaitGroup, lines <-chan imports.LineImportWorkerData, errors chan<- imports.LineImportWorkerError) {
workerID := model.NewId()
processedLines := uint64(0)
c.Logger().Info("Started new bulk import worker", mlog.String("bulk_import_worker_id", workerID))
defer func() {
wg.Done()
c.Logger().Info("Bulk import worker finished", mlog.String("bulk_import_worker_id", workerID), mlog.Uint64("processed_lines", processedLines))
}()
postLines := []imports.LineImportWorkerData{}
directPostLines := []imports.LineImportWorkerData{}
for line := range lines {
switch {
case line.LineImportData.Type == "post":
postLines = append(postLines, line)
if line.Post == nil {
errors <- imports.LineImportWorkerError{Error: model.NewAppError("BulkImport", "app.import.import_line.null_post.error", nil, "", http.StatusBadRequest), LineNumber: line.LineNumber}
}
if len(postLines) >= importMultiplePostsThreshold {
if errLine, err := a.importMultiplePostLines(c, postLines, dryRun); err != nil {
errors <- imports.LineImportWorkerError{Error: err, LineNumber: errLine}
}
postLines = []imports.LineImportWorkerData{}
}
case line.LineImportData.Type == "direct_post":
directPostLines = append(directPostLines, line)
if line.DirectPost == nil {
errors <- imports.LineImportWorkerError{Error: model.NewAppError("BulkImport", "app.import.import_line.null_direct_post.error", nil, "", http.StatusBadRequest), LineNumber: line.LineNumber}
}
if len(directPostLines) >= importMultiplePostsThreshold {
if errLine, err := a.importMultipleDirectPostLines(c, directPostLines, dryRun); err != nil {
errors <- imports.LineImportWorkerError{Error: err, LineNumber: errLine}
}
directPostLines = []imports.LineImportWorkerData{}
}
default:
if err := a.importLine(c, line.LineImportData, dryRun); err != nil {
errors <- imports.LineImportWorkerError{Error: err, LineNumber: line.LineNumber}
}
}
processedLines++
if processedLines%statusUpdateAfterLines == 0 {
c.Logger().Info("Worker progress", mlog.String("bulk_import_worker_id", workerID), mlog.Uint64("processed_lines", processedLines))
}
}
if len(postLines) > 0 {
if errLine, err := a.importMultiplePostLines(c, postLines, dryRun); err != nil {
errors <- imports.LineImportWorkerError{Error: err, LineNumber: errLine}
}
}
if len(directPostLines) > 0 {
if errLine, err := a.importMultipleDirectPostLines(c, directPostLines, dryRun); err != nil {
errors <- imports.LineImportWorkerError{Error: err, LineNumber: errLine}
}
}
}
func (a *App) BulkImport(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int) (*model.AppError, int) {
return a.bulkImport(c, jsonlReader, attachmentsReader, dryRun, workers, "")
}
func (a *App) BulkImportWithPath(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int, importPath string) (*model.AppError, int) {
return a.bulkImport(c, jsonlReader, attachmentsReader, dryRun, workers, importPath)
}
// bulkImport will extract attachments from attachmentsReader if it is
// not nil. If it is nil, it will look for attachments on the
// filesystem in the locations specified by the JSONL file according
// to the older behavior
func (a *App) bulkImport(c request.CTX, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int, importPath string) (*model.AppError, int) {
scanner := bufio.NewScanner(jsonlReader)
buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, maxScanTokenSize)
lineNumber := 0
a.Srv().Store().LockToMaster()
defer a.Srv().Store().UnlockFromMaster()
errorsChan := make(chan imports.LineImportWorkerError, (2*workers)+1) // size chosen to ensure it never gets filled up completely.
var wg sync.WaitGroup
var linesChan chan imports.LineImportWorkerData
lastLineType := ""
var attachedFiles map[string]*zip.File
if attachmentsReader != nil {
attachedFiles = make(map[string]*zip.File, len(attachmentsReader.File))
for _, fi := range attachmentsReader.File {
attachedFiles[fi.Name] = fi
}
}
for scanner.Scan() {
lineNumber++
if lineNumber%statusUpdateAfterLines == 0 {
c.Logger().Info("Reader progress", mlog.Int("processed_lines", lineNumber))
}
var line imports.LineImportData
if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
return model.NewAppError("BulkImport", "app.import.bulk_import.json_decode.error", nil, "", http.StatusBadRequest).Wrap(err), lineNumber
}
if err := processAttachments(c, &line, importPath, attachedFiles); err != nil {
c.Logger().Warn("Error while processing import attachments. Objects might be broken.", mlog.Err(err))
}
if lineNumber == 1 {
importDataFileVersion, appErr := processImportDataFileVersionLine(line)
if appErr != nil {
return appErr, lineNumber
}
if importDataFileVersion != 1 {
return model.NewAppError("BulkImport", "app.import.bulk_import.unsupported_version.error", nil, "", http.StatusBadRequest), lineNumber
}
lastLineType = line.Type
continue
}
if line.Type != lastLineType {
// Only clear the worker queue if is not the first data entry
if lineNumber != 2 {
c.Logger().Info(
"Finished parsing segment, waiting for workers to finish",
mlog.String("old_segment", lastLineType),
mlog.String("new_segment", line.Type),
)
// Changing type. Clear out the worker queue before continuing.
close(linesChan)
wg.Wait()
// Check no errors occurred while waiting for the queue to empty.
if len(errorsChan) != 0 {
err := <-errorsChan
if stopOnError(c, err) {
return err.Error, err.LineNumber
}
}
}
c.Logger().Info(
"Starting workers for new segment",
mlog.String("old_segment", lastLineType),
mlog.String("new_segment", line.Type),
mlog.Int("workers", workers),
)
// Set up the workers and channel for this type.
lastLineType = line.Type
linesChan = make(chan imports.LineImportWorkerData, workers)
for i := 0; i < workers; i++ {
wg.Add(1)
go a.bulkImportWorker(c, dryRun, &wg, linesChan, errorsChan)
}
}
select {
case linesChan <- imports.LineImportWorkerData{LineImportData: line, LineNumber: lineNumber}:
case err := <-errorsChan:
if stopOnError(c, err) {
close(linesChan)
wg.Wait()
return err.Error, err.LineNumber
}
}
}
// No more lines. Clear out the worker queue before continuing.
if linesChan != nil {
close(linesChan)
}
wg.Wait()
// Check no errors occurred while waiting for the queue to empty.
if len(errorsChan) != 0 {
err := <-errorsChan
if stopOnError(c, err) {
return err.Error, err.LineNumber
}
}
if err := scanner.Err(); err != nil {
return model.NewAppError("BulkImport", "app.import.bulk_import.file_scan.error", nil, "", http.StatusInternalServerError).Wrap(err), 0
}
return nil, 0
}
func processImportDataFileVersionLine(line imports.LineImportData) (int, *model.AppError) {
if line.Type != "version" || line.Version == nil {
return -1, model.NewAppError("BulkImport", "app.import.process_import_data_file_version_line.invalid_version.error", nil, "", http.StatusBadRequest)
}
return *line.Version, nil
}
func (a *App) importLine(c request.CTX, line imports.LineImportData, dryRun bool) *model.AppError {
switch {
case line.Type == "scheme":
if line.Scheme == nil {
return model.NewAppError("BulkImport", "app.import.import_line.null_scheme.error", nil, "", http.StatusBadRequest)
}
return a.importScheme(c, line.Scheme, dryRun)
case line.Type == "team":
if line.Team == nil {
return model.NewAppError("BulkImport", "app.import.import_line.null_team.error", nil, "", http.StatusBadRequest)
}
return a.importTeam(c, line.Team, dryRun)
case line.Type == "channel":
if line.Channel == nil {
return model.NewAppError("BulkImport", "app.import.import_line.null_channel.error", nil, "", http.StatusBadRequest)
}
return a.importChannel(c, line.Channel, dryRun)
case line.Type == "user":
if line.User == nil {
return model.NewAppError("BulkImport", "app.import.import_line.null_user.error", nil, "", http.StatusBadRequest)
}
return a.importUser(c, line.User, dryRun)
case line.Type == "direct_channel":
if line.DirectChannel == nil {
return model.NewAppError("BulkImport", "app.import.import_line.null_direct_channel.error", nil, "", http.StatusBadRequest)
}
return a.importDirectChannel(c, line.DirectChannel, dryRun)
case line.Type == "emoji":
if line.Emoji == nil {
return model.NewAppError("BulkImport", "app.import.import_line.null_emoji.error", nil, "", http.StatusBadRequest)
}
return a.importEmoji(c, line.Emoji, dryRun)
default:
return model.NewAppError("BulkImport", "app.import.import_line.unknown_line_type.error", map[string]any{"Type": line.Type}, "", http.StatusBadRequest)
}
}
func (a *App) ListImports() ([]string, *model.AppError) {
imports, appErr := a.ListDirectory(*a.Config().ImportSettings.Directory)
if appErr != nil {
return nil, appErr
}
results := make([]string, 0, len(imports))
for i := 0; i < len(imports); i++ {
filename := filepath.Base(imports[i])
if !strings.HasSuffix(filename, model.IncompleteUploadSuffix) {
results = append(results, filename)
}
}
return results, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"context"
"crypto/sha1"
"errors"
"fmt"
"io"
"net/http"
"os"
"path"
"strings"
"github.com/mattermost/logr/v2"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/imports"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/app/teams"
"github.com/mattermost/mattermost-server/v6/server/channels/app/users"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// -- Bulk Import Functions --
// These functions import data directly into the database. Security and permission checks are bypassed but validity is
// still enforced.
func (a *App) importScheme(c request.CTX, data *imports.SchemeImportData, dryRun bool) *model.AppError {
var fields []logr.Field
if data != nil && data.Name != nil {
fields = append(fields, mlog.String("schema_name", *data.Name))
}
c.Logger().Info("Validating schema", fields...)
if err := imports.ValidateSchemeImportData(data); err != nil {
return err
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
c.Logger().Info("Importing schema", fields...)
scheme, err := a.GetSchemeByName(*data.Name)
if err != nil {
scheme = new(model.Scheme)
} else if scheme.Scope != *data.Scope {
return model.NewAppError("BulkImport", "app.import.import_scheme.scope_change.error", map[string]any{"SchemeName": scheme.Name}, "", http.StatusBadRequest)
}
scheme.Name = *data.Name
scheme.DisplayName = *data.DisplayName
scheme.Scope = *data.Scope
if data.Description != nil {
scheme.Description = *data.Description
}
if scheme.Id == "" {
scheme, err = a.CreateScheme(scheme)
} else {
scheme, err = a.UpdateScheme(scheme)
}
if err != nil {
return err
}
if scheme.Scope == model.SchemeScopeTeam {
data.DefaultTeamAdminRole.Name = &scheme.DefaultTeamAdminRole
if err := a.importRole(c, data.DefaultTeamAdminRole, dryRun, true); err != nil {
return err
}
data.DefaultTeamUserRole.Name = &scheme.DefaultTeamUserRole
if err := a.importRole(c, data.DefaultTeamUserRole, dryRun, true); err != nil {
return err
}
if data.DefaultTeamGuestRole == nil {
data.DefaultTeamGuestRole = &imports.RoleImportData{
DisplayName: model.NewString("Team Guest Role for Scheme"),
}
}
data.DefaultTeamGuestRole.Name = &scheme.DefaultTeamGuestRole
if err := a.importRole(c, data.DefaultTeamGuestRole, dryRun, true); err != nil {
return err
}
}
if scheme.Scope == model.SchemeScopeTeam || scheme.Scope == model.SchemeScopeChannel {
data.DefaultChannelAdminRole.Name = &scheme.DefaultChannelAdminRole
if err := a.importRole(c, data.DefaultChannelAdminRole, dryRun, true); err != nil {
return err
}
data.DefaultChannelUserRole.Name = &scheme.DefaultChannelUserRole
if err := a.importRole(c, data.DefaultChannelUserRole, dryRun, true); err != nil {
return err
}
if data.DefaultChannelGuestRole == nil {
data.DefaultChannelGuestRole = &imports.RoleImportData{
DisplayName: model.NewString("Channel Guest Role for Scheme"),
}
}
data.DefaultChannelGuestRole.Name = &scheme.DefaultChannelGuestRole
if err := a.importRole(c, data.DefaultChannelGuestRole, dryRun, true); err != nil {
return err
}
}
return nil
}
func (a *App) importRole(c request.CTX, data *imports.RoleImportData, dryRun bool, isSchemeRole bool) *model.AppError {
var fields []logr.Field
if data != nil && data.Name != nil {
fields = append(fields, mlog.String("role_name", *data.Name))
}
if !isSchemeRole {
c.Logger().Info("Validating role", fields...)
if err := imports.ValidateRoleImportData(data); err != nil {
return err
}
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
c.Logger().Info("Importing role", fields...)
role, err := a.GetRoleByName(context.Background(), *data.Name)
if err != nil {
role = new(model.Role)
}
role.Name = *data.Name
if data.DisplayName != nil {
role.DisplayName = *data.DisplayName
}
if data.Description != nil {
role.Description = *data.Description
}
if data.Permissions != nil {
role.Permissions = *data.Permissions
}
if isSchemeRole {
role.SchemeManaged = true
} else {
role.SchemeManaged = false
}
if role.Id == "" {
_, err = a.CreateRole(role)
} else {
_, err = a.UpdateRole(role)
}
return err
}
func (a *App) importTeam(c request.CTX, data *imports.TeamImportData, dryRun bool) *model.AppError {
var fields []logr.Field
if data != nil && data.Name != nil {
fields = append(fields, mlog.String("team_name", *data.Name))
}
c.Logger().Info("Validating team", fields...)
if err := imports.ValidateTeamImportData(data); err != nil {
return err
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
c.Logger().Info("Importing team", fields...)
var team *model.Team
team, err := a.Srv().Store().Team().GetByName(*data.Name)
if err != nil {
team = &model.Team{}
}
team.Name = *data.Name
team.DisplayName = *data.DisplayName
team.Type = *data.Type
if data.Description != nil {
team.Description = *data.Description
}
if data.AllowOpenInvite != nil {
team.AllowOpenInvite = *data.AllowOpenInvite
}
if data.Scheme != nil {
scheme, err := a.GetSchemeByName(*data.Scheme)
if err != nil {
return err
}
if scheme.DeleteAt != 0 {
return model.NewAppError("BulkImport", "app.import.import_team.scheme_deleted.error", nil, "", http.StatusBadRequest)
}
if scheme.Scope != model.SchemeScopeTeam {
return model.NewAppError("BulkImport", "app.import.import_team.scheme_wrong_scope.error", nil, "", http.StatusBadRequest)
}
team.SchemeId = &scheme.Id
}
if team.Id == "" {
if _, err := a.CreateTeam(c, team); err != nil {
return err
}
} else {
if _, err := a.ch.srv.teamService.UpdateTeam(team, teams.UpdateOptions{Imported: true}); err != nil {
var invErr *store.ErrInvalidInput
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return model.NewAppError("BulkImport", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &invErr):
return model.NewAppError("BulkImport", "app.team.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return model.NewAppError("BulkImport", "app.team.update.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
return nil
}
func (a *App) importChannel(c request.CTX, data *imports.ChannelImportData, dryRun bool) *model.AppError {
var fields []logr.Field
if data != nil && data.Name != nil {
fields = append(fields, mlog.String("channel_name", *data.Name))
}
c.Logger().Info("Validating channel", fields...)
if err := imports.ValidateChannelImportData(data); err != nil {
return err
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
c.Logger().Info("Importing channel", fields...)
team, err := a.Srv().Store().Team().GetByName(*data.Team)
if err != nil {
return model.NewAppError("BulkImport", "app.import.import_channel.team_not_found.error", map[string]any{"TeamName": *data.Team}, "", http.StatusBadRequest).Wrap(err)
}
var channel *model.Channel
if result, err := a.Srv().Store().Channel().GetByNameIncludeDeleted(team.Id, *data.Name, true); err == nil {
channel = result
} else {
channel = &model.Channel{}
}
channel.TeamId = team.Id
channel.Name = *data.Name
channel.DisplayName = *data.DisplayName
channel.Type = *data.Type
if data.Header != nil {
channel.Header = *data.Header
}
if data.Purpose != nil {
channel.Purpose = *data.Purpose
}
if data.Scheme != nil {
scheme, err := a.GetSchemeByName(*data.Scheme)
if err != nil {
return err
}
if scheme.DeleteAt != 0 {
return model.NewAppError("BulkImport", "app.import.import_channel.scheme_deleted.error", nil, "", http.StatusBadRequest)
}
if scheme.Scope != model.SchemeScopeChannel {
return model.NewAppError("BulkImport", "app.import.import_channel.scheme_wrong_scope.error", nil, "", http.StatusBadRequest)
}
channel.SchemeId = &scheme.Id
}
if channel.Id == "" {
if _, err := a.CreateChannel(c, channel, false); err != nil {
return err
}
} else {
if _, err := a.UpdateChannel(c, channel); err != nil {
return err
}
}
return nil
}
func (a *App) importUser(c request.CTX, data *imports.UserImportData, dryRun bool) *model.AppError {
var fields []logr.Field
if data != nil && data.Username != nil {
fields = append(fields, mlog.String("user_name", *data.Username))
}
c.Logger().Info("Validating user", fields...)
if err := imports.ValidateUserImportData(data); err != nil {
return err
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
c.Logger().Info("Importing user", fields...)
// We want to avoid database writes if nothing has changed.
hasUserChanged := false
hasNotifyPropsChanged := false
hasUserRolesChanged := false
hasUserAuthDataChanged := false
hasUserEmailVerifiedChanged := false
var user *model.User
var nErr error
user, nErr = a.Srv().Store().User().GetByUsername(*data.Username)
if nErr != nil {
user = &model.User{}
user.MakeNonNil()
user.SetDefaultNotifications()
hasUserChanged = true
}
user.Username = *data.Username
if user.Email != *data.Email {
hasUserChanged = true
hasUserEmailVerifiedChanged = true // Changing the email resets email verified to false by default.
user.Email = *data.Email
user.Email = strings.ToLower(user.Email)
}
var password string
var authService string
var authData *string
if data.AuthService != nil {
if user.AuthService != *data.AuthService {
hasUserAuthDataChanged = true
}
authService = *data.AuthService
}
// AuthData and Password are mutually exclusive.
if data.AuthData != nil {
if user.AuthData == nil || *user.AuthData != *data.AuthData {
hasUserAuthDataChanged = true
}
authData = data.AuthData
password = ""
} else if data.Password != nil {
password = *data.Password
authData = nil
} else {
var err error
// If no AuthData or Password is specified, we must generate a password.
password, err = generatePassword(*a.Config().PasswordSettings.MinimumLength)
if err != nil {
return model.NewAppError("importUser", "app.import.generate_password.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
authData = nil
}
user.Password = password
user.AuthService = authService
user.AuthData = authData
// Automatically assume all emails are verified.
emailVerified := true
if user.EmailVerified != emailVerified {
user.EmailVerified = emailVerified
hasUserEmailVerifiedChanged = true
}
if data.Nickname != nil {
if user.Nickname != *data.Nickname {
user.Nickname = *data.Nickname
hasUserChanged = true
}
}
if data.FirstName != nil {
if user.FirstName != *data.FirstName {
user.FirstName = *data.FirstName
hasUserChanged = true
}
}
if data.LastName != nil {
if user.LastName != *data.LastName {
user.LastName = *data.LastName
hasUserChanged = true
}
}
if data.Position != nil {
if user.Position != *data.Position {
user.Position = *data.Position
hasUserChanged = true
}
}
if data.Locale != nil {
if user.Locale != *data.Locale {
user.Locale = *data.Locale
hasUserChanged = true
}
} else {
if user.Locale != *a.Config().LocalizationSettings.DefaultClientLocale {
user.Locale = *a.Config().LocalizationSettings.DefaultClientLocale
hasUserChanged = true
}
}
if data.DeleteAt != nil {
if user.DeleteAt != *data.DeleteAt {
user.DeleteAt = *data.DeleteAt
hasUserChanged = true
}
}
var roles string
if data.Roles != nil {
if user.Roles != *data.Roles {
roles = *data.Roles
hasUserRolesChanged = true
}
} else if user.Roles == "" {
// Set SYSTEM_USER roles on newly created users by default.
if user.Roles != model.SystemUserRoleId {
roles = model.SystemUserRoleId
hasUserRolesChanged = true
}
}
user.Roles = roles
if data.NotifyProps != nil {
if data.NotifyProps.Desktop != nil {
if value, ok := user.NotifyProps[model.DesktopNotifyProp]; !ok || value != *data.NotifyProps.Desktop {
user.AddNotifyProp(model.DesktopNotifyProp, *data.NotifyProps.Desktop)
hasNotifyPropsChanged = true
}
}
if data.NotifyProps.DesktopSound != nil {
if value, ok := user.NotifyProps[model.DesktopSoundNotifyProp]; !ok || value != *data.NotifyProps.DesktopSound {
user.AddNotifyProp(model.DesktopSoundNotifyProp, *data.NotifyProps.DesktopSound)
hasNotifyPropsChanged = true
}
}
if data.NotifyProps.Email != nil {
if value, ok := user.NotifyProps[model.EmailNotifyProp]; !ok || value != *data.NotifyProps.Email {
user.AddNotifyProp(model.EmailNotifyProp, *data.NotifyProps.Email)
hasNotifyPropsChanged = true
}
}
if data.NotifyProps.Mobile != nil {
if value, ok := user.NotifyProps[model.PushNotifyProp]; !ok || value != *data.NotifyProps.Mobile {
user.AddNotifyProp(model.PushNotifyProp, *data.NotifyProps.Mobile)
hasNotifyPropsChanged = true
}
}
if data.NotifyProps.MobilePushStatus != nil {
if value, ok := user.NotifyProps[model.PushStatusNotifyProp]; !ok || value != *data.NotifyProps.MobilePushStatus {
user.AddNotifyProp(model.PushStatusNotifyProp, *data.NotifyProps.MobilePushStatus)
hasNotifyPropsChanged = true
}
}
if data.NotifyProps.ChannelTrigger != nil {
if value, ok := user.NotifyProps[model.ChannelMentionsNotifyProp]; !ok || value != *data.NotifyProps.ChannelTrigger {
user.AddNotifyProp(model.ChannelMentionsNotifyProp, *data.NotifyProps.ChannelTrigger)
hasNotifyPropsChanged = true
}
}
if data.NotifyProps.CommentsTrigger != nil {
if value, ok := user.NotifyProps[model.CommentsNotifyProp]; !ok || value != *data.NotifyProps.CommentsTrigger {
user.AddNotifyProp(model.CommentsNotifyProp, *data.NotifyProps.CommentsTrigger)
hasNotifyPropsChanged = true
}
}
if data.NotifyProps.MentionKeys != nil {
if value, ok := user.NotifyProps[model.MentionKeysNotifyProp]; !ok || value != *data.NotifyProps.MentionKeys {
user.AddNotifyProp(model.MentionKeysNotifyProp, *data.NotifyProps.MentionKeys)
hasNotifyPropsChanged = true
}
} else {
user.UpdateMentionKeysFromUsername("")
}
}
var savedUser *model.User
var err error
if user.Id == "" {
if savedUser, err = a.ch.srv.userService.CreateUser(user, users.UserCreateOptions{FromImport: true}); err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return appErr
case errors.Is(err, users.AcceptedDomainError):
return model.NewAppError("importUser", "api.user.create_user.accepted_domain.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.Is(err, users.UserStoreIsEmptyError):
return model.NewAppError("importUser", "app.user.store_is_empty.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
case errors.As(err, &invErr):
switch invErr.Field {
case "email":
return model.NewAppError("importUser", "app.user.save.email_exists.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case "username":
return model.NewAppError("importUser", "app.user.save.username_exists.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return model.NewAppError("importUser", "app.user.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
default:
return model.NewAppError("importUser", "app.user.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
pref := model.Preference{UserId: savedUser.Id, Category: model.PreferenceCategoryTutorialSteps, Name: savedUser.Id, Value: "0"}
if err := a.Srv().Store().Preference().Save(model.Preferences{pref}); err != nil {
c.Logger().Warn("Encountered error saving tutorial preference", mlog.Err(err))
}
} else {
var appErr *model.AppError
if hasUserChanged {
if savedUser, appErr = a.UpdateUser(c, user, false); appErr != nil {
return appErr
}
}
if hasUserRolesChanged {
if savedUser, appErr = a.UpdateUserRoles(c, user.Id, roles, false); appErr != nil {
return appErr
}
}
if hasNotifyPropsChanged {
if appErr = a.updateUserNotifyProps(user.Id, user.NotifyProps); appErr != nil {
return appErr
}
if savedUser, appErr = a.GetUser(user.Id); appErr != nil {
return appErr
}
}
if password != "" {
if appErr = a.UpdatePassword(user, password); appErr != nil {
return appErr
}
} else {
if hasUserAuthDataChanged {
if _, nErr := a.Srv().Store().User().UpdateAuthData(user.Id, authService, authData, user.Email, false); nErr != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(nErr, &invErr):
return model.NewAppError("importUser", "app.user.update_auth_data.email_exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
default:
return model.NewAppError("importUser", "app.user.update_auth_data.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
}
}
if emailVerified {
if hasUserEmailVerifiedChanged {
if err := a.VerifyUserEmail(user.Id, user.Email); err != nil {
return err
}
}
}
}
if savedUser == nil {
savedUser = user
}
if data.ProfileImage != nil {
var file io.ReadCloser
var err error
if data.ProfileImageData != nil {
file, err = data.ProfileImageData.Open()
} else {
file, err = os.Open(*data.ProfileImage)
}
if err != nil {
c.Logger().Warn("Unable to open the profile image.", mlog.Err(err))
} else {
defer file.Close()
if limitErr := checkImageLimits(file, *a.Config().FileSettings.MaxImageResolution); limitErr != nil {
return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.check_image_limits.app_error", nil, "", http.StatusBadRequest)
}
if err := a.SetProfileImageFromFile(c, savedUser.Id, file); err != nil {
c.Logger().Warn("Unable to set the profile image from a file.", mlog.Err(err))
}
}
}
// Preferences.
var preferences model.Preferences
if data.Theme != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryTheme,
Name: "",
Value: *data.Theme,
})
}
if data.UseMilitaryTime != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameUseMilitaryTime,
Value: *data.UseMilitaryTime,
})
}
if data.CollapsePreviews != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameCollapseSetting,
Value: *data.CollapsePreviews,
})
}
if data.MessageDisplay != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameMessageDisplay,
Value: *data.MessageDisplay,
})
}
if data.CollapseConsecutive != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameCollapseConsecutive,
Value: *data.CollapseConsecutive,
})
}
if data.ColorizeUsernames != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameColorizeUsernames,
Value: *data.ColorizeUsernames,
})
}
if data.ChannelDisplayMode != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryDisplaySettings,
Name: "channel_display_mode",
Value: *data.ChannelDisplayMode,
})
}
if data.TutorialStep != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryTutorialSteps,
Name: savedUser.Id,
Value: *data.TutorialStep,
})
}
if data.UseMarkdownPreview != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryAdvancedSettings,
Name: "feature_enabled_markdown_preview",
Value: *data.UseMarkdownPreview,
})
}
if data.UseFormatting != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryAdvancedSettings,
Name: "formatting",
Value: *data.UseFormatting,
})
}
if data.ShowUnreadSection != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategorySidebarSettings,
Name: "show_unread_section",
Value: *data.ShowUnreadSection,
})
}
if data.EmailInterval != nil || savedUser.NotifyProps[model.EmailNotifyProp] == "false" {
var intervalSeconds string
if value := savedUser.NotifyProps[model.EmailNotifyProp]; value == "false" {
intervalSeconds = "0"
} else {
switch *data.EmailInterval {
case model.PreferenceEmailIntervalImmediately:
intervalSeconds = model.PreferenceEmailIntervalNoBatchingSeconds
case model.PreferenceEmailIntervalFifteen:
intervalSeconds = model.PreferenceEmailIntervalFifteenAsSeconds
case model.PreferenceEmailIntervalHour:
intervalSeconds = model.PreferenceEmailIntervalHourAsSeconds
}
}
if intervalSeconds != "" {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PreferenceCategoryNotifications,
Name: model.PreferenceNameEmailInterval,
Value: intervalSeconds,
})
}
}
if len(preferences) > 0 {
if err := a.Srv().Store().Preference().Save(preferences); err != nil {
return model.NewAppError("BulkImport", "app.import.import_user.save_preferences.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return a.importUserTeams(c, savedUser, data.Teams)
}
func (a *App) importUserTeams(c request.CTX, user *model.User, data *[]imports.UserTeamImportData) *model.AppError {
if data == nil {
return nil
}
teamNames := []string{}
for _, tdata := range *data {
teamNames = append(teamNames, *tdata.Name)
}
allTeams, err := a.getTeamsByNames(teamNames)
if err != nil {
return err
}
var (
teamThemePreferencesByID = map[string]model.Preferences{}
channels = map[string][]imports.UserChannelImportData{}
teamsByID = map[string]*model.Team{}
teamMemberByTeamID = map[string]*model.TeamMember{}
newTeamMembers = []*model.TeamMember{}
oldTeamMembers = []*model.TeamMember{}
rolesByTeamId = map[string]string{}
isGuestByTeamId = map[string]bool{}
isUserByTeamId = map[string]bool{}
isAdminByTeamId = map[string]bool{}
)
existingMemberships, nErr := a.Srv().Store().Team().GetTeamsForUser(context.Background(), user.Id, "", true)
if nErr != nil {
return model.NewAppError("importUserTeams", "app.team.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
existingMembershipsByTeamId := map[string]*model.TeamMember{}
for _, teamMembership := range existingMemberships {
existingMembershipsByTeamId[teamMembership.TeamId] = teamMembership
}
for _, tdata := range *data {
team := allTeams[strings.ToLower(*tdata.Name)]
// Team-specific theme Preferences.
if tdata.Theme != nil {
teamThemePreferencesByID[team.Id] = append(teamThemePreferencesByID[team.Id], model.Preference{
UserId: user.Id,
Category: model.PreferenceCategoryTheme,
Name: team.Id,
Value: *tdata.Theme,
})
}
isGuestByTeamId[team.Id] = false
isUserByTeamId[team.Id] = true
isAdminByTeamId[team.Id] = false
if tdata.Roles == nil {
isUserByTeamId[team.Id] = true
} else {
rawRoles := *tdata.Roles
explicitRoles := []string{}
for _, role := range strings.Fields(rawRoles) {
if role == model.TeamGuestRoleId {
isGuestByTeamId[team.Id] = true
isUserByTeamId[team.Id] = false
} else if role == model.TeamUserRoleId {
isUserByTeamId[team.Id] = true
} else if role == model.TeamAdminRoleId {
isAdminByTeamId[team.Id] = true
} else {
explicitRoles = append(explicitRoles, role)
}
}
rolesByTeamId[team.Id] = strings.Join(explicitRoles, " ")
}
member := &model.TeamMember{
TeamId: team.Id,
UserId: user.Id,
SchemeGuest: user.IsGuest(),
SchemeUser: !user.IsGuest(),
SchemeAdmin: team.Email == user.Email && !user.IsGuest(),
CreateAt: model.GetMillis(),
}
if !user.IsGuest() {
var userShouldBeAdmin bool
userShouldBeAdmin, err = a.UserIsInAdminRoleGroup(user.Id, team.Id, model.GroupSyncableTypeTeam)
if err != nil {
return err
}
member.SchemeAdmin = userShouldBeAdmin
}
if tdata.Channels != nil {
channels[team.Id] = append(channels[team.Id], *tdata.Channels...)
}
if !user.IsGuest() {
channels[team.Id] = append(channels[team.Id], imports.UserChannelImportData{Name: model.NewString(model.DefaultChannelName)})
}
teamsByID[team.Id] = team
teamMemberByTeamID[team.Id] = member
if _, ok := existingMembershipsByTeamId[team.Id]; !ok {
newTeamMembers = append(newTeamMembers, member)
} else {
oldTeamMembers = append(oldTeamMembers, member)
}
}
oldMembers, nErr := a.Srv().Store().Team().UpdateMultipleMembers(oldTeamMembers)
if nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return appErr
default:
return model.NewAppError("importUserTeams", "app.team.save_member.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
newMembers := []*model.TeamMember{}
if len(newTeamMembers) > 0 {
var nErr error
newMembers, nErr = a.Srv().Store().Team().SaveMultipleMembers(newTeamMembers, *a.Config().TeamSettings.MaxUsersPerTeam)
if nErr != nil {
var appErr *model.AppError
var conflictErr *store.ErrConflict
var limitExceededErr *store.ErrLimitExceeded
switch {
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
return appErr
case errors.As(nErr, &conflictErr):
return model.NewAppError("BulkImport", "app.import.import_user_teams.save_members.conflict.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &limitExceededErr):
return model.NewAppError("BulkImport", "app.import.import_user_teams.save_members.max_accounts.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
default: // last fallback in case it doesn't map to an existing app error.
return model.NewAppError("BulkImport", "app.import.import_user_teams.save_members.error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
}
for _, member := range append(newMembers, oldMembers...) {
if member.ExplicitRoles != rolesByTeamId[member.TeamId] {
if _, err = a.UpdateTeamMemberRoles(member.TeamId, user.Id, rolesByTeamId[member.TeamId]); err != nil {
return err
}
}
a.UpdateTeamMemberSchemeRoles(member.TeamId, user.Id, isGuestByTeamId[member.TeamId], isUserByTeamId[member.TeamId], isAdminByTeamId[member.TeamId])
}
for _, team := range allTeams {
if len(teamThemePreferencesByID[team.Id]) > 0 {
pref := teamThemePreferencesByID[team.Id]
if err := a.Srv().Store().Preference().Save(pref); err != nil {
return model.NewAppError("BulkImport", "app.import.import_user_teams.save_preferences.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
channelsToImport := channels[team.Id]
if err := a.importUserChannels(c, user, team, &channelsToImport); err != nil {
return err
}
}
return nil
}
func (a *App) importUserChannels(c request.CTX, user *model.User, team *model.Team, data *[]imports.UserChannelImportData) *model.AppError {
if data == nil {
return nil
}
channelNames := []string{}
for _, tdata := range *data {
channelNames = append(channelNames, *tdata.Name)
}
allChannels, err := a.getChannelsByNames(channelNames, team.Id)
if err != nil {
return err
}
var (
channelsByID = map[string]*model.Channel{}
channelMemberByChannelID = map[string]*model.ChannelMember{}
newChannelMembers = []*model.ChannelMember{}
oldChannelMembers = []*model.ChannelMember{}
rolesByChannelId = map[string]string{}
channelPreferencesByID = map[string]model.Preferences{}
isGuestByChannelId = map[string]bool{}
isUserByChannelId = map[string]bool{}
isAdminByChannelId = map[string]bool{}
)
existingMemberships, nErr := a.Srv().Store().Channel().GetMembersForUser(team.Id, user.Id)
if nErr != nil {
return model.NewAppError("importUserChannels", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
existingMembershipsByChannelId := map[string]model.ChannelMember{}
for _, channelMembership := range existingMemberships {
existingMembershipsByChannelId[channelMembership.ChannelId] = channelMembership
}
for _, cdata := range *data {
channel, ok := allChannels[strings.ToLower(*cdata.Name)]
if !ok {
return model.NewAppError("BulkImport", "app.import.import_user_channels.channel_not_found.error", nil, "", http.StatusInternalServerError)
}
if _, ok = channelsByID[channel.Id]; ok && *cdata.Name == model.DefaultChannelName {
// town-square membership was in the import and added by the importer (skip the added by the importer)
continue
}
isGuestByChannelId[channel.Id] = false
isUserByChannelId[channel.Id] = true
isAdminByChannelId[channel.Id] = false
if cdata.Roles != nil {
rawRoles := *cdata.Roles
explicitRoles := []string{}
for _, role := range strings.Fields(rawRoles) {
if role == model.ChannelGuestRoleId {
isGuestByChannelId[channel.Id] = true
isUserByChannelId[channel.Id] = false
} else if role == model.ChannelUserRoleId {
isUserByChannelId[channel.Id] = true
} else if role == model.ChannelAdminRoleId {
isAdminByChannelId[channel.Id] = true
} else {
explicitRoles = append(explicitRoles, role)
}
}
rolesByChannelId[channel.Id] = strings.Join(explicitRoles, " ")
}
if cdata.Favorite != nil && *cdata.Favorite {
channelPreferencesByID[channel.Id] = append(channelPreferencesByID[channel.Id], model.Preference{
UserId: user.Id,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel.Id,
Value: "true",
})
}
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: user.IsGuest(),
SchemeUser: !user.IsGuest(),
SchemeAdmin: false,
}
if !user.IsGuest() {
var userShouldBeAdmin bool
userShouldBeAdmin, err = a.UserIsInAdminRoleGroup(user.Id, team.Id, model.GroupSyncableTypeTeam)
if err != nil {
return err
}
member.SchemeAdmin = userShouldBeAdmin
}
if cdata.MentionCount != nil && cdata.MentionCountRoot != nil {
member.MentionCount = *cdata.MentionCount
member.MentionCountRoot = *cdata.MentionCountRoot
}
if cdata.UrgentMentionCount != nil {
member.UrgentMentionCount = *cdata.UrgentMentionCount
}
if cdata.MsgCount != nil && cdata.MsgCountRoot != nil {
member.MsgCount = *cdata.MsgCount
member.MsgCountRoot = *cdata.MsgCountRoot
}
if cdata.LastViewedAt != nil {
member.LastViewedAt = *cdata.LastViewedAt
}
if cdata.NotifyProps != nil {
if cdata.NotifyProps.Desktop != nil {
member.NotifyProps[model.DesktopNotifyProp] = *cdata.NotifyProps.Desktop
}
if cdata.NotifyProps.Mobile != nil {
member.NotifyProps[model.PushNotifyProp] = *cdata.NotifyProps.Mobile
}
if cdata.NotifyProps.MarkUnread != nil {
member.NotifyProps[model.MarkUnreadNotifyProp] = *cdata.NotifyProps.MarkUnread
}
}
channelsByID[channel.Id] = channel
channelMemberByChannelID[channel.Id] = member
if _, ok := existingMembershipsByChannelId[channel.Id]; !ok {
newChannelMembers = append(newChannelMembers, member)
} else {
oldChannelMembers = append(oldChannelMembers, member)
}
}
oldMembers, nErr := a.Srv().Store().Channel().UpdateMultipleMembers(oldChannelMembers)
if nErr != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return appErr
case errors.As(nErr, &nfErr):
return model.NewAppError("importUserChannels", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return model.NewAppError("importUserChannels", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
newMembers := []*model.ChannelMember{}
if len(newChannelMembers) > 0 {
newMembers, nErr = a.Srv().Store().Channel().SaveMultipleMembers(newChannelMembers)
if nErr != nil {
var cErr *store.ErrConflict
var appErr *model.AppError
switch {
case errors.As(nErr, &cErr):
switch cErr.Resource {
case "ChannelMembers":
return model.NewAppError("importUserChannels", "app.channel.save_member.exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
}
case errors.As(nErr, &appErr):
return appErr
default:
return model.NewAppError("importUserChannels", "app.channel.create_direct_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
}
for _, member := range append(newMembers, oldMembers...) {
if member.ExplicitRoles != rolesByChannelId[member.ChannelId] {
if _, err = a.UpdateChannelMemberRoles(c, member.ChannelId, user.Id, rolesByChannelId[member.ChannelId]); err != nil {
return err
}
}
a.UpdateChannelMemberSchemeRoles(c, member.ChannelId, user.Id, isGuestByChannelId[member.ChannelId], isUserByChannelId[member.ChannelId], isAdminByChannelId[member.ChannelId])
}
for _, channel := range allChannels {
if len(channelPreferencesByID[channel.Id]) > 0 {
pref := channelPreferencesByID[channel.Id]
if err := a.Srv().Store().Preference().Save(pref); err != nil {
return model.NewAppError("BulkImport", "app.import.import_user_channels.save_preferences.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
return nil
}
func (a *App) importReaction(data *imports.ReactionImportData, post *model.Post) *model.AppError {
if err := imports.ValidateReactionImportData(data, post.CreateAt); err != nil {
return err
}
var user *model.User
var nErr error
if user, nErr = a.Srv().Store().User().GetByUsername(*data.User); nErr != nil {
return model.NewAppError("BulkImport", "app.import.import_post.user_not_found.error", map[string]any{"Username": data.User}, "", http.StatusBadRequest).Wrap(nErr)
}
reaction := &model.Reaction{
UserId: user.Id,
PostId: post.Id,
EmojiName: *data.EmojiName,
CreateAt: *data.CreateAt,
}
if _, nErr = a.Srv().Store().Reaction().Save(reaction); nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return appErr
default:
return model.NewAppError("importReaction", "app.reaction.save.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return nil
}
func (a *App) importReplies(c request.CTX, data []imports.ReplyImportData, post *model.Post, teamID string) *model.AppError {
var err *model.AppError
usernames := []string{}
for _, replyData := range data {
replyData := replyData
if err = imports.ValidateReplyImportData(&replyData, post.CreateAt, a.MaxPostSize()); err != nil {
return err
}
usernames = append(usernames, *replyData.User)
}
users, err := a.getUsersByUsernames(usernames)
if err != nil {
return err
}
var (
postsWithData = []postAndData{}
postsForCreateList = []*model.Post{}
postsForOverwriteList = []*model.Post{}
)
for _, replyData := range data {
replyData := replyData
user := users[strings.ToLower(*replyData.User)]
// Check if this post already exists.
replies, nErr := a.Srv().Store().Post().GetPostsCreatedAt(post.ChannelId, *replyData.CreateAt)
if nErr != nil {
return model.NewAppError("importReplies", "app.post.get_posts_created_at.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
var reply *model.Post
for _, r := range replies {
if r.Message == *replyData.Message && r.RootId == post.Id {
reply = r
break
}
}
if reply == nil {
reply = &model.Post{}
}
reply.UserId = user.Id
reply.ChannelId = post.ChannelId
reply.RootId = post.Id
reply.Message = *replyData.Message
reply.CreateAt = *replyData.CreateAt
if reply.CreateAt < post.CreateAt {
c.Logger().Warn("Reply CreateAt is before parent post CreateAt, setting it to parent post CreateAt", mlog.Int64("reply_create_at", reply.CreateAt), mlog.Int64("parent_create_at", post.CreateAt))
reply.CreateAt = post.CreateAt
}
if replyData.Type != nil {
reply.Type = *replyData.Type
}
if replyData.EditAt != nil {
reply.EditAt = *replyData.EditAt
}
fileIDs := a.uploadAttachments(c, replyData.Attachments, reply, teamID)
for _, fileID := range reply.FileIds {
if _, ok := fileIDs[fileID]; !ok {
a.Srv().Store().FileInfo().PermanentDelete(fileID)
}
}
reply.FileIds = make([]string, 0)
for fileID := range fileIDs {
reply.FileIds = append(reply.FileIds, fileID)
}
if reply.Id == "" {
postsForCreateList = append(postsForCreateList, reply)
} else {
postsForOverwriteList = append(postsForOverwriteList, reply)
}
postsWithData = append(postsWithData, postAndData{post: reply, replyData: &replyData})
}
if len(postsForCreateList) > 0 {
if _, _, err := a.Srv().Store().Post().SaveMultiple(postsForCreateList); err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return appErr
case errors.As(err, &invErr):
return model.NewAppError("importReplies", "app.post.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return model.NewAppError("importReplies", "app.post.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
if _, _, nErr := a.Srv().Store().Post().OverwriteMultiple(postsForOverwriteList); nErr != nil {
return model.NewAppError("importReplies", "app.post.overwrite.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
for _, postWithData := range postsWithData {
a.updateFileInfoWithPostId(postWithData.post)
}
return nil
}
func (a *App) importAttachment(c request.CTX, data *imports.AttachmentImportData, post *model.Post, teamID string) (*model.FileInfo, *model.AppError) {
var (
name string
file io.Reader
)
if data.Data != nil {
zipFile, err := data.Data.Open()
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.attachment.bad_file.error", map[string]any{"FilePath": *data.Path}, "", http.StatusBadRequest).Wrap(err)
}
defer zipFile.Close()
name = data.Data.Name
file = zipFile.(io.Reader)
c.Logger().Info("Preparing file upload from ZIP", mlog.String("file_name", name), mlog.Uint64("file_size", data.Data.UncompressedSize64))
} else {
realFile, err := os.Open(*data.Path)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.attachment.bad_file.error", map[string]any{"FilePath": *data.Path}, "", http.StatusBadRequest).Wrap(err)
}
defer realFile.Close()
name = realFile.Name()
file = realFile
fields := []logr.Field{mlog.String("file_name", name)}
if info, err := realFile.Stat(); err != nil {
fields = append(fields, mlog.Int64("file_size", info.Size()))
}
c.Logger().Info("Preparing file upload from file system", fields...)
}
timestamp := utils.TimeFromMillis(post.CreateAt)
fileData, err := io.ReadAll(file)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.attachment.read_file_data.error", map[string]any{"FilePath": *data.Path}, "", http.StatusBadRequest)
}
// Go over existing files in the post and see if there already exists a file with the same name, size and hash. If so - skip it
if post.Id != "" {
oldFiles, err := a.getFileInfosForPostIgnoreCloudLimit(post.Id, true, false)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.attachment.file_upload.error", map[string]any{"FilePath": *data.Path}, "", http.StatusBadRequest)
}
for _, oldFile := range oldFiles {
if oldFile.Name != path.Base(name) || oldFile.Size != int64(len(fileData)) {
continue
}
// check sha1
newHash := sha1.Sum(fileData)
oldFileData, err := a.getFileIgnoreCloudLimit(oldFile.Id)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.attachment.file_upload.error", map[string]any{"FilePath": *data.Path}, "", http.StatusBadRequest)
}
oldHash := sha1.Sum(oldFileData)
if bytes.Equal(oldHash[:], newHash[:]) {
mlog.Info("Skipping uploading of file because name already exists", mlog.Any("file_name", name))
return oldFile, nil
}
}
}
mlog.Info("Uploading file with name", mlog.String("file_name", name))
fileInfo, appErr := a.DoUploadFile(c, timestamp, teamID, post.ChannelId, post.UserId, name, fileData)
if appErr != nil {
mlog.Error("Failed to upload file", mlog.Err(appErr), mlog.String("file_name", name))
return nil, appErr
}
if fileInfo.IsImage() && !fileInfo.IsSvg() {
a.HandleImages([]string{fileInfo.PreviewPath}, []string{fileInfo.ThumbnailPath}, [][]byte{fileData})
}
return fileInfo, nil
}
type postAndData struct {
post *model.Post
postData *imports.PostImportData
directPostData *imports.DirectPostImportData
replyData *imports.ReplyImportData
team *model.Team
lineNumber int
}
func (a *App) getUsersByUsernames(usernames []string) (map[string]*model.User, *model.AppError) {
uniqueUsernames := utils.RemoveDuplicatesFromStringArray(usernames)
allUsers, err := a.Srv().Store().User().GetProfilesByUsernames(uniqueUsernames, nil)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.get_users_by_username.some_users_not_found.error", nil, "", http.StatusBadRequest).Wrap(err)
}
if len(allUsers) != len(uniqueUsernames) {
return nil, model.NewAppError("BulkImport", "app.import.get_users_by_username.some_users_not_found.error", nil, "", http.StatusBadRequest)
}
users := make(map[string]*model.User)
for _, user := range allUsers {
users[strings.ToLower(user.Username)] = user
}
return users, nil
}
func (a *App) getTeamsByNames(names []string) (map[string]*model.Team, *model.AppError) {
allTeams, err := a.Srv().Store().Team().GetByNames(names)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.get_teams_by_names.some_teams_not_found.error", nil, "", http.StatusBadRequest).Wrap(err)
}
teams := make(map[string]*model.Team)
for _, team := range allTeams {
teams[strings.ToLower(team.Name)] = team
}
return teams, nil
}
func (a *App) getChannelsByNames(names []string, teamID string) (map[string]*model.Channel, *model.AppError) {
allChannels, err := a.Srv().Store().Channel().GetByNames(teamID, names, true)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.get_teams_by_names.some_teams_not_found.error", nil, "", http.StatusBadRequest).Wrap(err)
}
channels := make(map[string]*model.Channel)
for _, channel := range allChannels {
channels[strings.ToLower(channel.Name)] = channel
}
return channels, nil
}
// getChannelsForPosts returns map[teamName]map[channelName]*model.Channel
func (a *App) getChannelsForPosts(teams map[string]*model.Team, data []*imports.PostImportData) (map[string]map[string]*model.Channel, *model.AppError) {
teamChannels := make(map[string]map[string]*model.Channel)
for _, postData := range data {
teamName := strings.ToLower(*postData.Team)
if _, ok := teamChannels[teamName]; !ok {
teamChannels[teamName] = make(map[string]*model.Channel)
}
channelName := strings.ToLower(*postData.Channel)
if channel, ok := teamChannels[teamName][channelName]; !ok || channel == nil {
var err error
channel, err = a.Srv().Store().Channel().GetByName(teams[teamName].Id, *postData.Channel, true)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.import_post.channel_not_found.error", map[string]any{"ChannelName": *postData.Channel}, "", http.StatusBadRequest).Wrap(err)
}
teamChannels[teamName][channelName] = channel
}
}
return teamChannels, nil
}
// getPostStrID returns a string ID composed of several post fields to
// uniquely identify a post before it's imported, so it has no ID yet
func getPostStrID(post *model.Post) string {
return fmt.Sprintf("%d%s%s", post.CreateAt, post.ChannelId, post.Message)
}
// importMultiplePostLines will return an error and the line that
// caused it whenever possible
func (a *App) importMultiplePostLines(c request.CTX, lines []imports.LineImportWorkerData, dryRun bool) (int, *model.AppError) {
if len(lines) == 0 {
return 0, nil
}
c.Logger().Info("Validating post lines", mlog.Int("count", len(lines)), mlog.Int("first_line", lines[0].LineNumber))
for _, line := range lines {
if err := imports.ValidatePostImportData(line.Post, a.MaxPostSize()); err != nil {
return line.LineNumber, err
}
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return 0, nil
}
c.Logger().Info("Importing post lines", mlog.Int("count", len(lines)), mlog.Int("first_line", lines[0].LineNumber))
usernames := []string{}
teamNames := make([]string, len(lines))
postsData := make([]*imports.PostImportData, len(lines))
for i, line := range lines {
usernames = append(usernames, *line.Post.User)
if line.Post.FlaggedBy != nil {
usernames = append(usernames, *line.Post.FlaggedBy...)
}
teamNames[i] = *line.Post.Team
postsData[i] = line.Post
}
users, err := a.getUsersByUsernames(usernames)
if err != nil {
return 0, err
}
teams, err := a.getTeamsByNames(teamNames)
if err != nil {
return 0, err
}
channels, err := a.getChannelsForPosts(teams, postsData)
if err != nil {
return 0, err
}
var (
postsWithData = []postAndData{}
postsForCreateList = []*model.Post{}
postsForCreateMap = map[string]int{}
postsForOverwriteList = []*model.Post{}
postsForOverwriteMap = map[string]int{}
)
for _, line := range lines {
team := teams[strings.ToLower(*line.Post.Team)]
channel := channels[*line.Post.Team][*line.Post.Channel]
user := users[strings.ToLower(*line.Post.User)]
// Check if this post already exists.
posts, nErr := a.Srv().Store().Post().GetPostsCreatedAt(channel.Id, *line.Post.CreateAt)
if nErr != nil {
return line.LineNumber, model.NewAppError("importMultiplePostLines", "app.post.get_posts_created_at.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
var post *model.Post
for _, p := range posts {
if p.Message == *line.Post.Message {
post = p
break
}
}
if post == nil {
post = &model.Post{}
}
post.ChannelId = channel.Id
post.Message = *line.Post.Message
post.UserId = user.Id
post.CreateAt = *line.Post.CreateAt
post.Hashtags, _ = model.ParseHashtags(post.Message)
if line.Post.Type != nil {
post.Type = *line.Post.Type
}
if line.Post.EditAt != nil {
post.EditAt = *line.Post.EditAt
}
if line.Post.Props != nil {
post.Props = *line.Post.Props
}
if line.Post.IsPinned != nil {
post.IsPinned = *line.Post.IsPinned
}
fileIDs := a.uploadAttachments(c, line.Post.Attachments, post, team.Id)
for _, fileID := range post.FileIds {
if _, ok := fileIDs[fileID]; !ok {
a.Srv().Store().FileInfo().PermanentDelete(fileID)
}
}
post.FileIds = make([]string, 0)
for fileID := range fileIDs {
post.FileIds = append(post.FileIds, fileID)
}
if post.Id == "" {
postsForCreateList = append(postsForCreateList, post)
postsForCreateMap[getPostStrID(post)] = line.LineNumber
} else {
postsForOverwriteList = append(postsForOverwriteList, post)
postsForOverwriteMap[getPostStrID(post)] = line.LineNumber
}
postsWithData = append(postsWithData, postAndData{post: post, postData: line.Post, team: team, lineNumber: line.LineNumber})
}
if len(postsForCreateList) > 0 {
if _, idx, nErr := a.Srv().Store().Post().SaveMultiple(postsForCreateList); nErr != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
var retErr *model.AppError
switch {
case errors.As(nErr, &appErr):
retErr = appErr
case errors.As(nErr, &invErr):
retErr = model.NewAppError("importMultiplePostLines", "app.post.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
default:
retErr = model.NewAppError("importMultiplePostLines", "app.post.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
if idx != -1 && idx < len(postsForCreateList) {
post := postsForCreateList[idx]
if lineNumber, ok := postsForCreateMap[getPostStrID(post)]; ok {
return lineNumber, retErr
}
}
return 0, retErr
}
}
if _, idx, err := a.Srv().Store().Post().OverwriteMultiple(postsForOverwriteList); err != nil {
if idx != -1 && idx < len(postsForOverwriteList) {
post := postsForOverwriteList[idx]
if lineNumber, ok := postsForOverwriteMap[getPostStrID(post)]; ok {
return lineNumber, model.NewAppError("importMultiplePostLines", "app.post.overwrite.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return 0, model.NewAppError("importMultiplePostLines", "app.post.overwrite.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, postWithData := range postsWithData {
postWithData := postWithData
if postWithData.postData.FlaggedBy != nil {
var preferences model.Preferences
for _, username := range *postWithData.postData.FlaggedBy {
user := users[strings.ToLower(username)]
preferences = append(preferences, model.Preference{
UserId: user.Id,
Category: model.PreferenceCategoryFlaggedPost,
Name: postWithData.post.Id,
Value: "true",
})
}
if len(preferences) > 0 {
if err := a.Srv().Store().Preference().Save(preferences); err != nil {
return postWithData.lineNumber, model.NewAppError("BulkImport", "app.import.import_post.save_preferences.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
if postWithData.postData.Reactions != nil {
for _, reaction := range *postWithData.postData.Reactions {
reaction := reaction
if err := a.importReaction(&reaction, postWithData.post); err != nil {
return postWithData.lineNumber, err
}
}
}
if postWithData.postData.Replies != nil && len(*postWithData.postData.Replies) > 0 {
err := a.importReplies(c, *postWithData.postData.Replies, postWithData.post, postWithData.team.Id)
if err != nil {
return postWithData.lineNumber, err
}
}
a.updateFileInfoWithPostId(postWithData.post)
}
return 0, nil
}
// uploadAttachments imports new attachments and returns current attachments of the post as a map
func (a *App) uploadAttachments(c request.CTX, attachments *[]imports.AttachmentImportData, post *model.Post, teamID string) map[string]bool {
if attachments == nil {
return nil
}
fileIDs := make(map[string]bool)
for _, attachment := range *attachments {
attachment := attachment
fileInfo, err := a.importAttachment(c, &attachment, post, teamID)
if err != nil {
if attachment.Path != nil {
mlog.Warn(
"failed to import attachment",
mlog.String("path", *attachment.Path),
mlog.String("error", err.Error()))
} else {
mlog.Warn("failed to import attachment; path was nil",
mlog.String("error", err.Error()))
}
continue
}
fileIDs[fileInfo.Id] = true
}
return fileIDs
}
func (a *App) updateFileInfoWithPostId(post *model.Post) {
for _, fileID := range post.FileIds {
if err := a.Srv().Store().FileInfo().AttachToPost(fileID, post.Id, post.ChannelId, post.UserId); err != nil {
mlog.Error("Error attaching files to post.", mlog.String("post_id", post.Id), mlog.Any("post_file_ids", post.FileIds), mlog.Err(err))
}
}
}
func (a *App) importDirectChannel(c request.CTX, data *imports.DirectChannelImportData, dryRun bool) *model.AppError {
var err *model.AppError
if err = imports.ValidateDirectChannelImportData(data); err != nil {
return err
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
var userIDs []string
userMap, err := a.getUsersByUsernames(*data.Members)
if err != nil {
return err
}
for _, user := range *data.Members {
userIDs = append(userIDs, userMap[strings.ToLower(user)].Id)
}
var channel *model.Channel
if len(userIDs) == 2 {
ch, err := a.createDirectChannel(c, userIDs[0], userIDs[1])
if err != nil && err.Id != store.ChannelExistsError {
return model.NewAppError("BulkImport", "app.import.import_direct_channel.create_direct_channel.error", nil, "", http.StatusBadRequest).Wrap(err)
}
channel = ch
} else {
ch, err := a.createGroupChannel(c, userIDs)
if err != nil && err.Id != store.ChannelExistsError {
return model.NewAppError("BulkImport", "app.import.import_direct_channel.create_group_channel.error", nil, "", http.StatusBadRequest).Wrap(err)
}
channel = ch
}
var preferences model.Preferences
for _, userID := range userIDs {
preferences = append(preferences, model.Preference{
UserId: userID,
Category: model.PreferenceCategoryDirectChannelShow,
Name: channel.Id,
Value: "true",
})
}
if data.FavoritedBy != nil {
for _, favoriter := range *data.FavoritedBy {
preferences = append(preferences, model.Preference{
UserId: userMap[strings.ToLower(favoriter)].Id,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel.Id,
Value: "true",
})
}
}
if err := a.Srv().Store().Preference().Save(preferences); err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
appErr.StatusCode = http.StatusBadRequest
return appErr
default:
return model.NewAppError("importDirectChannel", "app.preference.save.updating.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
}
if data.Header != nil {
channel.Header = *data.Header
if _, appErr := a.Srv().Store().Channel().Update(channel); appErr != nil {
return model.NewAppError("BulkImport", "app.import.import_direct_channel.update_header_failed.error", nil, "", http.StatusBadRequest).Wrap(appErr)
}
}
return nil
}
// importMultipleDirectPostLines will return an error and the line
// that caused it whenever possible
func (a *App) importMultipleDirectPostLines(c request.CTX, lines []imports.LineImportWorkerData, dryRun bool) (int, *model.AppError) {
if len(lines) == 0 {
return 0, nil
}
for _, line := range lines {
if err := imports.ValidateDirectPostImportData(line.DirectPost, a.MaxPostSize()); err != nil {
return line.LineNumber, err
}
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return 0, nil
}
usernames := []string{}
for _, line := range lines {
usernames = append(usernames, *line.DirectPost.User)
if line.DirectPost.FlaggedBy != nil {
usernames = append(usernames, *line.DirectPost.FlaggedBy...)
}
usernames = append(usernames, *line.DirectPost.ChannelMembers...)
}
users, err := a.getUsersByUsernames(usernames)
if err != nil {
return 0, err
}
var (
postsWithData = []postAndData{}
postsForCreateList = []*model.Post{}
postsForCreateMap = map[string]int{}
postsForOverwriteList = []*model.Post{}
postsForOverwriteMap = map[string]int{}
)
for _, line := range lines {
var userIDs []string
var err *model.AppError
for _, username := range *line.DirectPost.ChannelMembers {
user := users[strings.ToLower(username)]
userIDs = append(userIDs, user.Id)
}
var channel *model.Channel
var ch *model.Channel
if len(userIDs) == 2 {
ch, err = a.GetOrCreateDirectChannel(c, userIDs[0], userIDs[1])
if err != nil && err.Id != store.ChannelExistsError {
return line.LineNumber, model.NewAppError("BulkImport", "app.import.import_direct_post.create_direct_channel.error", nil, "", http.StatusBadRequest).Wrap(err)
}
channel = ch
} else {
ch, err = a.createGroupChannel(c, userIDs)
if err != nil && err.Id != store.ChannelExistsError {
return line.LineNumber, model.NewAppError("BulkImport", "app.import.import_direct_post.create_group_channel.error", nil, "", http.StatusBadRequest).Wrap(err)
}
channel = ch
}
user := users[strings.ToLower(*line.DirectPost.User)]
// Check if this post already exists.
posts, nErr := a.Srv().Store().Post().GetPostsCreatedAt(channel.Id, *line.DirectPost.CreateAt)
if nErr != nil {
return line.LineNumber, model.NewAppError("BulkImport", "app.post.get_posts_created_at.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
var post *model.Post
for _, p := range posts {
if p.Message == *line.DirectPost.Message {
post = p
break
}
}
if post == nil {
post = &model.Post{}
}
post.ChannelId = channel.Id
post.Message = *line.DirectPost.Message
post.UserId = user.Id
post.CreateAt = *line.DirectPost.CreateAt
post.Hashtags, _ = model.ParseHashtags(post.Message)
if line.DirectPost.Type != nil {
post.Type = *line.DirectPost.Type
}
if line.DirectPost.EditAt != nil {
post.EditAt = *line.DirectPost.EditAt
}
if line.DirectPost.Props != nil {
post.Props = *line.DirectPost.Props
}
if line.DirectPost.IsPinned != nil {
post.IsPinned = *line.DirectPost.IsPinned
}
fileIDs := a.uploadAttachments(c, line.DirectPost.Attachments, post, "noteam")
for _, fileID := range post.FileIds {
if _, ok := fileIDs[fileID]; !ok {
a.Srv().Store().FileInfo().PermanentDelete(fileID)
}
}
post.FileIds = make([]string, 0)
for fileID := range fileIDs {
post.FileIds = append(post.FileIds, fileID)
}
if post.Id == "" {
postsForCreateList = append(postsForCreateList, post)
postsForCreateMap[getPostStrID(post)] = line.LineNumber
} else {
postsForOverwriteList = append(postsForOverwriteList, post)
postsForOverwriteMap[getPostStrID(post)] = line.LineNumber
}
postsWithData = append(postsWithData, postAndData{post: post, directPostData: line.DirectPost, lineNumber: line.LineNumber})
}
if len(postsForCreateList) > 0 {
if _, idx, err := a.Srv().Store().Post().SaveMultiple(postsForCreateList); err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
var retErr *model.AppError
switch {
case errors.As(err, &appErr):
retErr = appErr
case errors.As(err, &invErr):
retErr = model.NewAppError("importMultiplePostLines", "app.post.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
retErr = model.NewAppError("importMultiplePostLines", "app.post.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if idx != -1 && idx < len(postsForCreateList) {
post := postsForCreateList[idx]
if lineNumber, ok := postsForCreateMap[getPostStrID(post)]; ok {
return lineNumber, retErr
}
}
return 0, retErr
}
}
if _, idx, err := a.Srv().Store().Post().OverwriteMultiple(postsForOverwriteList); err != nil {
if idx != -1 && idx < len(postsForOverwriteList) {
post := postsForOverwriteList[idx]
if lineNumber, ok := postsForOverwriteMap[getPostStrID(post)]; ok {
return lineNumber, model.NewAppError("importMultiplePostLines", "app.post.overwrite.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return 0, model.NewAppError("importMultiplePostLines", "app.post.overwrite.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, postWithData := range postsWithData {
if postWithData.directPostData.FlaggedBy != nil {
var preferences model.Preferences
for _, username := range *postWithData.directPostData.FlaggedBy {
user := users[strings.ToLower(username)]
preferences = append(preferences, model.Preference{
UserId: user.Id,
Category: model.PreferenceCategoryFlaggedPost,
Name: postWithData.post.Id,
Value: "true",
})
}
if len(preferences) > 0 {
if err := a.Srv().Store().Preference().Save(preferences); err != nil {
return postWithData.lineNumber, model.NewAppError("BulkImport", "app.import.import_post.save_preferences.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
if postWithData.directPostData.Reactions != nil {
for _, reaction := range *postWithData.directPostData.Reactions {
reaction := reaction
if err := a.importReaction(&reaction, postWithData.post); err != nil {
return postWithData.lineNumber, err
}
}
}
if postWithData.directPostData.Replies != nil {
if err := a.importReplies(c, *postWithData.directPostData.Replies, postWithData.post, "noteam"); err != nil {
return postWithData.lineNumber, err
}
}
a.updateFileInfoWithPostId(postWithData.post)
}
return 0, nil
}
func (a *App) importEmoji(c request.CTX, data *imports.EmojiImportData, dryRun bool) *model.AppError {
var fields []logr.Field
if data != nil && data.Name != nil {
fields = append(fields, mlog.String("emoji_name", *data.Name))
}
c.Logger().Info("Validating emoji", fields...)
aerr := imports.ValidateEmojiImportData(data)
if aerr != nil {
if aerr.Id == "model.emoji.system_emoji_name.app_error" {
mlog.Warn("Skipping emoji import due to name conflict with system emoji", mlog.String("emoji_name", *data.Name))
return nil
}
return aerr
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
c.Logger().Info("Importing emoji", fields...)
var emoji *model.Emoji
emoji, err := a.Srv().Store().Emoji().GetByName(context.Background(), *data.Name, true)
if err != nil {
var nfErr *store.ErrNotFound
if !errors.As(err, &nfErr) {
return model.NewAppError("importEmoji", "app.emoji.get_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
alreadyExists := emoji != nil
if !alreadyExists {
emoji = &model.Emoji{
Name: *data.Name,
}
emoji.PreSave()
}
var file io.ReadCloser
if data.Data != nil {
file, err = data.Data.Open()
} else {
file, err = os.Open(*data.Image)
}
if err != nil {
return model.NewAppError("BulkImport", "app.import.emoji.bad_file.error", map[string]any{"EmojiName": *data.Name}, "", http.StatusBadRequest)
}
defer file.Close()
reader := utils.NewLimitedReaderWithError(file, MaxEmojiFileSize)
if _, err := a.WriteFile(reader, getEmojiImagePath(emoji.Id)); err != nil {
return err
}
if !alreadyExists {
if _, err := a.Srv().Store().Emoji().Save(emoji); err != nil {
return model.NewAppError("importEmoji", "api.emoji.create.internal_error", nil, "", http.StatusBadRequest).Wrap(err)
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"crypto/rand"
"math/big"
)
const (
passwordSpecialChars = "!$%^&*(),."
passwordNumbers = "0123456789"
passwordUpperCaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
passwordLowerCaseLetters = "abcdefghijklmnopqrstuvwxyz"
passwordAllChars = passwordSpecialChars + passwordNumbers + passwordUpperCaseLetters + passwordLowerCaseLetters
)
func randInt(max int) (int, error) {
val, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
if err != nil {
return 0, err
}
return int(val.Int64()), nil
}
func generatePassword(minimumLength int) (string, error) {
upperIdx, err := randInt(len(passwordUpperCaseLetters))
if err != nil {
return "", err
}
numberIdx, err := randInt(len(passwordNumbers))
if err != nil {
return "", err
}
lowerIdx, err := randInt(len(passwordLowerCaseLetters))
if err != nil {
return "", err
}
specialIdx, err := randInt(len(passwordSpecialChars))
if err != nil {
return "", err
}
// Make sure we are guaranteed at least one of each type to meet any possible password complexity requirements.
password := string([]rune(passwordUpperCaseLetters)[upperIdx]) +
string([]rune(passwordNumbers)[numberIdx]) +
string([]rune(passwordLowerCaseLetters)[lowerIdx]) +
string([]rune(passwordSpecialChars)[specialIdx])
for len(password) < minimumLength {
i, err := randInt(len(passwordAllChars))
if err != nil {
return "", err
}
password = password + string([]rune(passwordAllChars)[i])
}
return password, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imports
import (
"encoding/json"
"net/http"
"os"
"strings"
"unicode/utf8"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func ValidateSchemeImportData(data *SchemeImportData) *model.AppError {
if data.Scope == nil {
return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.null_scope.error", nil, "", http.StatusBadRequest)
}
switch *data.Scope {
case model.SchemeScopeTeam:
if data.DefaultTeamAdminRole == nil || data.DefaultTeamUserRole == nil || data.DefaultChannelAdminRole == nil || data.DefaultChannelUserRole == nil {
return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.wrong_roles_for_scope.error", nil, "", http.StatusBadRequest)
}
case model.SchemeScopeChannel:
if data.DefaultTeamAdminRole != nil || data.DefaultTeamUserRole != nil || data.DefaultChannelAdminRole == nil || data.DefaultChannelUserRole == nil {
return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.wrong_roles_for_scope.error", nil, "", http.StatusBadRequest)
}
default:
return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.unknown_scheme.error", nil, "", http.StatusBadRequest)
}
if data.Name == nil || !model.IsValidSchemeName(*data.Name) {
return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.name_invalid.error", nil, "", http.StatusBadRequest)
}
if data.DisplayName == nil || *data.DisplayName == "" || len(*data.DisplayName) > model.SchemeDisplayNameMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.display_name_invalid.error", nil, "", http.StatusBadRequest)
}
if data.Description != nil && len(*data.Description) > model.SchemeDescriptionMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.description_invalid.error", nil, "", http.StatusBadRequest)
}
if data.DefaultTeamAdminRole != nil {
if err := ValidateRoleImportData(data.DefaultTeamAdminRole); err != nil {
return err
}
}
if data.DefaultTeamUserRole != nil {
if err := ValidateRoleImportData(data.DefaultTeamUserRole); err != nil {
return err
}
}
if data.DefaultTeamGuestRole != nil {
if err := ValidateRoleImportData(data.DefaultTeamGuestRole); err != nil {
return err
}
}
if data.DefaultChannelAdminRole != nil {
if err := ValidateRoleImportData(data.DefaultChannelAdminRole); err != nil {
return err
}
}
if data.DefaultChannelUserRole != nil {
if err := ValidateRoleImportData(data.DefaultChannelUserRole); err != nil {
return err
}
}
if data.DefaultChannelGuestRole != nil {
if err := ValidateRoleImportData(data.DefaultChannelGuestRole); err != nil {
return err
}
}
return nil
}
func ValidateRoleImportData(data *RoleImportData) *model.AppError {
if data.Name == nil || !model.IsValidRoleName(*data.Name) {
return model.NewAppError("BulkImport", "app.import.validate_role_import_data.name_invalid.error", nil, "", http.StatusBadRequest)
}
if data.DisplayName == nil || *data.DisplayName == "" || len(*data.DisplayName) > model.RoleDisplayNameMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_role_import_data.display_name_invalid.error", nil, "", http.StatusBadRequest)
}
if data.Description != nil && len(*data.Description) > model.RoleDescriptionMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_role_import_data.description_invalid.error", nil, "", http.StatusBadRequest)
}
if data.Permissions != nil {
for _, permission := range *data.Permissions {
permissionValidated := false
for _, p := range append(model.AllPermissions, model.DeprecatedPermissions...) {
if permission == p.Id {
permissionValidated = true
break
}
}
if !permissionValidated {
return model.NewAppError("BulkImport", "app.import.validate_role_import_data.invalid_permission.error", nil, "permission"+permission, http.StatusBadRequest)
}
}
}
return nil
}
func ValidateTeamImportData(data *TeamImportData) *model.AppError {
if data.Name == nil {
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.name_missing.error", nil, "", http.StatusBadRequest)
} else if len(*data.Name) > model.TeamNameMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.name_length.error", nil, "", http.StatusBadRequest)
} else if model.IsReservedTeamName(*data.Name) {
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.name_reserved.error", nil, "", http.StatusBadRequest)
} else if !model.IsValidTeamName(*data.Name) {
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.name_characters.error", nil, "", http.StatusBadRequest)
}
if data.DisplayName == nil {
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.display_name_missing.error", nil, "", http.StatusBadRequest)
} else if utf8.RuneCountInString(*data.DisplayName) == 0 || utf8.RuneCountInString(*data.DisplayName) > model.TeamDisplayNameMaxRunes {
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.display_name_length.error", nil, "", http.StatusBadRequest)
}
if data.Type == nil {
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.type_missing.error", nil, "", http.StatusBadRequest)
} else if *data.Type != model.TeamOpen && *data.Type != model.TeamInvite {
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.type_invalid.error", nil, "", http.StatusBadRequest)
}
if data.Description != nil && len(*data.Description) > model.TeamDescriptionMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.description_length.error", nil, "", http.StatusBadRequest)
}
if data.Scheme != nil && !model.IsValidSchemeName(*data.Scheme) {
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.scheme_invalid.error", nil, "", http.StatusBadRequest)
}
return nil
}
func ValidateChannelImportData(data *ChannelImportData) *model.AppError {
if data.Team == nil {
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.team_missing.error", nil, "", http.StatusBadRequest)
}
if data.Name == nil {
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.name_missing.error", nil, "", http.StatusBadRequest)
} else if len(*data.Name) > model.ChannelNameMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.name_length.error", nil, "", http.StatusBadRequest)
} else if !model.IsValidChannelIdentifier(*data.Name) {
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.name_characters.error", nil, "", http.StatusBadRequest)
}
if data.DisplayName == nil || utf8.RuneCountInString(*data.DisplayName) == 0 {
data.DisplayName = data.Name // when displayName is missing we use name instead for displaying so we might as well convert it here.
} else if utf8.RuneCountInString(*data.DisplayName) > model.ChannelDisplayNameMaxRunes {
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.display_name_length.error", nil, "", http.StatusBadRequest)
}
if data.Type == nil {
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.type_missing.error", nil, "", http.StatusBadRequest)
} else if *data.Type != model.ChannelTypeOpen && *data.Type != model.ChannelTypePrivate {
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.type_invalid.error", nil, "", http.StatusBadRequest)
}
if data.Header != nil && utf8.RuneCountInString(*data.Header) > model.ChannelHeaderMaxRunes {
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.header_length.error", nil, "", http.StatusBadRequest)
}
if data.Purpose != nil && utf8.RuneCountInString(*data.Purpose) > model.ChannelPurposeMaxRunes {
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.purpose_length.error", nil, "", http.StatusBadRequest)
}
if data.Scheme != nil && !model.IsValidSchemeName(*data.Scheme) {
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.scheme_invalid.error", nil, "", http.StatusBadRequest)
}
return nil
}
func ValidateUserImportData(data *UserImportData) *model.AppError {
if data.ProfileImage != nil {
if _, err := os.Stat(*data.ProfileImage); os.IsNotExist(err) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.profile_image.error", nil, "", http.StatusBadRequest)
}
}
if data.Username == nil {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.username_missing.error", nil, "", http.StatusBadRequest)
} else if !model.IsValidUsername(*data.Username) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.username_invalid.error", nil, "", http.StatusBadRequest)
}
if data.Email == nil {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.email_missing.error", nil, "", http.StatusBadRequest)
} else if *data.Email == "" || len(*data.Email) > model.UserEmailMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.email_length.error", nil, "", http.StatusBadRequest)
}
if data.AuthData != nil && data.Password != nil {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.auth_data_and_password.error", nil, "", http.StatusBadRequest)
}
if data.AuthData != nil && len(*data.AuthData) > model.UserAuthDataMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.auth_data_length.error", nil, "", http.StatusBadRequest)
}
blank := func(str *string) bool {
if str == nil {
return true
}
return *str == ""
}
if (!blank(data.AuthService) && blank(data.AuthData)) || (blank(data.AuthService) && !blank(data.AuthData)) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.auth_data_and_service_dependency.error", nil, "", http.StatusBadRequest)
}
if appErr := validateAuthService(data.AuthService); appErr != nil {
return appErr
}
if data.Password != nil && *data.Password == "" {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.password_length.error", nil, "", http.StatusBadRequest)
}
if data.Password != nil && len(*data.Password) > model.UserPasswordMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.password_length.error", nil, "", http.StatusBadRequest)
}
if data.Nickname != nil && utf8.RuneCountInString(*data.Nickname) > model.UserNicknameMaxRunes {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.nickname_length.error", nil, "", http.StatusBadRequest)
}
if data.FirstName != nil && utf8.RuneCountInString(*data.FirstName) > model.UserFirstNameMaxRunes {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.first_name_length.error", nil, "", http.StatusBadRequest)
}
if data.LastName != nil && utf8.RuneCountInString(*data.LastName) > model.UserLastNameMaxRunes {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.last_name_length.error", nil, "", http.StatusBadRequest)
}
if data.Position != nil && utf8.RuneCountInString(*data.Position) > model.UserPositionMaxRunes {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.position_length.error", nil, "", http.StatusBadRequest)
}
if data.Roles != nil && !model.IsValidUserRoles(*data.Roles) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.roles_invalid.error", nil, "", http.StatusBadRequest)
}
if data.NotifyProps != nil {
if data.NotifyProps.Desktop != nil && !isValidUserNotifyLevel(*data.NotifyProps.Desktop) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.notify_props_desktop_invalid.error", nil, "", http.StatusBadRequest)
}
if data.NotifyProps.DesktopSound != nil && !isValidTrueOrFalseString(*data.NotifyProps.DesktopSound) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.notify_props_desktop_sound_invalid.error", nil, "", http.StatusBadRequest)
}
if data.NotifyProps.Email != nil && !isValidTrueOrFalseString(*data.NotifyProps.Email) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.notify_props_email_invalid.error", nil, "", http.StatusBadRequest)
}
if data.NotifyProps.Mobile != nil && !isValidUserNotifyLevel(*data.NotifyProps.Mobile) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.notify_props_mobile_invalid.error", nil, "", http.StatusBadRequest)
}
if data.NotifyProps.MobilePushStatus != nil && !isValidPushStatusNotifyLevel(*data.NotifyProps.MobilePushStatus) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.notify_props_mobile_push_status_invalid.error", nil, "", http.StatusBadRequest)
}
if data.NotifyProps.ChannelTrigger != nil && !isValidTrueOrFalseString(*data.NotifyProps.ChannelTrigger) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.notify_props_channel_trigger_invalid.error", nil, "", http.StatusBadRequest)
}
if data.NotifyProps.CommentsTrigger != nil && !isValidCommentsNotifyLevel(*data.NotifyProps.CommentsTrigger) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.notify_props_comments_trigger_invalid.error", nil, "", http.StatusBadRequest)
}
}
if data.UseMarkdownPreview != nil && !isValidTrueOrFalseString(*data.UseMarkdownPreview) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.advanced_props_feature_markdown_preview.error", nil, "", http.StatusBadRequest)
}
if data.UseFormatting != nil && !isValidTrueOrFalseString(*data.UseFormatting) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.advanced_props_formatting.error", nil, "", http.StatusBadRequest)
}
if data.ShowUnreadSection != nil && !isValidTrueOrFalseString(*data.ShowUnreadSection) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.advanced_props_show_unread_section.error", nil, "", http.StatusBadRequest)
}
if data.EmailInterval != nil && !isValidEmailBatchingInterval(*data.EmailInterval) {
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.advanced_props_email_interval.error", nil, "", http.StatusBadRequest)
}
if data.Teams != nil {
return ValidateUserTeamsImportData(data.Teams)
}
return nil
}
var validAuthServices = []string{
"",
model.UserAuthServiceEmail,
model.UserAuthServiceGitlab,
model.UserAuthServiceSaml,
model.UserAuthServiceLdap,
model.ServiceGoogle,
model.ServiceOffice365,
}
func validateAuthService(authService *string) *model.AppError {
if authService == nil {
return nil
}
for _, valid := range validAuthServices {
if *authService == valid {
return nil
}
}
return model.NewAppError("BulkImport", "app.import.validate_user_teams_import_data.invalid_auth_service.error", map[string]any{"AuthService": *authService}, "", http.StatusBadRequest)
}
func ValidateUserTeamsImportData(data *[]UserTeamImportData) *model.AppError {
if data == nil {
return nil
}
for _, tdata := range *data {
if tdata.Name == nil {
return model.NewAppError("BulkImport", "app.import.validate_user_teams_import_data.team_name_missing.error", nil, "", http.StatusBadRequest)
}
if tdata.Roles != nil && !model.IsValidUserRoles(*tdata.Roles) {
return model.NewAppError("BulkImport", "app.import.validate_user_teams_import_data.invalid_roles.error", nil, "", http.StatusBadRequest)
}
if tdata.Channels != nil {
if err := ValidateUserChannelsImportData(tdata.Channels); err != nil {
return err
}
}
if tdata.Theme != nil && strings.Trim(*tdata.Theme, " \t\r") != "" {
var unused map[string]string
if err := json.NewDecoder(strings.NewReader(*tdata.Theme)).Decode(&unused); err != nil {
return model.NewAppError("BulkImport", "app.import.validate_user_teams_import_data.invalid_team_theme.error", nil, "", http.StatusBadRequest).Wrap(err)
}
}
}
return nil
}
func ValidateUserChannelsImportData(data *[]UserChannelImportData) *model.AppError {
if data == nil {
return nil
}
for _, cdata := range *data {
if cdata.Name == nil {
return model.NewAppError("BulkImport", "app.import.validate_user_channels_import_data.channel_name_missing.error", nil, "", http.StatusBadRequest)
}
if cdata.Roles != nil && !model.IsValidUserRoles(*cdata.Roles) {
return model.NewAppError("BulkImport", "app.import.validate_user_channels_import_data.invalid_roles.error", nil, "", http.StatusBadRequest)
}
if cdata.NotifyProps != nil {
if cdata.NotifyProps.Desktop != nil && !model.IsChannelNotifyLevelValid(*cdata.NotifyProps.Desktop) {
return model.NewAppError("BulkImport", "app.import.validate_user_channels_import_data.invalid_notify_props_desktop.error", nil, "", http.StatusBadRequest)
}
if cdata.NotifyProps.Mobile != nil && !model.IsChannelNotifyLevelValid(*cdata.NotifyProps.Mobile) {
return model.NewAppError("BulkImport", "app.import.validate_user_channels_import_data.invalid_notify_props_mobile.error", nil, "", http.StatusBadRequest)
}
if cdata.NotifyProps.MarkUnread != nil && !model.IsChannelMarkUnreadLevelValid(*cdata.NotifyProps.MarkUnread) {
return model.NewAppError("BulkImport", "app.import.validate_user_channels_import_data.invalid_notify_props_mark_unread.error", nil, "", http.StatusBadRequest)
}
}
}
return nil
}
func ValidateReactionImportData(data *ReactionImportData, parentCreateAt int64) *model.AppError {
if data.User == nil {
return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.user_missing.error", nil, "", http.StatusBadRequest)
}
if data.EmojiName == nil {
return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.emoji_name_missing.error", nil, "", http.StatusBadRequest)
} else if utf8.RuneCountInString(*data.EmojiName) > model.EmojiNameMaxLength {
return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.emoji_name_length.error", nil, "", http.StatusBadRequest)
}
if data.CreateAt == nil {
return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.create_at_missing.error", nil, "", http.StatusBadRequest)
} else if *data.CreateAt == 0 {
return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.create_at_zero.error", nil, "", http.StatusBadRequest)
} else if *data.CreateAt < parentCreateAt {
return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.create_at_before_parent.error", nil, "", http.StatusBadRequest)
}
return nil
}
func ValidateReplyImportData(data *ReplyImportData, parentCreateAt int64, maxPostSize int) *model.AppError {
if data.User == nil {
return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.user_missing.error", nil, "", http.StatusBadRequest)
}
if data.Message == nil {
return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.message_missing.error", nil, "", http.StatusBadRequest)
} else if utf8.RuneCountInString(*data.Message) > maxPostSize {
return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.message_length.error", nil, "", http.StatusBadRequest)
}
if data.CreateAt == nil {
return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.create_at_missing.error", nil, "", http.StatusBadRequest)
} else if *data.CreateAt == 0 {
return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.create_at_zero.error", nil, "", http.StatusBadRequest)
} else if *data.CreateAt < parentCreateAt {
mlog.Warn("Reply CreateAt is before parent post CreateAt", mlog.Int64("reply_create_at", *data.CreateAt), mlog.Int64("parent_create_at", parentCreateAt))
}
return nil
}
func ValidatePostImportData(data *PostImportData, maxPostSize int) *model.AppError {
if data.Team == nil {
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.team_missing.error", nil, "", http.StatusBadRequest)
}
if data.Channel == nil {
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.channel_missing.error", nil, "", http.StatusBadRequest)
}
if data.User == nil {
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.user_missing.error", nil, "", http.StatusBadRequest)
}
if data.Message == nil {
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.message_missing.error", nil, "", http.StatusBadRequest)
} else if utf8.RuneCountInString(*data.Message) > maxPostSize {
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.message_length.error", nil, "", http.StatusBadRequest)
}
if data.CreateAt == nil {
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.create_at_missing.error", nil, "", http.StatusBadRequest)
} else if *data.CreateAt == 0 {
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.create_at_zero.error", nil, "", http.StatusBadRequest)
}
if data.Reactions != nil {
for _, reaction := range *data.Reactions {
reaction := reaction
ValidateReactionImportData(&reaction, *data.CreateAt)
}
}
if data.Replies != nil {
for _, reply := range *data.Replies {
reply := reply
ValidateReplyImportData(&reply, *data.CreateAt, maxPostSize)
}
}
if data.Props != nil && utf8.RuneCountInString(model.StringInterfaceToJSON(*data.Props)) > model.PostPropsMaxRunes {
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.props_too_large.error", nil, "", http.StatusBadRequest)
}
return nil
}
func ValidateDirectChannelImportData(data *DirectChannelImportData) *model.AppError {
if data.Members == nil {
return model.NewAppError("BulkImport", "app.import.validate_direct_channel_import_data.members_required.error", nil, "", http.StatusBadRequest)
}
if len(*data.Members) != 2 {
if len(*data.Members) < model.ChannelGroupMinUsers {
return model.NewAppError("BulkImport", "app.import.validate_direct_channel_import_data.members_too_few.error", nil, "", http.StatusBadRequest)
} else if len(*data.Members) > model.ChannelGroupMaxUsers {
return model.NewAppError("BulkImport", "app.import.validate_direct_channel_import_data.members_too_many.error", nil, "", http.StatusBadRequest)
}
}
if data.Header != nil && utf8.RuneCountInString(*data.Header) > model.ChannelHeaderMaxRunes {
return model.NewAppError("BulkImport", "app.import.validate_direct_channel_import_data.header_length.error", nil, "", http.StatusBadRequest)
}
if data.FavoritedBy != nil {
for _, favoriter := range *data.FavoritedBy {
found := false
for _, member := range *data.Members {
if favoriter == member {
found = true
break
}
}
if !found {
return model.NewAppError("BulkImport", "app.import.validate_direct_channel_import_data.unknown_favoriter.error", map[string]any{"Username": favoriter}, "", http.StatusBadRequest)
}
}
}
return nil
}
func ValidateDirectPostImportData(data *DirectPostImportData, maxPostSize int) *model.AppError {
if data.ChannelMembers == nil {
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.channel_members_required.error", nil, "", http.StatusBadRequest)
}
if len(*data.ChannelMembers) != 2 {
if len(*data.ChannelMembers) < model.ChannelGroupMinUsers {
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.channel_members_too_few.error", nil, "", http.StatusBadRequest)
} else if len(*data.ChannelMembers) > model.ChannelGroupMaxUsers {
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.channel_members_too_many.error", nil, "", http.StatusBadRequest)
}
}
if data.User == nil {
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.user_missing.error", nil, "", http.StatusBadRequest)
}
if data.Message == nil {
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.message_missing.error", nil, "", http.StatusBadRequest)
} else if utf8.RuneCountInString(*data.Message) > maxPostSize {
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.message_length.error", nil, "", http.StatusBadRequest)
}
if data.CreateAt == nil {
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.create_at_missing.error", nil, "", http.StatusBadRequest)
} else if *data.CreateAt == 0 {
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.create_at_zero.error", nil, "", http.StatusBadRequest)
}
if data.FlaggedBy != nil {
for _, flagger := range *data.FlaggedBy {
found := false
for _, member := range *data.ChannelMembers {
if flagger == member {
found = true
break
}
}
if !found {
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.unknown_flagger.error", map[string]any{"Username": flagger}, "", http.StatusBadRequest)
}
}
}
if data.Reactions != nil {
for _, reaction := range *data.Reactions {
reaction := reaction
ValidateReactionImportData(&reaction, *data.CreateAt)
}
}
if data.Replies != nil {
for _, reply := range *data.Replies {
reply := reply
ValidateReplyImportData(&reply, *data.CreateAt, maxPostSize)
}
}
return nil
}
// ValidateEmojiImportData validates emoji data and returns if the import name
// conflicts with a system emoji.
func ValidateEmojiImportData(data *EmojiImportData) *model.AppError {
if data == nil {
return model.NewAppError("BulkImport", "app.import.validate_emoji_import_data.empty.error", nil, "", http.StatusBadRequest)
}
if data.Name == nil || *data.Name == "" {
return model.NewAppError("BulkImport", "app.import.validate_emoji_import_data.name_missing.error", nil, "", http.StatusBadRequest)
}
if data.Image == nil || *data.Image == "" {
return model.NewAppError("BulkImport", "app.import.validate_emoji_import_data.image_missing.error", nil, "", http.StatusBadRequest)
}
if err := model.IsValidEmojiName(*data.Name); err != nil {
return err
}
return nil
}
func isValidTrueOrFalseString(value string) bool {
return value == "true" || value == "false"
}
func isValidUserNotifyLevel(notifyLevel string) bool {
return notifyLevel == model.ChannelNotifyAll ||
notifyLevel == model.ChannelNotifyMention ||
notifyLevel == model.ChannelNotifyNone
}
func isValidPushStatusNotifyLevel(notifyLevel string) bool {
return notifyLevel == model.StatusOnline ||
notifyLevel == model.StatusAway ||
notifyLevel == model.StatusOffline
}
func isValidCommentsNotifyLevel(notifyLevel string) bool {
return notifyLevel == model.CommentsNotifyAny ||
notifyLevel == model.CommentsNotifyRoot ||
notifyLevel == model.CommentsNotifyNever
}
func isValidEmailBatchingInterval(emailInterval string) bool {
return emailInterval == model.PreferenceEmailIntervalImmediately ||
emailInterval == model.PreferenceEmailIntervalFifteen ||
emailInterval == model.PreferenceEmailIntervalHour
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Integration Action Flow
//
// 1. An integration creates an interactive message button or menu.
// 2. A user clicks on a button or selects an option from the menu.
// 3. The client sends a request to server to complete the post action, calling DoPostAction below.
// 4. DoPostAction will send an HTTP POST request to the integration containing contextual data, including
// an encoded and signed trigger ID. Slash commands also include trigger IDs in their payloads.
// 5. The integration performs any actions it needs to and optionally makes a request back to the MM server
// using the trigger ID to open an interactive dialog.
// 6. If that optional request is made, OpenInteractiveDialog sends a WebSocket event to all connected clients
// for the relevant user, telling them to display the dialog.
// 7. The user fills in the dialog and submits it, where SubmitInteractiveDialog will submit it back to the
// integration for handling.
package app
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
"path/filepath"
"strings"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) DoPostAction(c *request.Context, postID, actionId, userID, selectedOption string) (string, *model.AppError) {
return a.DoPostActionWithCookie(c, postID, actionId, userID, selectedOption, nil)
}
func (a *App) DoPostActionWithCookie(c *request.Context, postID, actionId, userID, selectedOption string, cookie *model.PostActionCookie) (string, *model.AppError) {
// PostAction may result in the original post being updated. For the
// updated post, we need to unconditionally preserve the original
// IsPinned and HasReaction attributes, and preserve its entire
// original Props set unless the plugin returns a replacement value.
// originalXxx variables are used to preserve these values.
var originalProps map[string]any
originalIsPinned := false
originalHasReactions := false
// If the updated post does contain a replacement Props set, we still
// need to preserve some original values, as listed in
// model.PostActionRetainPropKeys. remove and retain track these.
remove := []string{}
retain := map[string]any{}
datasource := ""
upstreamURL := ""
rootPostId := ""
upstreamRequest := &model.PostActionIntegrationRequest{
UserId: userID,
PostId: postID,
}
// See if the post exists in the DB, if so ignore the cookie.
// Start all queries here for parallel execution
pchan := make(chan store.StoreResult, 1)
go func() {
post, err := a.Srv().Store().Post().GetSingle(postID, false)
pchan <- store.StoreResult{Data: post, NErr: err}
close(pchan)
}()
cchan := make(chan store.StoreResult, 1)
go func() {
channel, err := a.Srv().Store().Channel().GetForPost(postID)
cchan <- store.StoreResult{Data: channel, NErr: err}
close(cchan)
}()
userChan := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), upstreamRequest.UserId)
userChan <- store.StoreResult{Data: user, NErr: err}
close(userChan)
}()
result := <-pchan
if result.NErr != nil {
if cookie == nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return "", model.NewAppError("DoPostActionWithCookie", "app.post.get.app_error", nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return "", model.NewAppError("DoPostActionWithCookie", "app.post.get.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
if cookie.Integration == nil {
return "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_integration.app_error", nil, "no Integration in action cookie", http.StatusBadRequest)
}
if postID != cookie.PostId {
return "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_integration.app_error", nil, "postId doesn't match", http.StatusBadRequest)
}
channel, err := a.Srv().Store().Channel().Get(cookie.ChannelId, true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return "", model.NewAppError("DoPostActionWithCookie", "app.channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return "", model.NewAppError("DoPostActionWithCookie", "app.channel.get.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
upstreamRequest.ChannelId = cookie.ChannelId
upstreamRequest.ChannelName = channel.Name
upstreamRequest.TeamId = channel.TeamId
upstreamRequest.Type = cookie.Type
upstreamRequest.Context = cookie.Integration.Context
datasource = cookie.DataSource
retain = cookie.RetainProps
remove = cookie.RemoveProps
rootPostId = cookie.RootPostId
upstreamURL = cookie.Integration.URL
} else {
post := result.Data.(*model.Post)
result = <-cchan
if result.NErr != nil {
return "", model.NewAppError("DoPostActionWithCookie", "app.channel.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
channel := result.Data.(*model.Channel)
action := post.GetAction(actionId)
if action == nil || action.Integration == nil {
return "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_id.app_error", nil, fmt.Sprintf("action=%v", action), http.StatusNotFound)
}
upstreamRequest.ChannelId = post.ChannelId
upstreamRequest.ChannelName = channel.Name
upstreamRequest.TeamId = channel.TeamId
upstreamRequest.Type = action.Type
upstreamRequest.Context = action.Integration.Context
datasource = action.DataSource
// Save the original values that may need to be preserved (including selected
// Props, i.e. override_username, override_icon_url)
for _, key := range model.PostActionRetainPropKeys {
value, ok := post.GetProps()[key]
if ok {
retain[key] = value
} else {
remove = append(remove, key)
}
}
originalProps = post.GetProps()
originalIsPinned = post.IsPinned
originalHasReactions = post.HasReactions
if post.RootId == "" {
rootPostId = post.Id
} else {
rootPostId = post.RootId
}
upstreamURL = action.Integration.URL
}
teamChan := make(chan store.StoreResult, 1)
go func() {
defer close(teamChan)
// Direct and group channels won't have teams.
if upstreamRequest.TeamId == "" {
return
}
team, err := a.Srv().Store().Team().Get(upstreamRequest.TeamId)
teamChan <- store.StoreResult{Data: team, NErr: err}
}()
ur := <-userChan
if ur.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(ur.NErr, &nfErr):
return "", model.NewAppError("DoPostActionWithCookie", MissingAccountError, nil, "", http.StatusNotFound).Wrap(ur.NErr)
default:
return "", model.NewAppError("DoPostActionWithCookie", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(ur.NErr)
}
}
user := ur.Data.(*model.User)
upstreamRequest.UserName = user.Username
tr, ok := <-teamChan
if ok {
if tr.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(tr.NErr, &nfErr):
return "", model.NewAppError("DoPostActionWithCookie", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(tr.NErr)
default:
return "", model.NewAppError("DoPostActionWithCookie", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(tr.NErr)
}
}
team := tr.Data.(*model.Team)
upstreamRequest.TeamName = team.Name
}
if upstreamRequest.Type == model.PostActionTypeSelect {
if selectedOption != "" {
if upstreamRequest.Context == nil {
upstreamRequest.Context = map[string]any{}
}
upstreamRequest.DataSource = datasource
upstreamRequest.Context["selected_option"] = selectedOption
}
}
clientTriggerId, _, appErr := upstreamRequest.GenerateTriggerId(a.AsymmetricSigningKey())
if appErr != nil {
return "", appErr
}
if strings.HasPrefix(upstreamURL, "/warn_metrics/") {
appErr = a.doLocalWarnMetricsRequest(c, upstreamURL, upstreamRequest)
if appErr != nil {
return "", appErr
}
return "", nil
}
requestJSON, err := json.Marshal(upstreamRequest)
if err != nil {
return "", model.NewAppError("DoPostActionWithCookie", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
resp, appErr := a.DoActionRequest(c, upstreamURL, requestJSON)
if appErr != nil {
return "", appErr
}
defer resp.Body.Close()
var response model.PostActionIntegrationResponse
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if len(respBytes) > 0 {
if err = json.Unmarshal(respBytes, &response); err != nil {
return "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
}
if response.Update != nil {
response.Update.Id = postID
// Restore the post attributes and Props that need to be preserved
if response.Update.GetProps() == nil {
response.Update.SetProps(originalProps)
} else {
for key, value := range retain {
response.Update.AddProp(key, value)
}
for _, key := range remove {
response.Update.DelProp(key)
}
}
response.Update.IsPinned = originalIsPinned
response.Update.HasReactions = originalHasReactions
if _, appErr = a.UpdatePost(c, response.Update, false); appErr != nil {
return "", appErr
}
}
if response.EphemeralText != "" {
ephemeralPost := &model.Post{
Message: response.EphemeralText,
ChannelId: upstreamRequest.ChannelId,
RootId: rootPostId,
UserId: userID,
}
if !response.SkipSlackParsing {
ephemeralPost.Message = model.ParseSlackLinksToMarkdown(response.EphemeralText)
}
for key, value := range retain {
ephemeralPost.AddProp(key, value)
}
a.SendEphemeralPost(c, userID, ephemeralPost)
}
return clientTriggerId, nil
}
// Perform an HTTP POST request to an integration's action endpoint.
// Caller must consume and close returned http.Response as necessary.
// For internal requests, requests are routed directly to a plugin ServerHTTP hook
func (a *App) DoActionRequest(c *request.Context, rawURL string, body []byte) (*http.Response, *model.AppError) {
inURL, err := url.Parse(rawURL)
if err != nil {
return nil, model.NewAppError("DoActionRequest", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
rawURLPath := path.Clean(rawURL)
if strings.HasPrefix(rawURLPath, "/plugins/") || strings.HasPrefix(rawURLPath, "plugins/") {
return a.DoLocalRequest(c, rawURLPath, body)
}
req, err := http.NewRequest("POST", rawURL, bytes.NewReader(body))
if err != nil {
return nil, model.NewAppError("DoActionRequest", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
// Allow access to plugin routes for action buttons
var httpClient *http.Client
subpath, _ := utils.GetSubpathFromConfig(a.Config())
siteURL, _ := url.Parse(*a.Config().ServiceSettings.SiteURL)
if (inURL.Hostname() == "localhost" || inURL.Hostname() == "127.0.0.1" || inURL.Hostname() == siteURL.Hostname()) && strings.HasPrefix(inURL.Path, path.Join(subpath, "plugins")) {
req.Header.Set(model.HeaderAuth, "Bearer "+c.Session().Token)
httpClient = a.HTTPService().MakeClient(true)
} else {
httpClient = a.HTTPService().MakeClient(false)
}
resp, httpErr := httpClient.Do(req)
if httpErr != nil {
return nil, model.NewAppError("DoActionRequest", "api.post.do_action.action_integration.app_error", nil, "err="+httpErr.Error(), http.StatusBadRequest)
}
if resp.StatusCode != http.StatusOK {
return resp, model.NewAppError("DoActionRequest", "api.post.do_action.action_integration.app_error", nil, fmt.Sprintf("status=%v", resp.StatusCode), http.StatusBadRequest)
}
return resp, nil
}
type LocalResponseWriter struct {
data []byte
headers http.Header
status int
}
func (w *LocalResponseWriter) Header() http.Header {
if w.headers == nil {
w.headers = make(http.Header)
}
return w.headers
}
func (w *LocalResponseWriter) Write(bytes []byte) (int, error) {
w.data = make([]byte, len(bytes))
copy(w.data, bytes)
return len(w.data), nil
}
func (w *LocalResponseWriter) WriteHeader(statusCode int) {
w.status = statusCode
}
func (a *App) doPluginRequest(c *request.Context, method, rawURL string, values url.Values, body []byte) (*http.Response, *model.AppError) {
return a.ch.doPluginRequest(c, method, rawURL, values, body)
}
func (ch *Channels) doPluginRequest(c *request.Context, method, rawURL string, values url.Values, body []byte) (*http.Response, *model.AppError) {
rawURL = strings.TrimPrefix(rawURL, "/")
inURL, err := url.Parse(rawURL)
if err != nil {
return nil, model.NewAppError("doPluginRequest", "api.post.do_action.action_integration.app_error", nil, "err="+err.Error(), http.StatusBadRequest)
}
result := strings.Split(inURL.Path, "/")
if len(result) < 2 {
return nil, model.NewAppError("doPluginRequest", "api.post.do_action.action_integration.app_error", nil, "err=Unable to find pluginId", http.StatusBadRequest)
}
if result[0] != "plugins" {
return nil, model.NewAppError("doPluginRequest", "api.post.do_action.action_integration.app_error", nil, "err=plugins not in path", http.StatusBadRequest)
}
pluginID := result[1]
path := strings.TrimPrefix(inURL.Path, "plugins/"+pluginID)
base, err := url.Parse(path)
if err != nil {
return nil, model.NewAppError("doPluginRequest", "api.post.do_action.action_integration.app_error", nil, "err="+err.Error(), http.StatusBadRequest)
}
// merge the rawQuery params (if any) with the function's provided values
rawValues := inURL.Query()
if len(rawValues) != 0 {
if values == nil {
values = make(url.Values)
}
for k, vs := range rawValues {
for _, v := range vs {
values.Add(k, v)
}
}
}
if values != nil {
base.RawQuery = values.Encode()
}
w := &LocalResponseWriter{}
r, err := http.NewRequest(method, base.String(), bytes.NewReader(body))
if err != nil {
return nil, model.NewAppError("doPluginRequest", "api.post.do_action.action_integration.app_error", nil, "err="+err.Error(), http.StatusBadRequest)
}
r.Header.Set("Mattermost-User-Id", c.Session().UserId)
r.Header.Set(model.HeaderAuth, "Bearer "+c.Session().Token)
params := make(map[string]string)
params["plugin_id"] = pluginID
r = mux.SetURLVars(r, params)
ch.ServePluginRequest(w, r)
resp := &http.Response{
StatusCode: w.status,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: w.headers,
Body: io.NopCloser(bytes.NewReader(w.data)),
}
if resp.StatusCode == 0 {
resp.StatusCode = http.StatusOK
}
return resp, nil
}
func (a *App) doLocalWarnMetricsRequest(c *request.Context, rawURL string, upstreamRequest *model.PostActionIntegrationRequest) *model.AppError {
_, err := url.Parse(rawURL)
if err != nil {
return model.NewAppError("doLocalWarnMetricsRequest", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
warnMetricId := filepath.Base(rawURL)
if warnMetricId == "" {
return model.NewAppError("doLocalWarnMetricsRequest", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest)
}
license := a.Srv().License()
if license != nil {
mlog.Debug("License is present, skip this call")
return nil
}
user, appErr := a.GetUser(c.Session().UserId)
if appErr != nil {
return appErr
}
botPost := &model.Post{
UserId: upstreamRequest.Context["bot_user_id"].(string),
ChannelId: upstreamRequest.ChannelId,
HasReactions: true,
}
isE0Edition := (model.BuildEnterpriseReady == "true") // license == nil was already validated upstream
_, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, i18n.T, isE0Edition)
botPost.Message = ":white_check_mark: " + warnMetricDisplayTexts.BotSuccessMessage
if isE0Edition {
if appErr = a.RequestLicenseAndAckWarnMetric(c, warnMetricId, true); appErr != nil {
botPost.Message = ":warning: " + i18n.T("api.server.warn_metric.bot_response.start_trial_failure.message")
}
} else {
forceAck := upstreamRequest.Context["force_ack"].(bool)
if appErr = a.NotifyAndSetWarnMetricAck(warnMetricId, user, forceAck, true); appErr != nil {
if forceAck {
return appErr
}
mailtoLinkText := a.buildWarnMetricMailtoLink(warnMetricId, user)
botPost.Message = ":warning: " + i18n.T("api.server.warn_metric.bot_response.notification_failure.message")
actions := []*model.PostAction{}
actions = append(actions,
&model.PostAction{
Id: "emailUs",
Name: i18n.T("api.server.warn_metric.email_us"),
Type: model.PostActionTypeButton,
Options: []*model.PostActionOptions{
{
Text: "WarnMetricMailtoUrl",
Value: mailtoLinkText,
},
{
Text: "TrackEventId",
Value: warnMetricId,
},
},
Integration: &model.PostActionIntegration{
Context: model.StringInterface{
"bot_user_id": botPost.UserId,
"force_ack": true,
},
URL: fmt.Sprintf("/warn_metrics/ack/%s", model.SystemWarnMetricNumberOfActiveUsers500),
},
},
)
attachments := []*model.SlackAttachment{{
AuthorName: "",
Title: "",
Actions: actions,
Text: i18n.T("api.server.warn_metric.bot_response.notification_failure.body"),
}}
model.ParseSlackAttachment(botPost, attachments)
}
}
if _, err := a.CreatePostAsUser(c, botPost, c.Session().Id, true); err != nil {
return err
}
return nil
}
type MailToLinkContent struct {
MetricId string `json:"metric_id"`
MailRecipient string `json:"mail_recipient"`
MailCC string `json:"mail_cc"`
MailSubject string `json:"mail_subject"`
MailBody string `json:"mail_body"`
}
func (mlc *MailToLinkContent) ToJSON() string {
b, _ := json.Marshal(mlc)
return string(b)
}
func (a *App) buildWarnMetricMailtoLink(warnMetricId string, user *model.User) string {
T := i18n.GetUserTranslations(user.Locale)
_, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, T, false)
mailBody := warnMetricDisplayTexts.EmailBody
mailBody += T("api.server.warn_metric.bot_response.mailto_contact_header", map[string]any{"Contact": user.GetFullName()})
mailBody += "\r\n"
mailBody += T("api.server.warn_metric.bot_response.mailto_email_header", map[string]any{"Email": user.Email})
mailBody += "\r\n"
registeredUsersCount, err := a.Srv().Store().User().Count(model.UserCountOptions{})
if err != nil {
mlog.Warn("Error retrieving the number of registered users", mlog.Err(err))
} else {
mailBody += i18n.T("api.server.warn_metric.bot_response.mailto_registered_users_header", map[string]any{"NoRegisteredUsers": registeredUsersCount})
mailBody += "\r\n"
}
mailBody += T("api.server.warn_metric.bot_response.mailto_site_url_header", map[string]any{"SiteUrl": a.GetSiteURL()})
mailBody += "\r\n"
mailBody += T("api.server.warn_metric.bot_response.mailto_diagnostic_id_header", map[string]any{"DiagnosticId": a.TelemetryId()})
mailBody += "\r\n"
mailBody += T("api.server.warn_metric.bot_response.mailto_footer")
mailToLinkContent := &MailToLinkContent{
MetricId: warnMetricId,
MailRecipient: model.MmSupportAdvisorAddress,
MailCC: user.Email,
MailSubject: T("api.server.warn_metric.bot_response.mailto_subject"),
MailBody: mailBody,
}
return mailToLinkContent.ToJSON()
}
func (a *App) DoLocalRequest(c *request.Context, rawURL string, body []byte) (*http.Response, *model.AppError) {
return a.doPluginRequest(c, "POST", rawURL, nil, body)
}
func (a *App) OpenInteractiveDialog(request model.OpenDialogRequest) *model.AppError {
clientTriggerId, userID, appErr := request.DecodeAndVerifyTriggerId(a.AsymmetricSigningKey())
if appErr != nil {
return appErr
}
request.TriggerId = clientTriggerId
jsonRequest, err := json.Marshal(request)
if err != nil {
a.ch.srv.Log().Warn("Error encoding request", mlog.Err(err))
}
message := model.NewWebSocketEvent(model.WebsocketEventOpenDialog, "", "", userID, nil, "")
message.Add("dialog", string(jsonRequest))
a.Publish(message)
return nil
}
func (a *App) SubmitInteractiveDialog(c *request.Context, request model.SubmitDialogRequest) (*model.SubmitDialogResponse, *model.AppError) {
url := request.URL
request.URL = ""
request.Type = "dialog_submission"
b, err := json.Marshal(request)
if err != nil {
return nil, model.NewAppError("SubmitInteractiveDialog", "app.submit_interactive_dialog.json_error", nil, "", http.StatusBadRequest).Wrap(err)
}
resp, appErr := a.DoActionRequest(c, url, b)
if appErr != nil {
return nil, appErr
}
defer resp.Body.Close()
var response model.SubmitDialogResponse
json.NewDecoder(resp.Body).Decode(&response) // Don't fail, an empty response is acceptable
return &response, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func (a *App) GetJob(id string) (*model.Job, *model.AppError) {
job, err := a.Srv().Store().Job().Get(id)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetJob", "app.job.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetJob", "app.job.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return job, nil
}
func (a *App) GetJobsPage(page int, perPage int) ([]*model.Job, *model.AppError) {
return a.GetJobs(page*perPage, perPage)
}
func (a *App) GetJobs(offset int, limit int) ([]*model.Job, *model.AppError) {
jobs, err := a.Srv().Store().Job().GetAllPage(offset, limit)
if err != nil {
return nil, model.NewAppError("GetJobs", "app.job.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return jobs, nil
}
func (a *App) GetJobsByTypePage(jobType string, page int, perPage int) ([]*model.Job, *model.AppError) {
return a.GetJobsByType(jobType, page*perPage, perPage)
}
func (a *App) GetJobsByType(jobType string, offset int, limit int) ([]*model.Job, *model.AppError) {
jobs, err := a.Srv().Store().Job().GetAllByTypePage(jobType, offset, limit)
if err != nil {
return nil, model.NewAppError("GetJobsByType", "app.job.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return jobs, nil
}
func (a *App) GetJobsByTypesPage(jobType []string, page int, perPage int) ([]*model.Job, *model.AppError) {
return a.GetJobsByTypes(jobType, page*perPage, perPage)
}
func (a *App) GetJobsByTypes(jobTypes []string, offset int, limit int) ([]*model.Job, *model.AppError) {
jobs, err := a.Srv().Store().Job().GetAllByTypesPage(jobTypes, offset, limit)
if err != nil {
return nil, model.NewAppError("GetJobsByType", "app.job.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return jobs, nil
}
func (a *App) CreateJob(job *model.Job) (*model.Job, *model.AppError) {
return a.Srv().Jobs.CreateJob(job.Type, job.Data)
}
func (a *App) CancelJob(jobId string) *model.AppError {
return a.Srv().Jobs.RequestCancellation(jobId)
}
func (a *App) SessionHasPermissionToCreateJob(session model.Session, job *model.Job) (bool, *model.Permission) {
switch job.Type {
case model.JobTypeBlevePostIndexing:
return a.SessionHasPermissionTo(session, model.PermissionCreatePostBleveIndexesJob), model.PermissionCreatePostBleveIndexesJob
case model.JobTypeDataRetention:
return a.SessionHasPermissionTo(session, model.PermissionCreateDataRetentionJob), model.PermissionCreateDataRetentionJob
case model.JobTypeMessageExport:
return a.SessionHasPermissionTo(session, model.PermissionCreateComplianceExportJob), model.PermissionCreateComplianceExportJob
case model.JobTypeElasticsearchPostIndexing:
return a.SessionHasPermissionTo(session, model.PermissionCreateElasticsearchPostIndexingJob), model.PermissionCreateElasticsearchPostIndexingJob
case model.JobTypeElasticsearchPostAggregation:
return a.SessionHasPermissionTo(session, model.PermissionCreateElasticsearchPostAggregationJob), model.PermissionCreateElasticsearchPostAggregationJob
case model.JobTypeLdapSync:
return a.SessionHasPermissionTo(session, model.PermissionCreateLdapSyncJob), model.PermissionCreateLdapSyncJob
case
model.JobTypeMigrations,
model.JobTypePlugins,
model.JobTypeProductNotices,
model.JobTypeExpiryNotify,
model.JobTypeActiveUsers,
model.JobTypeImportProcess,
model.JobTypeImportDelete,
model.JobTypeExportProcess,
model.JobTypeExportDelete,
model.JobTypeCloud,
model.JobTypeExtractContent:
return a.SessionHasPermissionTo(session, model.PermissionManageJobs), model.PermissionManageJobs
}
return false, nil
}
func (a *App) SessionHasPermissionToReadJob(session model.Session, jobType string) (bool, *model.Permission) {
switch jobType {
case model.JobTypeDataRetention:
return a.SessionHasPermissionTo(session, model.PermissionReadDataRetentionJob), model.PermissionReadDataRetentionJob
case model.JobTypeMessageExport:
return a.SessionHasPermissionTo(session, model.PermissionReadComplianceExportJob), model.PermissionReadComplianceExportJob
case model.JobTypeElasticsearchPostIndexing:
return a.SessionHasPermissionTo(session, model.PermissionReadElasticsearchPostIndexingJob), model.PermissionReadElasticsearchPostIndexingJob
case model.JobTypeElasticsearchPostAggregation:
return a.SessionHasPermissionTo(session, model.PermissionReadElasticsearchPostAggregationJob), model.PermissionReadElasticsearchPostAggregationJob
case model.JobTypeLdapSync:
return a.SessionHasPermissionTo(session, model.PermissionReadLdapSyncJob), model.PermissionReadLdapSyncJob
case
model.JobTypeBlevePostIndexing,
model.JobTypeMigrations,
model.JobTypePlugins,
model.JobTypeProductNotices,
model.JobTypeExpiryNotify,
model.JobTypeActiveUsers,
model.JobTypeImportProcess,
model.JobTypeImportDelete,
model.JobTypeExportProcess,
model.JobTypeExportDelete,
model.JobTypeCloud,
model.JobTypeExtractContent:
return a.SessionHasPermissionTo(session, model.PermissionReadJobs), model.PermissionReadJobs
}
return false, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"io"
"mime/multipart"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// SyncLdap starts an LDAP sync job.
// If includeRemovedMembers is true, then members who left or were removed from a team/channel will
// be re-added; otherwise, they will not be re-added.
func (a *App) SyncLdap(includeRemovedMembers bool) {
a.Srv().Go(func() {
if license := a.Srv().License(); license != nil && *license.Features.LDAP {
if !*a.Config().LdapSettings.EnableSync {
mlog.Error("LdapSettings.EnableSync is set to false. Skipping LDAP sync.")
return
}
ldapI := a.Ldap()
if ldapI == nil {
mlog.Error("Not executing ldap sync because ldap is not available")
return
}
ldapI.StartSynchronizeJob(false, includeRemovedMembers)
}
})
}
func (a *App) TestLdap() *model.AppError {
license := a.Srv().License()
if ldapI := a.Ldap(); ldapI != nil && license != nil && *license.Features.LDAP && (*a.Config().LdapSettings.Enable || *a.Config().LdapSettings.EnableSync) {
if err := ldapI.RunTest(); err != nil {
err.StatusCode = 500
return err
}
} else {
err := model.NewAppError("TestLdap", "ent.ldap.disabled.app_error", nil, "", http.StatusNotImplemented)
return err
}
return nil
}
// GetLdapGroup retrieves a single LDAP group by the given LDAP group id.
func (a *App) GetLdapGroup(ldapGroupID string) (*model.Group, *model.AppError) {
var group *model.Group
if a.Ldap() != nil {
var err *model.AppError
group, err = a.Ldap().GetGroup(ldapGroupID)
if err != nil {
return nil, err
}
} else {
ae := model.NewAppError("GetLdapGroup", "ent.ldap.app_error", map[string]any{"ldap_group_id": ldapGroupID}, "", http.StatusNotImplemented)
return nil, ae
}
return group, nil
}
// GetAllLdapGroupsPage retrieves all LDAP groups under the configured base DN using the default or configured group
// filter.
func (a *App) GetAllLdapGroupsPage(page int, perPage int, opts model.LdapGroupSearchOpts) ([]*model.Group, int, *model.AppError) {
var groups []*model.Group
var total int
if a.Ldap() != nil {
var err *model.AppError
groups, total, err = a.Ldap().GetAllGroupsPage(page, perPage, opts)
if err != nil {
return nil, 0, err
}
} else {
ae := model.NewAppError("GetAllLdapGroupsPage", "ent.ldap.app_error", nil, "", http.StatusNotImplemented)
return nil, 0, ae
}
return groups, total, nil
}
func (a *App) SwitchEmailToLdap(email, password, code, ldapLoginId, ldapPassword string) (string, *model.AppError) {
if a.Srv().License() != nil && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
return "", model.NewAppError("emailToLdap", "api.user.email_to_ldap.not_available.app_error", nil, "", http.StatusForbidden)
}
user, err := a.GetUserByEmail(email)
if err != nil {
return "", err
}
if err := a.CheckPasswordAndAllCriteria(user, password, code); err != nil {
return "", err
}
if err := a.RevokeAllSessions(user.Id); err != nil {
return "", err
}
ldapInterface := a.Ldap()
if ldapInterface == nil {
return "", model.NewAppError("SwitchEmailToLdap", "api.user.email_to_ldap.not_available.app_error", nil, "", http.StatusNotImplemented)
}
if err := ldapInterface.SwitchToLdap(user.Id, ldapLoginId, ldapPassword); err != nil {
return "", err
}
a.Srv().Go(func() {
if err := a.Srv().EmailService.SendSignInChangeEmail(user.Email, "AD/LDAP", user.Locale, a.GetSiteURL()); err != nil {
mlog.Error("Could not send sign in method changed e-mail", mlog.Err(err))
}
})
return "/login?extra=signin_change", nil
}
func (a *App) SwitchLdapToEmail(ldapPassword, code, email, newPassword string) (string, *model.AppError) {
if a.Srv().License() != nil && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
return "", model.NewAppError("ldapToEmail", "api.user.ldap_to_email.not_available.app_error", nil, "", http.StatusForbidden)
}
user, err := a.GetUserByEmail(email)
if err != nil {
return "", err
}
if user.AuthService != model.UserAuthServiceLdap {
return "", model.NewAppError("SwitchLdapToEmail", "api.user.ldap_to_email.not_ldap_account.app_error", nil, "", http.StatusBadRequest)
}
ldapInterface := a.Ldap()
if ldapInterface == nil || user.AuthData == nil {
return "", model.NewAppError("SwitchLdapToEmail", "api.user.ldap_to_email.not_available.app_error", nil, "", http.StatusNotImplemented)
}
if err := ldapInterface.CheckPasswordAuthData(*user.AuthData, ldapPassword); err != nil {
return "", err
}
if err := a.CheckUserMfa(user, code); err != nil {
return "", err
}
if err := a.UpdatePassword(user, newPassword); err != nil {
return "", err
}
if err := a.RevokeAllSessions(user.Id); err != nil {
return "", err
}
T := i18n.GetUserTranslations(user.Locale)
a.Srv().Go(func() {
if err := a.Srv().EmailService.SendSignInChangeEmail(user.Email, T("api.templates.signin_change_email.body.method_email"), user.Locale, a.GetSiteURL()); err != nil {
mlog.Error("Could not send sign in method changed e-mail", mlog.Err(err))
}
})
return "/login?extra=signin_change", nil
}
func (a *App) MigrateIdLDAP(toAttribute string) *model.AppError {
if ldapI := a.Ldap(); ldapI != nil {
if err := ldapI.MigrateIDAttribute(toAttribute); err != nil {
switch err := err.(type) {
case *model.AppError:
return err
default:
return model.NewAppError("IdMigrateLDAP", "ent.ldap_id_migrate.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return nil
}
return model.NewAppError("IdMigrateLDAP", "ent.ldap.disabled.app_error", nil, "", http.StatusNotImplemented)
}
func (a *App) writeLdapFile(filename string, fileData *multipart.FileHeader) *model.AppError {
file, err := fileData.Open()
if err != nil {
return model.NewAppError("AddLdapCertificate", "api.admin.add_certificate.open.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return model.NewAppError("AddLdapCertificate", "api.admin.add_certificate.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
err = a.Srv().platform.SetConfigFile(filename, data)
if err != nil {
return model.NewAppError("AddLdapCertificate", "api.admin.add_certificate.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) AddLdapPublicCertificate(fileData *multipart.FileHeader) *model.AppError {
if err := a.writeLdapFile(model.LdapPublicCertificateName, fileData); err != nil {
return err
}
cfg := a.Config().Clone()
*cfg.LdapSettings.PublicCertificateFile = model.LdapPublicCertificateName
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
func (a *App) AddLdapPrivateCertificate(fileData *multipart.FileHeader) *model.AppError {
if err := a.writeLdapFile(model.LdapPrivateKeyName, fileData); err != nil {
return err
}
cfg := a.Config().Clone()
*cfg.LdapSettings.PrivateKeyFile = model.LdapPrivateKeyName
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
func (a *App) removeLdapFile(filename string) *model.AppError {
if err := a.Srv().platform.RemoveConfigFile(filename); err != nil {
return model.NewAppError("RemoveLdapFile", "api.admin.remove_certificate.delete.app_error", map[string]any{"Filename": filename}, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) RemoveLdapPublicCertificate() *model.AppError {
if err := a.removeLdapFile(*a.Config().LdapSettings.PublicCertificateFile); err != nil {
return err
}
cfg := a.Config().Clone()
*cfg.LdapSettings.PublicCertificateFile = ""
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
func (a *App) RemoveLdapPrivateCertificate() *model.AppError {
if err := a.removeLdapFile(*a.Config().LdapSettings.PrivateKeyFile); err != nil {
return err
}
cfg := a.Config().Clone()
*cfg.LdapSettings.PrivateKeyFile = ""
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const (
JWTDefaultTokenExpiration = 7 * 24 * time.Hour // 7 days of expiration
)
// ensure the license service wrapper implements `product.LicenseService`
var _ product.LicenseService = (*licenseWrapper)(nil)
// licenseWrapper is an adapter struct that only exposes the
// config related functionality to be passed down to other products.
type licenseWrapper struct {
srv *Server
}
func (w *licenseWrapper) Name() product.ServiceKey {
return product.LicenseKey
}
func (w *licenseWrapper) GetLicense() *model.License {
return w.srv.License()
}
func (w *licenseWrapper) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError {
if *w.srv.platform.Config().ExperimentalSettings.RestrictSystemAdmin {
return model.NewAppError("RequestTrialLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden)
}
if !termsAccepted {
return model.NewAppError("RequestTrialLicense", "api.license.request-trial.bad-request.terms-not-accepted", nil, "", http.StatusBadRequest)
}
if users == 0 {
return model.NewAppError("RequestTrialLicense", "api.license.request-trial.bad-request", nil, "", http.StatusBadRequest)
}
requester, err := w.srv.userService.GetUser(requesterID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return model.NewAppError("RequestTrialLicense", MissingAccountError, nil, "", http.StatusNotFound).Wrap(err)
default:
return model.NewAppError("RequestTrialLicense", "app.user.get_by_username.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
trialLicenseRequest := &model.TrialLicenseRequest{
ServerID: w.srv.TelemetryId(),
Name: requester.GetDisplayName(model.ShowFullName),
Email: requester.Email,
SiteName: *w.srv.platform.Config().TeamSettings.SiteName,
SiteURL: *w.srv.platform.Config().ServiceSettings.SiteURL,
Users: users,
TermsAccepted: termsAccepted,
ReceiveEmailsAccepted: receiveEmailsAccepted,
}
return w.srv.platform.RequestTrialLicense(trialLicenseRequest)
}
// JWTClaims custom JWT claims with the needed information for the
// renewal process
type JWTClaims struct {
LicenseID string `json:"license_id"`
ActiveUsers int64 `json:"active_users"`
jwt.StandardClaims
}
func (s *Server) License() *model.License {
return s.platform.License()
}
func (s *Server) LoadLicense() {
s.platform.LoadLicense()
}
func (s *Server) SaveLicense(licenseBytes []byte) (*model.License, *model.AppError) {
return s.platform.SaveLicense(licenseBytes)
}
func (s *Server) SetLicense(license *model.License) bool {
return s.platform.SetLicense(license)
}
func (s *Server) ValidateAndSetLicenseBytes(b []byte) bool {
return s.platform.ValidateAndSetLicenseBytes(b)
}
func (s *Server) SetClientLicense(m map[string]string) {
s.platform.SetClientLicense(m)
}
func (s *Server) ClientLicense() map[string]string {
return s.platform.ClientLicense()
}
func (s *Server) RemoveLicense() *model.AppError {
return s.platform.RemoveLicense()
}
func (s *Server) AddLicenseListener(listener func(oldLicense, newLicense *model.License)) string {
return s.platform.AddLicenseListener(listener)
}
func (s *Server) RemoveLicenseListener(id string) {
s.platform.RemoveLicenseListener(id)
}
func (s *Server) GetSanitizedClientLicense() map[string]string {
return s.platform.GetSanitizedClientLicense()
}
// GenerateRenewalToken returns a renewal token that expires after duration expiration
func (s *Server) GenerateRenewalToken(expiration time.Duration) (string, *model.AppError) {
return s.platform.GenerateRenewalToken(expiration)
}
// GenerateLicenseRenewalLink returns a link that points to the CWS where clients can renew license
func (s *Server) GenerateLicenseRenewalLink() (string, string, *model.AppError) {
return s.platform.GenerateLicenseRenewalLink()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"crypto/subtle"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/avct/uasurfer"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const cwsTokenEnv = "CWS_CLOUD_TOKEN"
func (a *App) CheckForClientSideCert(r *http.Request) (string, string, string) {
pem := r.Header.Get("X-SSL-Client-Cert") // mapped to $ssl_client_cert from nginx
subject := r.Header.Get("X-SSL-Client-Cert-Subject-DN") // mapped to $ssl_client_s_dn from nginx
email := ""
if subject != "" {
for _, v := range strings.Split(subject, "/") {
kv := strings.Split(v, "=")
if len(kv) == 2 && kv[0] == "emailAddress" {
email = kv[1]
}
}
}
return pem, subject, email
}
func (a *App) AuthenticateUserForLogin(c *request.Context, id, loginId, password, mfaToken, cwsToken string, ldapOnly bool) (user *model.User, err *model.AppError) {
// Do statistics
defer func() {
if a.Metrics() != nil {
if user == nil || err != nil {
a.Metrics().IncrementLoginFail()
} else {
a.Metrics().IncrementLogin()
}
}
}()
if password == "" && !IsCWSLogin(a, cwsToken) {
return nil, model.NewAppError("AuthenticateUserForLogin", "api.user.login.blank_pwd.app_error", nil, "", http.StatusBadRequest)
}
// Get the MM user we are trying to login
if user, err = a.GetUserForLogin(id, loginId); err != nil {
return nil, err
}
// CWS login allow to use the one-time token to login the users when they're redirected to their
// installation for the first time
if IsCWSLogin(a, cwsToken) {
if err = checkUserNotBot(user); err != nil {
return nil, err
}
token, err := a.Srv().Store().Token().GetByToken(cwsToken)
if nfErr := new(store.ErrNotFound); err != nil && !errors.As(err, &nfErr) {
mlog.Debug("Error retrieving the cws token from the store", mlog.Err(err))
return nil, model.NewAppError("AuthenticateUserForLogin",
"api.user.login_by_cws.invalid_token.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// If token is stored in the database that means it was used
if token != nil {
return nil, model.NewAppError("AuthenticateUserForLogin",
"api.user.login_by_cws.invalid_token.app_error", nil, "", http.StatusBadRequest)
}
envToken, ok := os.LookupEnv(cwsTokenEnv)
if ok && subtle.ConstantTimeCompare([]byte(envToken), []byte(cwsToken)) == 1 {
token = &model.Token{
Token: cwsToken,
CreateAt: model.GetMillis(),
Type: TokenTypeCWSAccess,
}
err := a.Srv().Store().Token().Save(token)
if err != nil {
mlog.Debug("Error storing the cws token in the store", mlog.Err(err))
return nil, model.NewAppError("AuthenticateUserForLogin",
"api.user.login_by_cws.invalid_token.app_error", nil, "", http.StatusInternalServerError)
}
return user, nil
}
return nil, model.NewAppError("AuthenticateUserForLogin",
"api.user.login_by_cws.invalid_token.app_error", nil, "", http.StatusBadRequest)
}
// If client side cert is enable and it's checking as a primary source
// then trust the proxy and cert that the correct user is supplied and allow
// them access
if *a.Config().ExperimentalSettings.ClientSideCertEnable && *a.Config().ExperimentalSettings.ClientSideCertCheck == model.ClientSideCertCheckPrimaryAuth {
// Unless the user is a bot.
if err = checkUserNotBot(user); err != nil {
return nil, err
}
return user, nil
}
// and then authenticate them
if user, err = a.authenticateUser(c, user, password, mfaToken); err != nil {
return nil, err
}
return user, nil
}
func (a *App) GetUserForLogin(id, loginId string) (*model.User, *model.AppError) {
enableUsername := *a.Config().EmailSettings.EnableSignInWithUsername
enableEmail := *a.Config().EmailSettings.EnableSignInWithEmail
// If we are given a userID then fail if we can't find a user with that ID
if id != "" {
user, err := a.GetUser(id)
if err != nil {
if err.Id != MissingAccountError {
err.StatusCode = http.StatusInternalServerError
return nil, err
}
err.StatusCode = http.StatusBadRequest
return nil, err
}
return user, nil
}
// Try to get the user by username/email
if user, err := a.Srv().Store().User().GetForLogin(loginId, enableUsername, enableEmail); err == nil {
return user, nil
}
// Try to get the user with LDAP if enabled
if *a.Config().LdapSettings.Enable && a.Ldap() != nil {
if ldapUser, err := a.Ldap().GetUser(loginId); err == nil {
if user, err := a.GetUserByAuth(ldapUser.AuthData, model.UserAuthServiceLdap); err == nil {
return user, nil
}
return ldapUser, nil
}
}
return nil, model.NewAppError("GetUserForLogin", "store.sql_user.get_for_login.app_error", nil, "", http.StatusBadRequest)
}
func (a *App) DoLogin(c *request.Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceID string, isMobile, isOAuthUser, isSaml bool) *model.AppError {
var rejectionReason string
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
rejectionReason = hooks.UserWillLogIn(pluginContext, user)
return rejectionReason == ""
}, plugin.UserWillLogInID)
if rejectionReason != "" {
return model.NewAppError("DoLogin", "Login rejected by plugin: "+rejectionReason, nil, "", http.StatusBadRequest)
}
session := &model.Session{UserId: user.Id, Roles: user.GetRawRoles(), DeviceId: deviceID, IsOAuth: false, Props: map[string]string{
model.UserAuthServiceIsMobile: strconv.FormatBool(isMobile),
model.UserAuthServiceIsSaml: strconv.FormatBool(isSaml),
model.UserAuthServiceIsOAuth: strconv.FormatBool(isOAuthUser),
}}
session.GenerateCSRF()
if deviceID != "" {
a.ch.srv.platform.SetSessionExpireInHours(session, *a.Config().ServiceSettings.SessionLengthMobileInHours)
// A special case where we logout of all other sessions with the same Id
if err := a.RevokeSessionsForDeviceId(user.Id, deviceID, ""); err != nil {
err.StatusCode = http.StatusInternalServerError
return err
}
} else if isMobile {
a.ch.srv.platform.SetSessionExpireInHours(session, *a.Config().ServiceSettings.SessionLengthMobileInHours)
} else if isOAuthUser || isSaml {
a.ch.srv.platform.SetSessionExpireInHours(session, *a.Config().ServiceSettings.SessionLengthSSOInHours)
} else {
a.ch.srv.platform.SetSessionExpireInHours(session, *a.Config().ServiceSettings.SessionLengthWebInHours)
}
ua := uasurfer.Parse(r.UserAgent())
plat := getPlatformName(ua)
os := getOSName(ua)
bname := getBrowserName(ua, r.UserAgent())
bversion := getBrowserVersion(ua, r.UserAgent())
session.AddProp(model.SessionPropPlatform, plat)
session.AddProp(model.SessionPropOs, os)
session.AddProp(model.SessionPropBrowser, fmt.Sprintf("%v/%v", bname, bversion))
if user.IsGuest() {
session.AddProp(model.SessionPropIsGuest, "true")
} else {
session.AddProp(model.SessionPropIsGuest, "false")
}
var err *model.AppError
if session, err = a.CreateSession(session); err != nil {
err.StatusCode = http.StatusInternalServerError
return err
}
w.Header().Set(model.HeaderToken, session.Token)
c.SetSession(session)
if a.Srv().License() != nil && *a.Srv().License().Features.LDAP && a.Ldap() != nil {
userVal := *user
sessionVal := *session
a.Srv().Go(func() {
a.Ldap().UpdateProfilePictureIfNecessary(c, userVal, sessionVal)
})
}
a.Srv().Go(func() {
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.UserHasLoggedIn(pluginContext, user)
return true
}, plugin.UserHasLoggedInID)
})
return nil
}
func (a *App) AttachCloudSessionCookie(c *request.Context, w http.ResponseWriter, r *http.Request) {
secure := false
if GetProtocol(r) == "https" {
secure = true
}
maxAgeSeconds := *a.Config().ServiceSettings.SessionLengthWebInHours * 60 * 60
subpath, _ := utils.GetSubpathFromConfig(a.Config())
expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAgeSeconds), 0)
domain := ""
if siteURL, err := url.Parse(a.GetSiteURL()); err == nil {
domain = siteURL.Hostname()
}
if domain == "" {
return
}
var workspaceName string
if strings.Contains(domain, "localhost") {
workspaceName = "localhost"
} else {
// ensure we have a format for a cloud workspace url i.e. example.cloud.mattermost.com
if len(strings.Split(domain, ".")) != 4 {
return
}
workspaceName = strings.SplitN(domain, ".", 2)[0]
domain = strings.SplitN(domain, ".", 3)[2]
domain = "." + domain
}
cookie := &http.Cookie{
Name: model.SessionCookieCloudUrl,
Value: workspaceName,
Path: subpath,
MaxAge: maxAgeSeconds,
Expires: expiresAt,
Domain: domain,
Secure: secure,
}
http.SetCookie(w, cookie)
}
func (a *App) AttachSessionCookies(c *request.Context, w http.ResponseWriter, r *http.Request) {
secure := false
if GetProtocol(r) == "https" {
secure = true
}
maxAgeSeconds := *a.Config().ServiceSettings.SessionLengthWebInHours * 60 * 60
domain := a.GetCookieDomain()
subpath, _ := utils.GetSubpathFromConfig(a.Config())
expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAgeSeconds), 0)
sessionCookie := &http.Cookie{
Name: model.SessionCookieToken,
Value: c.Session().Token,
Path: subpath,
MaxAge: maxAgeSeconds,
Expires: expiresAt,
HttpOnly: true,
Domain: domain,
Secure: secure,
}
userCookie := &http.Cookie{
Name: model.SessionCookieUser,
Value: c.Session().UserId,
Path: subpath,
MaxAge: maxAgeSeconds,
Expires: expiresAt,
Domain: domain,
Secure: secure,
}
csrfCookie := &http.Cookie{
Name: model.SessionCookieCsrf,
Value: c.Session().GetCSRF(),
Path: subpath,
MaxAge: maxAgeSeconds,
Expires: expiresAt,
Domain: domain,
Secure: secure,
}
http.SetCookie(w, sessionCookie)
http.SetCookie(w, userCookie)
http.SetCookie(w, csrfCookie)
// For context see: https://mattermost.atlassian.net/browse/MM-39583
if a.License().IsCloud() {
a.AttachCloudSessionCookie(c, w, r)
}
}
func GetProtocol(r *http.Request) string {
if r.Header.Get(model.HeaderForwardedProto) == "https" || r.TLS != nil {
return "https"
}
return "http"
}
func IsCWSLogin(a *App, token string) bool {
return a.License().IsCloud() && token != ""
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"fmt"
"reflect"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const EmojisPermissionsMigrationKey = "EmojisPermissionsMigrationComplete"
const GuestRolesCreationMigrationKey = "GuestRolesCreationMigrationComplete"
const SystemConsoleRolesCreationMigrationKey = "SystemConsoleRolesCreationMigrationComplete"
const CustomGroupAdminRoleCreationMigrationKey = "CustomGroupAdminRoleCreationMigrationComplete"
const ContentExtractionConfigDefaultTrueMigrationKey = "ContentExtractionConfigDefaultTrueMigrationComplete"
const PlaybookRolesCreationMigrationKey = "PlaybookRolesCreationMigrationComplete"
const FirstAdminSetupCompleteKey = model.SystemFirstAdminSetupComplete
const remainingSchemaMigrationsKey = "RemainingSchemaMigrations"
const postPriorityConfigDefaultTrueMigrationKey = "PostPriorityConfigDefaultTrueMigrationComplete"
// This function migrates the default built in roles from code/config to the database.
func (a *App) DoAdvancedPermissionsMigration() {
a.Srv().doAdvancedPermissionsMigration()
}
func (s *Server) doAdvancedPermissionsMigration() {
// If the migration is already marked as completed, don't do it again.
if _, err := s.Store().System().GetByName(model.AdvancedPermissionsMigrationKey); err == nil {
return
}
mlog.Info("Migrating roles to database.")
roles := model.MakeDefaultRoles()
allSucceeded := true
for _, role := range roles {
_, err := s.Store().Role().Save(role)
if err == nil {
continue
}
// If this failed for reasons other than the role already existing, don't mark the migration as done.
fetchedRole, err := s.Store().Role().GetByName(context.Background(), role.Name)
if err != nil {
mlog.Fatal("Failed to migrate role to database.", mlog.Err(err))
allSucceeded = false
continue
}
// If the role already existed, check it is the same and update if not.
if !reflect.DeepEqual(fetchedRole.Permissions, role.Permissions) ||
fetchedRole.DisplayName != role.DisplayName ||
fetchedRole.Description != role.Description ||
fetchedRole.SchemeManaged != role.SchemeManaged {
role.Id = fetchedRole.Id
if _, err = s.Store().Role().Save(role); err != nil {
// Role is not the same, but failed to update.
mlog.Fatal("Failed to migrate role to database.", mlog.Err(err))
allSucceeded = false
}
}
}
if !allSucceeded {
return
}
config := s.platform.Config()
*config.ServiceSettings.PostEditTimeLimit = -1
if _, _, err := s.platform.SaveConfig(config, true); err != nil {
mlog.Error("Failed to update config in Advanced Permissions Phase 1 Migration.", mlog.Err(err))
}
system := model.System{
Name: model.AdvancedPermissionsMigrationKey,
Value: "true",
}
if err := s.Store().System().Save(&system); err != nil {
mlog.Fatal("Failed to mark advanced permissions migration as completed.", mlog.Err(err))
}
}
func (a *App) SetPhase2PermissionsMigrationStatus(isComplete bool) error {
if !isComplete {
if _, err := a.Srv().Store().System().PermanentDeleteByName(model.MigrationKeyAdvancedPermissionsPhase2); err != nil {
return err
}
}
a.Srv().phase2PermissionsMigrationComplete = isComplete
return nil
}
func (a *App) DoEmojisPermissionsMigration() {
a.Srv().doEmojisPermissionsMigration()
}
func (s *Server) doEmojisPermissionsMigration() {
// If the migration is already marked as completed, don't do it again.
if _, err := s.Store().System().GetByName(EmojisPermissionsMigrationKey); err == nil {
return
}
var role *model.Role
var systemAdminRole *model.Role
var err *model.AppError
mlog.Info("Migrating emojis config to database.")
// Emoji creation is set to all by default
role, err = s.GetRoleByName(context.Background(), model.SystemUserRoleId)
if err != nil {
mlog.Fatal("Failed to migrate emojis creation permissions from mattermost config.", mlog.Err(err))
return
}
if role != nil {
role.Permissions = append(role.Permissions, model.PermissionCreateEmojis.Id, model.PermissionDeleteEmojis.Id)
if _, nErr := s.Store().Role().Save(role); nErr != nil {
mlog.Fatal("Failed to migrate emojis creation permissions from mattermost config.", mlog.Err(nErr))
return
}
}
systemAdminRole, err = s.GetRoleByName(context.Background(), model.SystemAdminRoleId)
if err != nil {
mlog.Fatal("Failed to migrate emojis creation permissions from mattermost config.", mlog.Err(err))
return
}
systemAdminRole.Permissions = append(systemAdminRole.Permissions,
model.PermissionCreateEmojis.Id,
model.PermissionDeleteEmojis.Id,
model.PermissionDeleteOthersEmojis.Id,
)
if _, err := s.Store().Role().Save(systemAdminRole); err != nil {
mlog.Fatal("Failed to migrate emojis creation permissions from mattermost config.", mlog.Err(err))
return
}
system := model.System{
Name: EmojisPermissionsMigrationKey,
Value: "true",
}
if err := s.Store().System().Save(&system); err != nil {
mlog.Fatal("Failed to mark emojis permissions migration as completed.", mlog.Err(err))
}
}
func (a *App) DoGuestRolesCreationMigration() {
a.Srv().doGuestRolesCreationMigration()
}
func (s *Server) doGuestRolesCreationMigration() {
// If the migration is already marked as completed, don't do it again.
if _, err := s.Store().System().GetByName(GuestRolesCreationMigrationKey); err == nil {
return
}
roles := model.MakeDefaultRoles()
allSucceeded := true
if _, err := s.Store().Role().GetByName(context.Background(), model.ChannelGuestRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.ChannelGuestRoleId]); err != nil {
mlog.Fatal("Failed to create new guest role to database.", mlog.Err(err))
allSucceeded = false
}
}
if _, err := s.Store().Role().GetByName(context.Background(), model.TeamGuestRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.TeamGuestRoleId]); err != nil {
mlog.Fatal("Failed to create new guest role to database.", mlog.Err(err))
allSucceeded = false
}
}
if _, err := s.Store().Role().GetByName(context.Background(), model.SystemGuestRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.SystemGuestRoleId]); err != nil {
mlog.Fatal("Failed to create new guest role to database.", mlog.Err(err))
allSucceeded = false
}
}
schemes, err := s.Store().Scheme().GetAllPage("", 0, 1000000)
if err != nil {
mlog.Fatal("Failed to get all schemes.", mlog.Err(err))
allSucceeded = false
}
for _, scheme := range schemes {
if scheme.DefaultTeamGuestRole == "" || scheme.DefaultChannelGuestRole == "" {
if scheme.Scope == model.SchemeScopeTeam {
// Team Guest Role
teamGuestRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("Team Guest Role for Scheme %s", scheme.Name),
Permissions: roles[model.TeamGuestRoleId].Permissions,
SchemeManaged: true,
}
if savedRole, err := s.Store().Role().Save(teamGuestRole); err != nil {
mlog.Fatal("Failed to create new guest role for custom scheme.", mlog.Err(err))
allSucceeded = false
} else {
scheme.DefaultTeamGuestRole = savedRole.Name
}
}
// Channel Guest Role
channelGuestRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("Channel Guest Role for Scheme %s", scheme.Name),
Permissions: roles[model.ChannelGuestRoleId].Permissions,
SchemeManaged: true,
}
if savedRole, err := s.Store().Role().Save(channelGuestRole); err != nil {
mlog.Fatal("Failed to create new guest role for custom scheme.", mlog.Err(err))
allSucceeded = false
} else {
scheme.DefaultChannelGuestRole = savedRole.Name
}
_, err := s.Store().Scheme().Save(scheme)
if err != nil {
mlog.Fatal("Failed to update custom scheme.", mlog.Err(err))
allSucceeded = false
}
}
}
if !allSucceeded {
return
}
system := model.System{
Name: GuestRolesCreationMigrationKey,
Value: "true",
}
if err := s.Store().System().Save(&system); err != nil {
mlog.Fatal("Failed to mark guest roles creation migration as completed.", mlog.Err(err))
}
}
func (a *App) DoSystemConsoleRolesCreationMigration() {
a.Srv().doSystemConsoleRolesCreationMigration()
}
func (s *Server) doSystemConsoleRolesCreationMigration() {
// If the migration is already marked as completed, don't do it again.
if _, err := s.Store().System().GetByName(SystemConsoleRolesCreationMigrationKey); err == nil {
return
}
roles := model.MakeDefaultRoles()
allSucceeded := true
if _, err := s.Store().Role().GetByName(context.Background(), model.SystemManagerRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.SystemManagerRoleId]); err != nil {
mlog.Fatal("Failed to create new role.", mlog.Err(err), mlog.String("role", model.SystemManagerRoleId))
allSucceeded = false
}
}
if _, err := s.Store().Role().GetByName(context.Background(), model.SystemReadOnlyAdminRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.SystemReadOnlyAdminRoleId]); err != nil {
mlog.Fatal("Failed to create new role.", mlog.Err(err), mlog.String("role", model.SystemReadOnlyAdminRoleId))
allSucceeded = false
}
}
if _, err := s.Store().Role().GetByName(context.Background(), model.SystemUserManagerRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.SystemUserManagerRoleId]); err != nil {
mlog.Fatal("Failed to create new role.", mlog.Err(err), mlog.String("role", model.SystemUserManagerRoleId))
allSucceeded = false
}
}
if !allSucceeded {
return
}
system := model.System{
Name: SystemConsoleRolesCreationMigrationKey,
Value: "true",
}
if err := s.Store().System().Save(&system); err != nil {
mlog.Fatal("Failed to mark system console roles creation migration as completed.", mlog.Err(err))
}
}
func (s *Server) doCustomGroupAdminRoleCreationMigration() {
// If the migration is already marked as completed, don't do it again.
if _, err := s.Store().System().GetByName(CustomGroupAdminRoleCreationMigrationKey); err == nil {
return
}
roles := model.MakeDefaultRoles()
allSucceeded := true
if _, err := s.Store().Role().GetByName(context.Background(), model.SystemCustomGroupAdminRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.SystemCustomGroupAdminRoleId]); err != nil {
mlog.Fatal("Failed to create new role.", mlog.Err(err), mlog.String("role", model.SystemCustomGroupAdminRoleId))
allSucceeded = false
}
}
if !allSucceeded {
return
}
system := model.System{
Name: CustomGroupAdminRoleCreationMigrationKey,
Value: "true",
}
if err := s.Store().System().Save(&system); err != nil {
mlog.Fatal("Failed to mark custom group admin role creation migration as completed.", mlog.Err(err))
}
}
func (s *Server) doContentExtractionConfigDefaultTrueMigration() {
// If the migration is already marked as completed, don't do it again.
if _, err := s.Store().System().GetByName(ContentExtractionConfigDefaultTrueMigrationKey); err == nil {
return
}
s.platform.UpdateConfig(func(config *model.Config) {
config.FileSettings.ExtractContent = model.NewBool(true)
})
system := model.System{
Name: ContentExtractionConfigDefaultTrueMigrationKey,
Value: "true",
}
if err := s.Store().System().Save(&system); err != nil {
mlog.Fatal("Failed to mark content extraction config migration as completed.", mlog.Err(err))
}
}
func (s *Server) doPlaybooksRolesCreationMigration() {
// If the migration is already marked as completed, don't do it again.
if _, err := s.Store().System().GetByName(PlaybookRolesCreationMigrationKey); err == nil {
return
}
roles := model.MakeDefaultRoles()
allSucceeded := true
if _, err := s.Store().Role().GetByName(context.Background(), model.PlaybookAdminRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.PlaybookAdminRoleId]); err != nil {
mlog.Fatal("Failed to create new playbook admin role to database.", mlog.Err(err))
allSucceeded = false
}
}
if _, err := s.Store().Role().GetByName(context.Background(), model.PlaybookMemberRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.PlaybookMemberRoleId]); err != nil {
mlog.Fatal("Failed to create new playbook member role to database.", mlog.Err(err))
allSucceeded = false
}
}
if _, err := s.Store().Role().GetByName(context.Background(), model.RunAdminRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.RunAdminRoleId]); err != nil {
mlog.Fatal("Failed to create new run admin role to database.", mlog.Err(err))
allSucceeded = false
}
}
if _, err := s.Store().Role().GetByName(context.Background(), model.RunMemberRoleId); err != nil {
if _, err := s.Store().Role().Save(roles[model.RunMemberRoleId]); err != nil {
mlog.Fatal("Failed to create new run member role to database.", mlog.Err(err))
allSucceeded = false
}
}
schemes, err := s.Store().Scheme().GetAllPage(model.SchemeScopeTeam, 0, 1000000)
if err != nil {
mlog.Fatal("Failed to get all schemes.", mlog.Err(err))
allSucceeded = false
}
for _, scheme := range schemes {
if scheme.Scope == model.SchemeScopeTeam {
if scheme.DefaultPlaybookAdminRole == "" {
playbookAdminRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("Playbook Admin Role for Scheme %s", scheme.Name),
Permissions: roles[model.PlaybookAdminRoleId].Permissions,
SchemeManaged: true,
}
if savedRole, err := s.Store().Role().Save(playbookAdminRole); err != nil {
mlog.Fatal("Failed to create new playbook admin role for existing custom scheme.", mlog.Err(err))
allSucceeded = false
} else {
scheme.DefaultPlaybookAdminRole = savedRole.Name
}
}
if scheme.DefaultPlaybookMemberRole == "" {
playbookMember := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("Playbook Member Role for Scheme %s", scheme.Name),
Permissions: roles[model.PlaybookMemberRoleId].Permissions,
SchemeManaged: true,
}
if savedRole, err := s.Store().Role().Save(playbookMember); err != nil {
mlog.Fatal("Failed to create new playbook member role for existing custom scheme.", mlog.Err(err))
allSucceeded = false
} else {
scheme.DefaultPlaybookMemberRole = savedRole.Name
}
}
if scheme.DefaultRunAdminRole == "" {
runAdminRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("Run Admin Role for Scheme %s", scheme.Name),
Permissions: roles[model.RunAdminRoleId].Permissions,
SchemeManaged: true,
}
if savedRole, err := s.Store().Role().Save(runAdminRole); err != nil {
mlog.Fatal("Failed to create new run admin role for existing custom scheme.", mlog.Err(err))
allSucceeded = false
} else {
scheme.DefaultRunAdminRole = savedRole.Name
}
}
if scheme.DefaultRunMemberRole == "" {
runMemberRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("Run Member Role for Scheme %s", scheme.Name),
Permissions: roles[model.RunMemberRoleId].Permissions,
SchemeManaged: true,
}
if savedRole, err := s.Store().Role().Save(runMemberRole); err != nil {
mlog.Fatal("Failed to create new run member role for existing custom scheme.", mlog.Err(err))
allSucceeded = false
} else {
scheme.DefaultRunMemberRole = savedRole.Name
}
}
_, err := s.Store().Scheme().Save(scheme)
if err != nil {
mlog.Fatal("Failed to update custom scheme.", mlog.Err(err))
allSucceeded = false
}
}
}
if !allSucceeded {
return
}
system := model.System{
Name: PlaybookRolesCreationMigrationKey,
Value: "true",
}
if err := s.Store().System().Save(&system); err != nil {
mlog.Fatal("Failed to mark playbook roles creation migration as completed.", mlog.Err(err))
}
}
// arbitrary choice, though if there is an longstanding installation with less than 10 messages,
// putting the first admin through onboarding shouldn't be very disruptive.
const existingInstallationPostsThreshold = 10
func (s *Server) doFirstAdminSetupCompleteMigration() {
// Don't run the migration until the flag is turned on.
if !s.platform.Config().FeatureFlags.UseCaseOnboarding {
return
}
// If the migration is already marked as completed, don't do it again.
if _, err := s.Store().System().GetByName(FirstAdminSetupCompleteKey); err == nil {
return
}
teams, err := s.Store().Team().GetAll()
if err != nil {
// can not confirm that admin has started in this case.
return
}
if len(teams) == 0 {
// No teams, and no existing preference. This is most likely a new instance.
// So do not mark that the admin has already done the first time setup.
return
}
// if there are teams, then if this isn't a new installation, there should be posts
postCount, err := s.Store().Post().AnalyticsPostCount(&model.PostCountOptions{})
if err != nil || postCount < existingInstallationPostsThreshold {
return
}
system := model.System{
Name: FirstAdminSetupCompleteKey,
Value: "true",
}
if err := s.Store().System().Save(&system); err != nil {
mlog.Fatal("Failed to mark first admin setup migration as completed.", mlog.Err(err))
}
}
func (s *Server) doRemainingSchemaMigrations() {
// If the migration is already marked as completed, don't do it again.
if _, err := s.Store().System().GetByName(remainingSchemaMigrationsKey); err == nil {
return
}
if teams, err := s.Store().Team().GetByEmptyInviteID(); err != nil {
mlog.Error("Error fetching Teams without InviteID", mlog.Err(err))
} else {
for _, team := range teams {
team.InviteId = model.NewId()
if _, err := s.Store().Team().Update(team); err != nil {
mlog.Error("Error updating Team InviteIDs", mlog.String("team_id", team.Id), mlog.Err(err))
}
}
}
system := model.System{
Name: remainingSchemaMigrationsKey,
Value: "true",
}
if err := s.Store().System().Save(&system); err != nil {
mlog.Fatal("Failed to mark the remaining schema migrations as completed.", mlog.Err(err))
}
}
func (s *Server) doPostPriorityConfigDefaultTrueMigration() {
// If the migration is already marked as completed, don't do it again.
if _, err := s.Store().System().GetByName(postPriorityConfigDefaultTrueMigrationKey); err == nil {
return
}
s.platform.UpdateConfig(func(config *model.Config) {
config.ServiceSettings.PostPriority = model.NewBool(true)
})
system := model.System{
Name: postPriorityConfigDefaultTrueMigrationKey,
Value: "true",
}
if err := s.Store().System().SaveOrUpdate(&system); err != nil {
mlog.Fatal("Failed to mark post priority config migration as completed.", mlog.Err(err))
}
}
func (a *App) DoAppMigrations() {
a.Srv().doAppMigrations()
}
func (s *Server) doAppMigrations() {
s.doAdvancedPermissionsMigration()
s.doEmojisPermissionsMigration()
s.doGuestRolesCreationMigration()
s.doSystemConsoleRolesCreationMigration()
s.doCustomGroupAdminRoleCreationMigration()
// This migration always must be the last, because can be based on previous
// migrations. For example, it needs the guest roles migration.
err := s.doPermissionsMigrations()
if err != nil {
mlog.Fatal("(app.App).DoPermissionsMigrations failed", mlog.Err(err))
}
s.doContentExtractionConfigDefaultTrueMigration()
s.doPlaybooksRolesCreationMigration()
s.doFirstAdminSetupCompleteMigration()
s.doRemainingSchemaMigrations()
s.doPostPriorityConfigDefaultTrueMigration()
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make misc-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
request "github.com/mattermost/mattermost-server/v6/server/channels/app/request"
mock "github.com/stretchr/testify/mock"
worktemplates "github.com/mattermost/mattermost-server/v6/server/channels/app/worktemplates"
)
// WorkTemplateExecutor is an autogenerated mock type for the WorkTemplateExecutor type
type WorkTemplateExecutor struct {
mock.Mock
}
// CreateBoard provides a mock function with given fields: c, wtcr, cBoard, linkToChannelID
func (_m *WorkTemplateExecutor) CreateBoard(c *request.Context, wtcr *worktemplates.ExecutionRequest, cBoard *model.WorkTemplateBoard, linkToChannelID string) (string, error) {
ret := _m.Called(c, wtcr, cBoard, linkToChannelID)
var r0 string
if rf, ok := ret.Get(0).(func(*request.Context, *worktemplates.ExecutionRequest, *model.WorkTemplateBoard, string) string); ok {
r0 = rf(c, wtcr, cBoard, linkToChannelID)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(*request.Context, *worktemplates.ExecutionRequest, *model.WorkTemplateBoard, string) error); ok {
r1 = rf(c, wtcr, cBoard, linkToChannelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateChannel provides a mock function with given fields: c, wtcr, cChannel
func (_m *WorkTemplateExecutor) CreateChannel(c *request.Context, wtcr *worktemplates.ExecutionRequest, cChannel *model.WorkTemplateChannel) (string, error) {
ret := _m.Called(c, wtcr, cChannel)
var r0 string
if rf, ok := ret.Get(0).(func(*request.Context, *worktemplates.ExecutionRequest, *model.WorkTemplateChannel) string); ok {
r0 = rf(c, wtcr, cChannel)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(*request.Context, *worktemplates.ExecutionRequest, *model.WorkTemplateChannel) error); ok {
r1 = rf(c, wtcr, cChannel)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreatePlaybook provides a mock function with given fields: c, wtcr, playbook, channel
func (_m *WorkTemplateExecutor) CreatePlaybook(c *request.Context, wtcr *worktemplates.ExecutionRequest, playbook *model.WorkTemplatePlaybook, channel model.WorkTemplateChannel) (string, error) {
ret := _m.Called(c, wtcr, playbook, channel)
var r0 string
if rf, ok := ret.Get(0).(func(*request.Context, *worktemplates.ExecutionRequest, *model.WorkTemplatePlaybook, model.WorkTemplateChannel) string); ok {
r0 = rf(c, wtcr, playbook, channel)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(*request.Context, *worktemplates.ExecutionRequest, *model.WorkTemplatePlaybook, model.WorkTemplateChannel) error); ok {
r1 = rf(c, wtcr, playbook, channel)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// InstallPlugin provides a mock function with given fields: c, wtcr, cIntegration, sendToChannelID
func (_m *WorkTemplateExecutor) InstallPlugin(c *request.Context, wtcr *worktemplates.ExecutionRequest, cIntegration *model.WorkTemplateIntegration, sendToChannelID string) error {
ret := _m.Called(c, wtcr, cIntegration, sendToChannelID)
var r0 error
if rf, ok := ret.Get(0).(func(*request.Context, *worktemplates.ExecutionRequest, *model.WorkTemplateIntegration, string) error); ok {
r0 = rf(c, wtcr, cIntegration, sendToChannelID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"encoding/json"
"net/http"
"sort"
"strings"
"sync"
"unicode"
"unicode/utf8"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/markdown"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) canSendPushNotifications() bool {
if !*a.Config().EmailSettings.SendPushNotifications {
return false
}
pushServer := *a.Config().EmailSettings.PushNotificationServer
if license := a.Srv().License(); pushServer == model.MHPNS && (license == nil || !*license.Features.MHPNS) {
mlog.Warn("Push notifications have been disabled. Update your license or go to System Console > Environment > Push Notification Server to use a different server")
return false
}
return true
}
func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Team, channel *model.Channel, sender *model.User, parentPostList *model.PostList, setOnline bool) ([]string, error) {
// Do not send notifications in archived channels
if channel.DeleteAt > 0 {
return []string{}, nil
}
isCRTAllowed := *a.Config().ServiceSettings.CollapsedThreads != model.CollapsedThreadsDisabled
pchan := make(chan store.StoreResult, 1)
go func() {
props, err := a.Srv().Store().User().GetAllProfilesInChannel(context.Background(), channel.Id, true)
pchan <- store.StoreResult{Data: props, NErr: err}
close(pchan)
}()
cmnchan := make(chan store.StoreResult, 1)
go func() {
props, err := a.Srv().Store().Channel().GetAllChannelMembersNotifyPropsForChannel(channel.Id, true)
cmnchan <- store.StoreResult{Data: props, NErr: err}
close(cmnchan)
}()
var gchan chan store.StoreResult
if a.allowGroupMentions(c, post) {
gchan = make(chan store.StoreResult, 1)
go func() {
groupsMap, err := a.getGroupsAllowedForReferenceInChannel(channel, team)
gchan <- store.StoreResult{Data: groupsMap, NErr: err}
close(gchan)
}()
}
var fchan chan store.StoreResult
if len(post.FileIds) != 0 {
fchan = make(chan store.StoreResult, 1)
go func() {
fileInfos, err := a.Srv().Store().FileInfo().GetForPost(post.Id, true, false, true)
fchan <- store.StoreResult{Data: fileInfos, NErr: err}
close(fchan)
}()
}
var tchan chan store.StoreResult
if isCRTAllowed && post.RootId != "" {
tchan = make(chan store.StoreResult, 1)
go func() {
followers, err := a.Srv().Store().Thread().GetThreadFollowers(post.RootId, true)
tchan <- store.StoreResult{Data: followers, NErr: err}
close(tchan)
}()
}
result := <-pchan
if result.NErr != nil {
return nil, result.NErr
}
profileMap := result.Data.(map[string]*model.User)
result = <-cmnchan
if result.NErr != nil {
return nil, result.NErr
}
channelMemberNotifyPropsMap := result.Data.(map[string]model.StringMap)
followers := make(model.StringArray, 0)
if tchan != nil {
result = <-tchan
if result.NErr != nil {
return nil, result.NErr
}
followers = result.Data.([]string)
}
groups := make(map[string]*model.Group)
if gchan != nil {
result = <-gchan
if result.NErr != nil {
return nil, result.NErr
}
groups = result.Data.(map[string]*model.Group)
}
mentions := &ExplicitMentions{}
allActivityPushUserIds := []string{}
var allowChannelMentions bool
var keywords map[string][]string
if channel.Type == model.ChannelTypeDirect {
otherUserId := channel.GetOtherUserIdForDM(post.UserId)
_, ok := profileMap[otherUserId]
if ok {
mentions.addMention(otherUserId, DMMention)
}
if post.GetProp("from_webhook") == "true" {
mentions.addMention(post.UserId, DMMention)
}
} else {
allowChannelMentions = a.allowChannelMentions(c, post, len(profileMap))
keywords = a.getMentionKeywordsInChannel(profileMap, allowChannelMentions, channelMemberNotifyPropsMap)
mentions = getExplicitMentions(post, keywords, groups)
// Add an implicit mention when a user is added to a channel
// even if the user has set 'username mentions' to false in account settings.
if post.Type == model.PostTypeAddToChannel {
addedUserId, ok := post.GetProp(model.PostPropsAddedUserId).(string)
if ok {
mentions.addMention(addedUserId, KeywordMention)
}
}
// Iterate through all groups that were mentioned and insert group members into the list of mentions or potential mentions
for _, group := range mentions.GroupMentions {
anyUsersMentionedByGroup, err := a.insertGroupMentions(group, channel, profileMap, mentions)
if err != nil {
return nil, err
}
if !anyUsersMentionedByGroup {
a.sendNoUsersNotifiedByGroupInChannel(c, sender, post, channel, group)
}
}
// get users that have comment thread mentions enabled
if post.RootId != "" && parentPostList != nil {
for _, threadPost := range parentPostList.Posts {
profile := profileMap[threadPost.UserId]
if profile == nil {
continue
}
// If this is the root post and it was posted by an OAuth bot, don't notify the user
if threadPost.Id == parentPostList.Order[0] && threadPost.IsFromOAuthBot() {
continue
}
if a.IsCRTEnabledForUser(c, profile.Id) {
continue
}
if profile.NotifyProps[model.CommentsNotifyProp] == model.CommentsNotifyAny || (profile.NotifyProps[model.CommentsNotifyProp] == model.CommentsNotifyRoot && threadPost.Id == parentPostList.Order[0]) {
mentionType := ThreadMention
if threadPost.Id == parentPostList.Order[0] {
mentionType = CommentMention
}
mentions.addMention(threadPost.UserId, mentionType)
}
}
}
// prevent the user from mentioning themselves
if post.GetProp("from_webhook") != "true" {
mentions.removeMention(post.UserId)
}
go func() {
_, err := a.sendOutOfChannelMentions(c, sender, post, channel, mentions.OtherPotentialMentions)
if err != nil {
mlog.Error("Failed to send warning for out of channel mentions", mlog.String("user_id", sender.Id), mlog.String("post_id", post.Id), mlog.Err(err))
}
}()
// find which users in the channel are set up to always receive mobile notifications
// excludes CRT users since those should be added in notificationsForCRT
for _, profile := range profileMap {
if (profile.NotifyProps[model.PushNotifyProp] == model.UserNotifyAll ||
channelMemberNotifyPropsMap[profile.Id][model.PushNotifyProp] == model.ChannelNotifyAll) &&
(post.UserId != profile.Id || post.GetProp("from_webhook") == "true") &&
!post.IsSystemMessage() &&
!(a.IsCRTEnabledForUser(c, profile.Id) && post.RootId != "") {
allActivityPushUserIds = append(allActivityPushUserIds, profile.Id)
}
}
}
mentionedUsersList := make(model.StringArray, 0, len(mentions.Mentions))
mentionAutofollowChans := []chan *model.AppError{}
threadParticipants := map[string]bool{post.UserId: true}
newParticipants := map[string]bool{}
participantMemberships := map[string]*model.ThreadMembership{}
membershipsMutex := &sync.Mutex{}
followersMutex := &sync.Mutex{}
if *a.Config().ServiceSettings.ThreadAutoFollow && post.RootId != "" {
var rootMentions *ExplicitMentions
if parentPostList != nil {
rootPost := parentPostList.Posts[parentPostList.Order[0]]
if rootPost.GetProp("from_webhook") != "true" {
threadParticipants[rootPost.UserId] = true
}
if channel.Type != model.ChannelTypeDirect {
rootMentions = getExplicitMentions(rootPost, keywords, groups)
for id := range rootMentions.Mentions {
threadParticipants[id] = true
}
}
}
for id := range mentions.Mentions {
threadParticipants[id] = true
}
// sema is a counting semaphore to throttle the number of concurrent DB requests.
// A concurrency of 8 should be sufficient.
// We don't want to set a higher limit which can bring down the DB.
sema := make(chan struct{}, 8)
// for each mention, make sure to update thread autofollow (if enabled) and update increment mention count
for id := range threadParticipants {
mac := make(chan *model.AppError, 1)
// Get token.
sema <- struct{}{}
go func(userID string) {
defer func() {
close(mac)
// Release token.
<-sema
}()
mentionType, incrementMentions := mentions.Mentions[userID]
// if the user was not explicitly mentioned, check if they explicitly unfollowed the thread
if !incrementMentions {
membership, err := a.Srv().Store().Thread().GetMembershipForUser(userID, post.RootId)
var nfErr *store.ErrNotFound
if err != nil && !errors.As(err, &nfErr) {
mac <- model.NewAppError("SendNotifications", "app.channel.autofollow.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if membership != nil && !membership.Following {
return
}
}
updateFollowing := *a.Config().ServiceSettings.ThreadAutoFollow
if mentionType == ThreadMention || mentionType == CommentMention {
incrementMentions = false
updateFollowing = false
}
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: incrementMentions,
UpdateFollowing: updateFollowing,
UpdateViewedTimestamp: false,
UpdateParticipants: userID == post.UserId,
}
threadMembership, err := a.Srv().Store().Thread().MaintainMembership(userID, post.RootId, opts)
if err != nil {
mac <- model.NewAppError("SendNotifications", "app.channel.autofollow.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
followersMutex.Lock()
// add new followers to existing followers
if threadMembership.Following && !followers.Contains(userID) {
followers = append(followers, userID)
newParticipants[userID] = true
}
followersMutex.Unlock()
membershipsMutex.Lock()
participantMemberships[userID] = threadMembership
membershipsMutex.Unlock()
mac <- nil
}(id)
mentionAutofollowChans = append(mentionAutofollowChans, mac)
}
}
for id := range mentions.Mentions {
mentionedUsersList = append(mentionedUsersList, id)
}
nErr := a.Srv().Store().Channel().IncrementMentionCount(post.ChannelId, mentionedUsersList, post.RootId == "", post.IsUrgent())
if nErr != nil {
mlog.Warn(
"Failed to update mention count",
mlog.String("post_id", post.Id),
mlog.String("channel_id", post.ChannelId),
mlog.Err(nErr),
)
}
// Log the problems that might have occurred while auto following the thread
for _, mac := range mentionAutofollowChans {
if err := <-mac; err != nil {
mlog.Warn(
"Failed to update thread autofollow from mention",
mlog.String("post_id", post.Id),
mlog.String("channel_id", post.ChannelId),
mlog.Err(err),
)
}
}
notificationsForCRT := &CRTNotifiers{}
if isCRTAllowed && post.RootId != "" {
for _, uid := range followers {
profile := profileMap[uid]
if profile == nil || !a.IsCRTEnabledForUser(c, uid) {
continue
}
if post.GetProp("from_webhook") != "true" && uid == post.UserId {
continue
}
// add user id to notificationsForCRT depending on threads notify props
notificationsForCRT.addFollowerToNotify(profile, mentions, channelMemberNotifyPropsMap[profile.Id], channel)
}
}
notification := &PostNotification{
Post: post.Clone(),
Channel: channel,
ProfileMap: profileMap,
Sender: sender,
}
if *a.Config().EmailSettings.SendEmailNotifications {
emailRecipients := append(mentionedUsersList, notificationsForCRT.Email...)
emailRecipients = model.RemoveDuplicateStrings(emailRecipients)
for _, id := range emailRecipients {
if profileMap[id] == nil {
continue
}
//If email verification is required and user email is not verified don't send email.
if *a.Config().EmailSettings.RequireEmailVerification && !profileMap[id].EmailVerified {
mlog.Debug("Skipped sending notification email, address not verified.", mlog.String("user_email", profileMap[id].Email), mlog.String("user_id", id))
continue
}
if a.userAllowsEmail(c, profileMap[id], channelMemberNotifyPropsMap[id], post) {
senderProfileImage, _, err := a.GetProfileImage(sender)
if err != nil {
a.Log().Warn("Unable to get the sender user profile image.", mlog.String("user_id", sender.Id), mlog.Err(err))
}
if err := a.sendNotificationEmail(c, notification, profileMap[id], team, senderProfileImage); err != nil {
mlog.Warn("Unable to send notification email.", mlog.Err(err))
}
}
}
}
// Check for channel-wide mentions in channels that have too many members for those to work
if int64(len(profileMap)) > *a.Config().TeamSettings.MaxNotificationsPerChannel {
T := i18n.GetUserTranslations(sender.Locale)
if mentions.HereMentioned {
a.SendEphemeralPost(
c,
post.UserId,
&model.Post{
ChannelId: post.ChannelId,
Message: T("api.post.disabled_here", map[string]any{"Users": *a.Config().TeamSettings.MaxNotificationsPerChannel}),
CreateAt: post.CreateAt + 1,
},
)
}
if mentions.ChannelMentioned {
a.SendEphemeralPost(
c,
post.UserId,
&model.Post{
ChannelId: post.ChannelId,
Message: T("api.post.disabled_channel", map[string]any{"Users": *a.Config().TeamSettings.MaxNotificationsPerChannel}),
CreateAt: post.CreateAt + 1,
},
)
}
if mentions.AllMentioned {
a.SendEphemeralPost(
c,
post.UserId,
&model.Post{
ChannelId: post.ChannelId,
Message: T("api.post.disabled_all", map[string]any{"Users": *a.Config().TeamSettings.MaxNotificationsPerChannel}),
CreateAt: post.CreateAt + 1,
},
)
}
}
if a.canSendPushNotifications() {
for _, id := range mentionedUsersList {
if profileMap[id] == nil || notificationsForCRT.Push.Contains(id) {
continue
}
var status *model.Status
var err *model.AppError
if status, err = a.GetStatus(id); err != nil {
status = &model.Status{UserId: id, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
}
if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], true, status, post) {
mentionType := mentions.Mentions[id]
replyToThreadType := ""
if mentionType == ThreadMention {
replyToThreadType = model.CommentsNotifyAny
} else if mentionType == CommentMention {
replyToThreadType = model.CommentsNotifyRoot
}
a.sendPushNotification(
notification,
profileMap[id],
mentionType == KeywordMention || mentionType == ChannelMention || mentionType == DMMention,
mentionType == ChannelMention,
replyToThreadType,
)
} else {
// register that a notification was not sent
a.NotificationsLog().Debug("Notification not sent",
mlog.String("ackId", ""),
mlog.String("type", model.PushTypeMessage),
mlog.String("userId", id),
mlog.String("postId", post.Id),
mlog.String("status", model.PushNotSent),
)
}
}
for _, id := range allActivityPushUserIds {
if profileMap[id] == nil || notificationsForCRT.Push.Contains(id) {
continue
}
if _, ok := mentions.Mentions[id]; !ok {
var status *model.Status
var err *model.AppError
if status, err = a.GetStatus(id); err != nil {
status = &model.Status{UserId: id, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
}
if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], false, status, post) {
a.sendPushNotification(
notification,
profileMap[id],
false,
false,
"",
)
} else {
// register that a notification was not sent
a.NotificationsLog().Debug("Notification not sent",
mlog.String("ackId", ""),
mlog.String("type", model.PushTypeMessage),
mlog.String("userId", id),
mlog.String("postId", post.Id),
mlog.String("status", model.PushNotSent),
)
}
}
}
for _, id := range notificationsForCRT.Push {
if profileMap[id] == nil {
continue
}
var status *model.Status
var err *model.AppError
if status, err = a.GetStatus(id); err != nil {
status = &model.Status{UserId: id, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
}
if DoesStatusAllowPushNotification(profileMap[id].NotifyProps, status, post.ChannelId) {
a.sendPushNotification(
notification,
profileMap[id],
false,
false,
model.CommentsNotifyCRT,
)
} else {
// register that a notification was not sent
a.NotificationsLog().Debug("Notification not sent",
mlog.String("ackId", ""),
mlog.String("type", model.PushTypeMessage),
mlog.String("userId", id),
mlog.String("postId", post.Id),
mlog.String("status", model.PushNotSent),
)
}
}
}
message := model.NewWebSocketEvent(model.WebsocketEventPosted, "", post.ChannelId, "", nil, "")
// Note that PreparePostForClient should've already been called by this point
postJSON, jsonErr := post.ToJSON()
if jsonErr != nil {
return nil, errors.Wrapf(jsonErr, "failed to encode post to JSON")
}
message.Add("post", postJSON)
message.Add("channel_type", channel.Type)
message.Add("channel_display_name", notification.GetChannelName(model.ShowUsername, ""))
message.Add("channel_name", channel.Name)
message.Add("sender_name", notification.GetSenderName(model.ShowUsername, *a.Config().ServiceSettings.EnablePostUsernameOverride))
message.Add("team_id", team.Id)
message.Add("set_online", setOnline)
if len(post.FileIds) != 0 && fchan != nil {
message.Add("otherFile", "true")
var infos []*model.FileInfo
if result := <-fchan; result.NErr != nil {
mlog.Warn("Unable to get fileInfo for push notifications.", mlog.String("post_id", post.Id), mlog.Err(result.NErr))
} else {
infos = result.Data.([]*model.FileInfo)
}
for _, info := range infos {
if info.IsImage() {
message.Add("image", "true")
break
}
}
}
if len(mentionedUsersList) != 0 {
message.Add("mentions", model.ArrayToJSON(mentionedUsersList))
}
if len(notificationsForCRT.Desktop) != 0 {
message.Add("followers", model.ArrayToJSON(notificationsForCRT.Desktop))
}
published, err := a.publishWebsocketEventForPermalinkPost(c, post, message)
if err != nil {
return nil, err
}
if !published {
a.Publish(message)
}
// If this is a reply in a thread, notify participants
if isCRTAllowed && post.RootId != "" {
for _, uid := range followers {
// A user following a thread but had left the channel won't get a notification
// https://mattermost.atlassian.net/browse/MM-36769
if profileMap[uid] == nil {
continue
}
if a.IsCRTEnabledForUser(c, uid) {
message := model.NewWebSocketEvent(model.WebsocketEventThreadUpdated, team.Id, "", uid, nil, "")
threadMembership := participantMemberships[uid]
if threadMembership == nil {
tm, err := a.Srv().Store().Thread().GetMembershipForUser(uid, post.RootId)
if err != nil {
return nil, errors.Wrapf(err, "Missing thread membership for participant in notifications. user_id=%q thread_id=%q", uid, post.RootId)
}
if tm == nil {
continue
}
threadMembership = tm
}
userThread, err := a.Srv().Store().Thread().GetThreadForUser(threadMembership, true, a.isPostPriorityEnabled())
if err != nil {
return nil, errors.Wrapf(err, "cannot get thread %q for user %q", post.RootId, uid)
}
if userThread != nil {
previousUnreadMentions := int64(0)
previousUnreadReplies := int64(0)
// if it's not a newly followed thread, calculate previous unread values.
if !newParticipants[uid] {
previousUnreadMentions = userThread.UnreadMentions
previousUnreadReplies = max(userThread.UnreadReplies-1, 0)
if mentions.isUserMentioned(uid) {
previousUnreadMentions = max(userThread.UnreadMentions-1, 0)
}
}
// set LastViewed to now for commenter
if uid == post.UserId {
opts := store.ThreadMembershipOpts{
UpdateViewedTimestamp: true,
}
// should set unread mentions, and unread replies to 0
_, err = a.Srv().Store().Thread().MaintainMembership(uid, post.RootId, opts)
if err != nil {
return nil, errors.Wrapf(err, "cannot maintain thread membership %q for user %q", post.RootId, uid)
}
userThread.UnreadMentions = 0
userThread.UnreadReplies = 0
}
a.sanitizeProfiles(userThread.Participants, false)
userThread.Post.SanitizeProps()
sanitizedPost, err := a.SanitizePostMetadataForUser(c, userThread.Post, uid)
if err != nil {
return nil, err
}
userThread.Post = sanitizedPost
payload, jsonErr := json.Marshal(userThread)
if jsonErr != nil {
mlog.Warn("Failed to encode thread to JSON")
}
message.Add("thread", string(payload))
message.Add("previous_unread_mentions", previousUnreadMentions)
message.Add("previous_unread_replies", previousUnreadReplies)
a.Publish(message)
}
}
}
}
return mentionedUsersList, nil
}
func max(a, b int64) int64 {
if a < b {
return b
}
return a
}
func (a *App) userAllowsEmail(c request.CTX, user *model.User, channelMemberNotificationProps model.StringMap, post *model.Post) bool {
// if user is a bot account, then we do not send email
if user.IsBot {
return false
}
userAllowsEmails := user.NotifyProps[model.EmailNotifyProp] != "false"
// if CRT is ON for user and the post is a reply disregard the channelEmail setting
if channelEmail, ok := channelMemberNotificationProps[model.EmailNotifyProp]; ok && !(a.IsCRTEnabledForUser(c, user.Id) && post.RootId != "") {
if channelEmail != model.ChannelNotifyDefault {
userAllowsEmails = channelEmail != "false"
}
}
// Remove the user as recipient when the user has muted the channel.
if channelMuted, ok := channelMemberNotificationProps[model.MarkUnreadNotifyProp]; ok {
if channelMuted == model.ChannelMarkUnreadMention {
mlog.Debug("Channel muted for user", mlog.String("user_id", user.Id), mlog.String("channel_mute", channelMuted))
userAllowsEmails = false
}
}
var status *model.Status
var err *model.AppError
if status, err = a.GetStatus(user.Id); err != nil {
status = &model.Status{
UserId: user.Id,
Status: model.StatusOffline,
Manual: false,
LastActivityAt: 0,
ActiveChannel: "",
}
}
autoResponderRelated := status.Status == model.StatusOutOfOffice || post.Type == model.PostTypeAutoResponder
emailNotificationsAllowedForStatus := status.Status != model.StatusOnline && status.Status != model.StatusDnd
return userAllowsEmails && emailNotificationsAllowedForStatus && user.DeleteAt == 0 && !autoResponderRelated
}
func (a *App) sendNoUsersNotifiedByGroupInChannel(c request.CTX, sender *model.User, post *model.Post, channel *model.Channel, group *model.Group) {
T := i18n.GetUserTranslations(sender.Locale)
ephemeralPost := &model.Post{
UserId: sender.Id,
RootId: post.RootId,
ChannelId: channel.Id,
Message: T("api.post.check_for_out_of_channel_group_users.message.none", model.StringInterface{"GroupName": group.Name}),
}
a.SendEphemeralPost(c, post.UserId, ephemeralPost)
}
// sendOutOfChannelMentions sends an ephemeral post to the sender of a post if any of the given potential mentions
// are outside of the post's channel. Returns whether or not an ephemeral post was sent.
func (a *App) sendOutOfChannelMentions(c request.CTX, sender *model.User, post *model.Post, channel *model.Channel, potentialMentions []string) (bool, error) {
outOfChannelUsers, outOfGroupsUsers, err := a.filterOutOfChannelMentions(sender, post, channel, potentialMentions)
if err != nil {
return false, err
}
if len(outOfChannelUsers) == 0 && len(outOfGroupsUsers) == 0 {
return false, nil
}
a.SendEphemeralPost(c, post.UserId, makeOutOfChannelMentionPost(sender, post, outOfChannelUsers, outOfGroupsUsers))
return true, nil
}
func (a *App) FilterUsersByVisible(viewer *model.User, otherUsers []*model.User) ([]*model.User, *model.AppError) {
result := []*model.User{}
for _, user := range otherUsers {
canSee, err := a.UserCanSeeOtherUser(viewer.Id, user.Id)
if err != nil {
return nil, err
}
if canSee {
result = append(result, user)
}
}
return result, nil
}
func (a *App) filterOutOfChannelMentions(sender *model.User, post *model.Post, channel *model.Channel, potentialMentions []string) ([]*model.User, []*model.User, error) {
if post.IsSystemMessage() {
return nil, nil, nil
}
if channel.TeamId == "" || channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
return nil, nil, nil
}
if len(potentialMentions) == 0 {
return nil, nil, nil
}
users, err := a.Srv().Store().User().GetProfilesByUsernames(potentialMentions, &model.ViewUsersRestrictions{Teams: []string{channel.TeamId}})
if err != nil {
return nil, nil, err
}
// Filter out inactive users and bots
allUsers := model.UserSlice(users).FilterByActive(true)
allUsers = allUsers.FilterWithoutBots()
allUsers, appErr := a.FilterUsersByVisible(sender, allUsers)
if appErr != nil {
return nil, nil, appErr
}
if len(allUsers) == 0 {
return nil, nil, nil
}
// Differentiate between users who can and can't be added to the channel
var outOfChannelUsers model.UserSlice
var outOfGroupsUsers model.UserSlice
if channel.IsGroupConstrained() {
nonMemberIDs, err := a.FilterNonGroupChannelMembers(allUsers.IDs(), channel)
if err != nil {
return nil, nil, err
}
outOfChannelUsers = allUsers.FilterWithoutID(nonMemberIDs)
outOfGroupsUsers = allUsers.FilterByID(nonMemberIDs)
} else {
outOfChannelUsers = allUsers
}
return outOfChannelUsers, outOfGroupsUsers, nil
}
func makeOutOfChannelMentionPost(sender *model.User, post *model.Post, outOfChannelUsers, outOfGroupsUsers []*model.User) *model.Post {
allUsers := model.UserSlice(append(outOfChannelUsers, outOfGroupsUsers...))
ocUsers := model.UserSlice(outOfChannelUsers)
ocUsernames := ocUsers.Usernames()
ocUserIDs := ocUsers.IDs()
ogUsers := model.UserSlice(outOfGroupsUsers)
ogUsernames := ogUsers.Usernames()
T := i18n.GetUserTranslations(sender.Locale)
ephemeralPostId := model.NewId()
var message string
if len(outOfChannelUsers) == 1 {
message = T("api.post.check_for_out_of_channel_mentions.message.one", map[string]any{
"Username": ocUsernames[0],
})
} else if len(outOfChannelUsers) > 1 {
preliminary, final := splitAtFinal(ocUsernames)
message = T("api.post.check_for_out_of_channel_mentions.message.multiple", map[string]any{
"Usernames": strings.Join(preliminary, ", @"),
"LastUsername": final,
})
}
if len(outOfGroupsUsers) == 1 {
if message != "" {
message += "\n"
}
message += T("api.post.check_for_out_of_channel_groups_mentions.message.one", map[string]any{
"Username": ogUsernames[0],
})
} else if len(outOfGroupsUsers) > 1 {
preliminary, final := splitAtFinal(ogUsernames)
if message != "" {
message += "\n"
}
message += T("api.post.check_for_out_of_channel_groups_mentions.message.multiple", map[string]any{
"Usernames": strings.Join(preliminary, ", @"),
"LastUsername": final,
})
}
props := model.StringInterface{
model.PropsAddChannelMember: model.StringInterface{
"post_id": ephemeralPostId,
"usernames": allUsers.Usernames(), // Kept for backwards compatibility of mobile app.
"not_in_channel_usernames": ocUsernames,
"user_ids": allUsers.IDs(), // Kept for backwards compatibility of mobile app.
"not_in_channel_user_ids": ocUserIDs,
"not_in_groups_usernames": ogUsernames,
"not_in_groups_user_ids": ogUsers.IDs(),
},
}
return &model.Post{
Id: ephemeralPostId,
RootId: post.RootId,
ChannelId: post.ChannelId,
Message: message,
CreateAt: post.CreateAt + 1,
Props: props,
}
}
func splitAtFinal(items []string) (preliminary []string, final string) {
if len(items) == 0 {
return
}
preliminary = items[:len(items)-1]
final = items[len(items)-1]
return
}
type ExplicitMentions struct {
// Mentions contains the ID of each user that was mentioned and how they were mentioned.
Mentions map[string]MentionType
// Contains a map of groups that were mentioned
GroupMentions map[string]*model.Group
// OtherPotentialMentions contains a list of strings that looked like mentions, but didn't have
// a corresponding keyword.
OtherPotentialMentions []string
// HereMentioned is true if the message contained @here.
HereMentioned bool
// AllMentioned is true if the message contained @all.
AllMentioned bool
// ChannelMentioned is true if the message contained @channel.
ChannelMentioned bool
}
type MentionType int
const (
// Different types of mentions ordered by their priority from lowest to highest
// A placeholder that should never be used in practice
NoMention MentionType = iota
// The post is in a thread that the user has commented on
ThreadMention
// The post is a comment on a thread started by the user
CommentMention
// The post contains an at-channel, at-all, or at-here
ChannelMention
// The post is a DM
DMMention
// The post contains an at-mention for the user
KeywordMention
// The post contains a group mention for the user
GroupMention
)
func (m *ExplicitMentions) isUserMentioned(userID string) bool {
if _, ok := m.Mentions[userID]; ok {
return true
}
if _, ok := m.GroupMentions[userID]; ok {
return true
}
return m.HereMentioned || m.AllMentioned || m.ChannelMentioned
}
func (m *ExplicitMentions) addMention(userID string, mentionType MentionType) {
if m.Mentions == nil {
m.Mentions = make(map[string]MentionType)
}
if currentType, ok := m.Mentions[userID]; ok && currentType >= mentionType {
return
}
m.Mentions[userID] = mentionType
}
func (m *ExplicitMentions) addGroupMention(word string, groups map[string]*model.Group) bool {
if strings.HasPrefix(word, "@") {
word = word[1:]
} else {
// Only allow group mentions when mentioned directly with @group-name
return false
}
group, groupFound := groups[word]
if !groupFound {
group = groups[strings.ToLower(word)]
}
if group == nil {
return false
}
if m.GroupMentions == nil {
m.GroupMentions = make(map[string]*model.Group)
}
if group.Name != nil {
m.GroupMentions[*group.Name] = group
}
return true
}
func (m *ExplicitMentions) addMentions(userIDs []string, mentionType MentionType) {
for _, userID := range userIDs {
m.addMention(userID, mentionType)
}
}
func (m *ExplicitMentions) removeMention(userID string) {
delete(m.Mentions, userID)
}
// Given a message and a map mapping mention keywords to the users who use them, returns a map of mentioned
// users and a slice of potential mention users not in the channel and whether or not @here was mentioned.
func getExplicitMentions(post *model.Post, keywords map[string][]string, groups map[string]*model.Group) *ExplicitMentions {
ret := &ExplicitMentions{}
buf := ""
mentionsEnabledFields := getMentionsEnabledFields(post)
for _, message := range mentionsEnabledFields {
markdown.Inspect(message, func(node any) bool {
text, ok := node.(*markdown.Text)
if !ok {
ret.processText(buf, keywords, groups)
buf = ""
return true
}
buf += text.Text
return false
})
}
ret.processText(buf, keywords, groups)
return ret
}
// Given a post returns the values of the fields in which mentions are possible.
// post.message, preText and text in the attachment are enabled.
func getMentionsEnabledFields(post *model.Post) model.StringArray {
ret := []string{}
ret = append(ret, post.Message)
for _, attachment := range post.Attachments() {
if attachment.Pretext != "" {
ret = append(ret, attachment.Pretext)
}
if attachment.Text != "" {
ret = append(ret, attachment.Text)
}
}
return ret
}
// allowChannelMentions returns whether or not the channel mentions are allowed for the given post.
func (a *App) allowChannelMentions(c request.CTX, post *model.Post, numProfiles int) bool {
if !a.HasPermissionToChannel(c, post.UserId, post.ChannelId, model.PermissionUseChannelMentions) {
return false
}
if post.Type == model.PostTypeHeaderChange || post.Type == model.PostTypePurposeChange {
return false
}
if int64(numProfiles) >= *a.Config().TeamSettings.MaxNotificationsPerChannel {
return false
}
return true
}
// allowGroupMentions returns whether or not the group mentions are allowed for the given post.
func (a *App) allowGroupMentions(c request.CTX, post *model.Post) bool {
if license := a.Srv().License(); license == nil || (license.SkuShortName != model.LicenseShortSkuProfessional && license.SkuShortName != model.LicenseShortSkuEnterprise) {
return false
}
if !a.HasPermissionToChannel(c, post.UserId, post.ChannelId, model.PermissionUseGroupMentions) {
return false
}
if post.Type == model.PostTypeHeaderChange || post.Type == model.PostTypePurposeChange {
return false
}
return true
}
// getGroupsAllowedForReferenceInChannel returns a map of groups allowed for reference in a given channel and team.
func (a *App) getGroupsAllowedForReferenceInChannel(channel *model.Channel, team *model.Team) (map[string]*model.Group, error) {
var err error
groupsMap := make(map[string]*model.Group)
opts := model.GroupSearchOpts{FilterAllowReference: true, IncludeMemberCount: true}
if channel.IsGroupConstrained() || (team != nil && team.IsGroupConstrained()) {
var groups []*model.GroupWithSchemeAdmin
if channel.IsGroupConstrained() {
groups, err = a.Srv().Store().Group().GetGroupsByChannel(channel.Id, opts)
} else {
groups, err = a.Srv().Store().Group().GetGroupsByTeam(team.Id, opts)
}
if err != nil {
return nil, errors.Wrap(err, "unable to get groups")
}
for _, group := range groups {
if group.Group.Name != nil {
groupsMap[*group.Group.Name] = &group.Group
}
}
return groupsMap, nil
}
groups, err := a.Srv().Store().Group().GetGroups(0, 0, opts, nil)
if err != nil {
return nil, errors.Wrap(err, "unable to get groups")
}
for _, group := range groups {
if group.Name != nil {
groupsMap[*group.Name] = group
}
}
return groupsMap, nil
}
// Given a map of user IDs to profiles, returns a list of mention
// keywords for all users in the channel.
func (a *App) getMentionKeywordsInChannel(profiles map[string]*model.User, allowChannelMentions bool, channelMemberNotifyPropsMap map[string]model.StringMap) map[string][]string {
keywords := make(map[string][]string)
for _, profile := range profiles {
addMentionKeywordsForUser(
keywords,
profile,
channelMemberNotifyPropsMap[profile.Id],
a.GetStatusFromCache(profile.Id),
allowChannelMentions,
)
}
return keywords
}
// insertGroupMentions adds group members in the channel to Mentions, adds group members not in the channel to OtherPotentialMentions
// returns false if no group members present in the team that the channel belongs to
func (a *App) insertGroupMentions(group *model.Group, channel *model.Channel, profileMap map[string]*model.User, mentions *ExplicitMentions) (bool, *model.AppError) {
var err error
var groupMembers []*model.User
outOfChannelGroupMembers := []*model.User{}
isGroupOrDirect := channel.IsGroupOrDirect()
if isGroupOrDirect {
groupMembers, err = a.Srv().Store().Group().GetMemberUsers(group.Id)
} else {
groupMembers, err = a.Srv().Store().Group().GetMemberUsersInTeam(group.Id, channel.TeamId)
}
if err != nil {
return false, model.NewAppError("insertGroupMentions", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if mentions.Mentions == nil {
mentions.Mentions = make(map[string]MentionType)
}
for _, member := range groupMembers {
if _, ok := profileMap[member.Id]; ok {
mentions.Mentions[member.Id] = GroupMention
} else {
outOfChannelGroupMembers = append(outOfChannelGroupMembers, member)
}
}
potentialGroupMembersMentioned := []string{}
for _, user := range outOfChannelGroupMembers {
potentialGroupMembersMentioned = append(potentialGroupMembersMentioned, user.Username)
}
if mentions.OtherPotentialMentions == nil {
mentions.OtherPotentialMentions = potentialGroupMembersMentioned
} else {
mentions.OtherPotentialMentions = append(mentions.OtherPotentialMentions, potentialGroupMembersMentioned...)
}
return isGroupOrDirect || len(groupMembers) > 0, nil
}
// addMentionKeywordsForUser adds the mention keywords for a given user to the given keyword map. Returns the provided keyword map.
func addMentionKeywordsForUser(keywords map[string][]string, profile *model.User, channelNotifyProps map[string]string, status *model.Status, allowChannelMentions bool) map[string][]string {
userMention := "@" + strings.ToLower(profile.Username)
keywords[userMention] = append(keywords[userMention], profile.Id)
// Add all the user's mention keys
for _, k := range profile.GetMentionKeys() {
// note that these are made lower case so that we can do a case insensitive check for them
key := strings.ToLower(k)
if key != "" {
keywords[key] = append(keywords[key], profile.Id)
}
}
// If turned on, add the user's case sensitive first name
if profile.NotifyProps[model.FirstNameNotifyProp] == "true" && profile.FirstName != "" {
keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id)
}
// Add @channel and @all to keywords if user has them turned on and the server allows them
if allowChannelMentions {
// Ignore channel mentions if channel is muted and channel mention setting is default
ignoreChannelMentions := channelNotifyProps[model.IgnoreChannelMentionsNotifyProp] == model.IgnoreChannelMentionsOn || (channelNotifyProps[model.MarkUnreadNotifyProp] == model.UserNotifyMention && channelNotifyProps[model.IgnoreChannelMentionsNotifyProp] == model.IgnoreChannelMentionsDefault)
if profile.NotifyProps[model.ChannelMentionsNotifyProp] == "true" && !ignoreChannelMentions {
keywords["@channel"] = append(keywords["@channel"], profile.Id)
keywords["@all"] = append(keywords["@all"], profile.Id)
if status != nil && status.Status == model.StatusOnline {
keywords["@here"] = append(keywords["@here"], profile.Id)
}
}
}
return keywords
}
// Represents either an email or push notification and contains the fields required to send it to any user.
type PostNotification struct {
Channel *model.Channel
Post *model.Post
ProfileMap map[string]*model.User
Sender *model.User
}
// Returns the name of the channel for this notification. For direct messages, this is the sender's name
// preceded by an at sign. For group messages, this is a comma-separated list of the members of the
// channel, with an option to exclude the recipient of the message from that list.
func (n *PostNotification) GetChannelName(userNameFormat, excludeId string) string {
switch n.Channel.Type {
case model.ChannelTypeDirect:
return n.Sender.GetDisplayNameWithPrefix(userNameFormat, "@")
case model.ChannelTypeGroup:
names := []string{}
for _, user := range n.ProfileMap {
if user.Id != excludeId {
names = append(names, user.GetDisplayName(userNameFormat))
}
}
sort.Strings(names)
return strings.Join(names, ", ")
default:
return n.Channel.DisplayName
}
}
// Returns the name of the sender of this notification, accounting for things like system messages
// and whether or not the username has been overridden by an integration.
func (n *PostNotification) GetSenderName(userNameFormat string, overridesAllowed bool) string {
if n.Post.IsSystemMessage() {
return i18n.T("system.message.name")
}
if overridesAllowed && n.Channel.Type != model.ChannelTypeDirect {
if value := n.Post.GetProps()["override_username"]; value != nil && n.Post.GetProp("from_webhook") == "true" {
if s, ok := value.(string); ok {
return s
}
}
}
return n.Sender.GetDisplayNameWithPrefix(userNameFormat, "@")
}
// checkForMention checks if there is a mention to a specific user or to the keywords here / channel / all
func (m *ExplicitMentions) checkForMention(word string, keywords map[string][]string, groups map[string]*model.Group) bool {
var mentionType MentionType
switch strings.ToLower(word) {
case "@here":
m.HereMentioned = true
mentionType = ChannelMention
case "@channel":
m.ChannelMentioned = true
mentionType = ChannelMention
case "@all":
m.AllMentioned = true
mentionType = ChannelMention
default:
mentionType = KeywordMention
}
m.addGroupMention(word, groups)
if ids, match := keywords[strings.ToLower(word)]; match {
m.addMentions(ids, mentionType)
return true
}
// Case-sensitive check for first name
if ids, match := keywords[word]; match {
m.addMentions(ids, mentionType)
return true
}
return false
}
// isKeywordMultibyte checks if a word containing a multibyte character contains a multibyte keyword
func isKeywordMultibyte(keywords map[string][]string, word string) ([]string, bool) {
ids := []string{}
match := false
var multibyteKeywords []string
for keyword := range keywords {
if len(keyword) != utf8.RuneCountInString(keyword) {
multibyteKeywords = append(multibyteKeywords, keyword)
}
}
if len(word) != utf8.RuneCountInString(word) {
for _, key := range multibyteKeywords {
if strings.Contains(word, key) {
ids, match = keywords[key]
}
}
}
return ids, match
}
// Processes text to filter mentioned users and other potential mentions
func (m *ExplicitMentions) processText(text string, keywords map[string][]string, groups map[string]*model.Group) {
systemMentions := map[string]bool{"@here": true, "@channel": true, "@all": true}
for _, word := range strings.FieldsFunc(text, func(c rune) bool {
// Split on any whitespace or punctuation that can't be part of an at mention or emoji pattern
return !(c == ':' || c == '.' || c == '-' || c == '_' || c == '@' || unicode.IsLetter(c) || unicode.IsNumber(c))
}) {
// skip word with format ':word:' with an assumption that it is an emoji format only
if word[0] == ':' && word[len(word)-1] == ':' {
continue
}
word = strings.TrimLeft(word, ":.-_")
if m.checkForMention(word, keywords, groups) {
continue
}
foundWithoutSuffix := false
wordWithoutSuffix := word
for wordWithoutSuffix != "" && strings.LastIndexAny(wordWithoutSuffix, ".-:_") == (len(wordWithoutSuffix)-1) {
wordWithoutSuffix = wordWithoutSuffix[0 : len(wordWithoutSuffix)-1]
if m.checkForMention(wordWithoutSuffix, keywords, groups) {
foundWithoutSuffix = true
break
}
}
if foundWithoutSuffix {
continue
}
if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") {
// No need to bother about unicode as we are looking for ASCII characters.
last := word[len(word)-1]
switch last {
// If the word is possibly at the end of a sentence, remove that character.
case '.', '-', ':':
word = word[:len(word)-1]
}
m.OtherPotentialMentions = append(m.OtherPotentialMentions, word[1:])
} else if strings.ContainsAny(word, ".-:") {
// This word contains a character that may be the end of a sentence, so split further
splitWords := strings.FieldsFunc(word, func(c rune) bool {
return c == '.' || c == '-' || c == ':'
})
for _, splitWord := range splitWords {
if m.checkForMention(splitWord, keywords, groups) {
continue
}
if _, ok := systemMentions[splitWord]; !ok && strings.HasPrefix(splitWord, "@") {
m.OtherPotentialMentions = append(m.OtherPotentialMentions, splitWord[1:])
}
}
}
if ids, match := isKeywordMultibyte(keywords, word); match {
m.addMentions(ids, KeywordMention)
}
}
}
func (a *App) GetNotificationNameFormat(user *model.User) string {
if !*a.Config().PrivacySettings.ShowFullName {
return model.ShowUsername
}
data, err := a.Srv().Store().Preference().Get(user.Id, model.PreferenceCategoryDisplaySettings, model.PreferenceNameNameFormat)
if err != nil {
return *a.Config().TeamSettings.TeammateNameDisplay
}
return data.Value
}
type CRTNotifiers struct {
// Desktop contains the user IDs of thread followers to receive desktop notification
Desktop model.StringArray
// Email contains the user IDs of thread followers to receive email notification
Email model.StringArray
// Push contains the user IDs of thread followers to receive push notification
Push model.StringArray
}
func (c *CRTNotifiers) addFollowerToNotify(user *model.User, mentions *ExplicitMentions, channelMemberNotificationProps model.StringMap, channel *model.Channel) {
_, userWasMentioned := mentions.Mentions[user.Id]
notifyDesktop, notifyPush, notifyEmail := shouldUserNotifyCRT(user, userWasMentioned)
notifyChannelDesktop, notifyChannelPush := shouldChannelMemberNotifyCRT(channelMemberNotificationProps, userWasMentioned)
// respect the user global notify props when there are no channel specific ones (default)
// otherwise respect the channel member's notify props
if (channelMemberNotificationProps[model.DesktopNotifyProp] == model.ChannelNotifyDefault && notifyDesktop) || notifyChannelDesktop {
c.Desktop = append(c.Desktop, user.Id)
}
if notifyEmail {
c.Email = append(c.Email, user.Id)
}
// respect the user global notify props when there are no channel specific ones (default)
// otherwise respect the channel member's notify props
if (channelMemberNotificationProps[model.PushNotifyProp] == model.ChannelNotifyDefault && notifyPush) || notifyChannelPush {
c.Push = append(c.Push, user.Id)
}
}
// user global settings check for desktop, email, and push notifications
func shouldUserNotifyCRT(user *model.User, isMentioned bool) (notifyDesktop, notifyPush, notifyEmail bool) {
notifyDesktop = false
notifyPush = false
notifyEmail = false
desktop := user.NotifyProps[model.DesktopNotifyProp]
push := user.NotifyProps[model.PushNotifyProp]
shouldEmail := user.NotifyProps[model.EmailNotifyProp] == "true"
desktopThreads := user.NotifyProps[model.DesktopThreadsNotifyProp]
emailThreads := user.NotifyProps[model.EmailThreadsNotifyProp]
pushThreads := user.NotifyProps[model.PushThreadsNotifyProp]
// user should be notified via desktop notification in the case the notify prop is not set as no notify
// and either the user was mentioned or the CRT notify prop for desktop is set to all
if desktop != model.UserNotifyNone && (isMentioned || desktopThreads == model.UserNotifyAll || desktop == model.UserNotifyAll) {
notifyDesktop = true
}
// user should be notified via email when emailing is enabled and
// either the user was mentioned, or the CRT notify prop for email is set to all
if shouldEmail && (isMentioned || emailThreads == model.UserNotifyAll) {
notifyEmail = true
}
// user should be notified via push in the case the notify prop is not set as no notify
// and either the user was mentioned or the CRT push notify prop is set to all
if push != model.UserNotifyNone && (isMentioned || pushThreads == model.UserNotifyAll || push == model.UserNotifyAll) {
notifyPush = true
}
return
}
// channel specific settings check for desktop and push notifications
func shouldChannelMemberNotifyCRT(notifyProps model.StringMap, isMentioned bool) (notifyDesktop, notifyPush bool) {
notifyDesktop = false
notifyPush = false
desktop := notifyProps[model.DesktopNotifyProp]
push := notifyProps[model.PushNotifyProp]
desktopThreads := notifyProps[model.DesktopThreadsNotifyProp]
pushThreads := notifyProps[model.PushThreadsNotifyProp]
// user should be notified via desktop notification in the case the notify prop is not set as no notify or default
// and either the user was mentioned or the CRT notify prop for desktop is set to all
if desktop != model.ChannelNotifyDefault && desktop != model.ChannelNotifyNone && (isMentioned || desktopThreads == model.ChannelNotifyAll || desktop == model.ChannelNotifyAll) {
notifyDesktop = true
}
// user should be notified via push in the case the notify prop is not set as no notify or default
// and either the user was mentioned or the CRT push notify prop is set to all
if push != model.ChannelNotifyDefault && push != model.ChannelNotifyNone && (isMentioned || pushThreads == model.ChannelNotifyAll || push == model.ChannelNotifyAll) {
notifyPush = true
}
return
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"fmt"
"html"
"html/template"
"io"
"strings"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
email "github.com/mattermost/mattermost-server/v6/server/channels/app/email"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) sendNotificationEmail(c request.CTX, notification *PostNotification, user *model.User, team *model.Team, senderProfileImage []byte) error {
channel := notification.Channel
post := notification.Post
if channel.IsGroupOrDirect() {
teams, err := a.Srv().Store().Team().GetTeamsByUserId(user.Id)
if err != nil {
return errors.Wrap(err, "unable to get user teams")
}
// if the recipient isn't in the current user's team, just pick one
found := false
for i := range teams {
if teams[i].Id == team.Id {
found = true
break
}
}
if !found && len(teams) > 0 {
team = teams[0]
} else {
// in case the user hasn't joined any teams we send them to the select_team page
team = &model.Team{Name: "select_team", DisplayName: *a.Config().TeamSettings.SiteName}
}
}
if *a.Config().EmailSettings.EnableEmailBatching {
var sendBatched bool
if data, err := a.Srv().Store().Preference().Get(user.Id, model.PreferenceCategoryNotifications, model.PreferenceNameEmailInterval); err != nil {
// if the call fails, assume that the interval has not been explicitly set and batch the notifications
sendBatched = true
} else {
// if the user has chosen to receive notifications immediately, don't batch them
sendBatched = data.Value != model.PreferenceEmailIntervalNoBatchingSeconds
}
if sendBatched {
if err := a.Srv().EmailService.AddNotificationEmailToBatch(user, post, team); err == nil {
return nil
}
}
// fall back to sending a single email if we can't batch it for some reason
}
translateFunc := i18n.GetUserTranslations(user.Locale)
var useMilitaryTime bool
if data, err := a.Srv().Store().Preference().Get(user.Id, model.PreferenceCategoryDisplaySettings, model.PreferenceNameUseMilitaryTime); err != nil {
useMilitaryTime = true
} else {
useMilitaryTime = data.Value == "true"
}
nameFormat := a.GetNotificationNameFormat(user)
channelName := notification.GetChannelName(nameFormat, "")
senderName := notification.GetSenderName(nameFormat, *a.Config().ServiceSettings.EnablePostUsernameOverride)
emailNotificationContentsType := model.EmailNotificationContentsFull
if license := a.Srv().License(); license != nil && *license.Features.EmailNotificationContents {
emailNotificationContentsType = *a.Config().EmailSettings.EmailNotificationContentsType
}
var subjectText string
if channel.Type == model.ChannelTypeDirect {
subjectText = getDirectMessageNotificationEmailSubject(user, post, translateFunc, *a.Config().TeamSettings.SiteName, senderName, useMilitaryTime)
} else if channel.Type == model.ChannelTypeGroup {
subjectText = getGroupMessageNotificationEmailSubject(user, post, translateFunc, *a.Config().TeamSettings.SiteName, channelName, emailNotificationContentsType, useMilitaryTime)
} else if *a.Config().EmailSettings.UseChannelInEmailNotifications {
subjectText = getNotificationEmailSubject(user, post, translateFunc, *a.Config().TeamSettings.SiteName, team.DisplayName+" ("+channelName+")", useMilitaryTime)
} else {
subjectText = getNotificationEmailSubject(user, post, translateFunc, *a.Config().TeamSettings.SiteName, team.DisplayName, useMilitaryTime)
}
senderPhoto := ""
embeddedFiles := make(map[string]io.Reader)
if emailNotificationContentsType == model.EmailNotificationContentsFull && senderProfileImage != nil {
senderPhoto = "user-avatar.png"
embeddedFiles = map[string]io.Reader{
senderPhoto: bytes.NewReader(senderProfileImage),
}
}
landingURL := a.GetSiteURL() + "/landing#/" + team.Name
var bodyText, err = a.getNotificationEmailBody(c, user, post, channel, channelName, senderName, team.Name, landingURL, emailNotificationContentsType, useMilitaryTime, translateFunc, senderPhoto)
if err != nil {
return errors.Wrap(err, "unable to render the email notification template")
}
templateString := "<%s@" + utils.GetHostnameFromSiteURL(a.GetSiteURL()) + ">"
messageID := ""
inReplyTo := ""
references := ""
if post.Id != "" {
messageID = fmt.Sprintf(templateString, post.Id)
}
if post.RootId != "" {
referencesVal := fmt.Sprintf(templateString, post.RootId)
inReplyTo = referencesVal
references = referencesVal
}
a.Srv().Go(func() {
if nErr := a.Srv().EmailService.SendMailWithEmbeddedFiles(user.Email, html.UnescapeString(subjectText), bodyText, embeddedFiles, messageID, inReplyTo, references, "Notification"); nErr != nil {
mlog.Error("Error while sending the email", mlog.String("user_email", user.Email), mlog.Err(nErr))
}
})
if a.Metrics() != nil {
a.Metrics().IncrementPostSentEmail()
}
return nil
}
/**
* Computes the subject line for direct notification email messages
*/
func getDirectMessageNotificationEmailSubject(user *model.User, post *model.Post, translateFunc i18n.TranslateFunc, siteName string, senderName string, useMilitaryTime bool) string {
t := getFormattedPostTime(user, post, useMilitaryTime, translateFunc)
var subjectParameters = map[string]any{
"SiteName": siteName,
"SenderDisplayName": senderName,
"Month": t.Month,
"Day": t.Day,
"Year": t.Year,
}
return translateFunc("app.notification.subject.direct.full", subjectParameters)
}
/**
* Computes the subject line for group, public, and private email messages
*/
func getNotificationEmailSubject(user *model.User, post *model.Post, translateFunc i18n.TranslateFunc, siteName string, teamName string, useMilitaryTime bool) string {
t := getFormattedPostTime(user, post, useMilitaryTime, translateFunc)
var subjectParameters = map[string]any{
"SiteName": siteName,
"TeamName": teamName,
"Month": t.Month,
"Day": t.Day,
"Year": t.Year,
}
return translateFunc("app.notification.subject.notification.full", subjectParameters)
}
/**
* Computes the subject line for group email messages
*/
func getGroupMessageNotificationEmailSubject(user *model.User, post *model.Post, translateFunc i18n.TranslateFunc, siteName string, channelName string, emailNotificationContentsType string, useMilitaryTime bool) string {
t := getFormattedPostTime(user, post, useMilitaryTime, translateFunc)
var subjectParameters = map[string]any{
"SiteName": siteName,
"Month": t.Month,
"Day": t.Day,
"Year": t.Year,
}
if emailNotificationContentsType == model.EmailNotificationContentsFull {
subjectParameters["ChannelName"] = channelName
return translateFunc("app.notification.subject.group_message.full", subjectParameters)
}
return translateFunc("app.notification.subject.group_message.generic", subjectParameters)
}
/**
* If the name is longer than i characters, replace remaining characters with ...
*/
func truncateUserNames(name string, i int) string {
runes := []rune(name)
if len(runes) > i {
newString := string(runes[:i])
return newString + "..."
}
return name
}
type postData struct {
SenderName string
ChannelName string
Message template.HTML
MessageURL string
SenderPhoto string
PostPhoto string
Time string
ShowChannelIcon bool
OtherChannelMembersCount int
MessageAttachments []*email.EmailMessageAttachment
}
/**
* Computes the email body for notification messages
*/
func (a *App) getNotificationEmailBody(c request.CTX, recipient *model.User, post *model.Post, channel *model.Channel, channelName string, senderName string, teamName string, landingURL string, emailNotificationContentsType string, useMilitaryTime bool, translateFunc i18n.TranslateFunc, senderPhoto string) (string, error) {
pData := postData{
SenderName: truncateUserNames(senderName, 22),
SenderPhoto: senderPhoto,
}
t := getFormattedPostTime(recipient, post, useMilitaryTime, translateFunc)
messageTime := map[string]any{
"Hour": t.Hour,
"Minute": t.Minute,
"TimeZone": t.TimeZone,
}
if emailNotificationContentsType == model.EmailNotificationContentsFull {
postMessage := a.GetMessageForNotification(post, translateFunc)
postMessage = html.EscapeString(postMessage)
mdPostMessage, mdErr := utils.MarkdownToHTML(postMessage, a.GetSiteURL())
if mdErr != nil {
mlog.Warn("Encountered error while converting markdown to HTML", mlog.Err(mdErr))
mdPostMessage = postMessage
}
normalizedPostMessage, err := a.generateHyperlinkForChannels(c, mdPostMessage, teamName, landingURL)
if err != nil {
mlog.Warn("Encountered error while generating hyperlink for channels", mlog.String("team_name", teamName), mlog.Err(err))
normalizedPostMessage = mdPostMessage
}
pData.Message = template.HTML(normalizedPostMessage)
pData.Time = translateFunc("app.notification.body.dm.time", messageTime)
pData.MessageAttachments = email.ProcessMessageAttachments(post, a.GetSiteURL())
}
data := a.Srv().EmailService.NewEmailTemplateData(recipient.Locale)
data.Props["SiteURL"] = a.GetSiteURL()
if teamName != "select_team" {
data.Props["ButtonURL"] = landingURL + "/pl/" + post.Id
} else {
data.Props["ButtonURL"] = landingURL
}
data.Props["SenderName"] = senderName
data.Props["Button"] = translateFunc("api.templates.post_body.button")
data.Props["NotificationFooterTitle"] = translateFunc("app.notification.footer.title")
data.Props["NotificationFooterInfoLogin"] = translateFunc("app.notification.footer.infoLogin")
data.Props["NotificationFooterInfo"] = translateFunc("app.notification.footer.info")
if channel.Type == model.ChannelTypeDirect {
// Direct Messages
data.Props["Title"] = translateFunc("app.notification.body.dm.title", map[string]any{"SenderName": senderName})
data.Props["SubTitle"] = translateFunc("app.notification.body.dm.subTitle", map[string]any{"SenderName": senderName})
} else if channel.Type == model.ChannelTypeGroup {
// Group Messages
data.Props["Title"] = translateFunc("app.notification.body.group.title", map[string]any{"SenderName": senderName})
data.Props["SubTitle"] = translateFunc("app.notification.body.group.subTitle", map[string]any{"SenderName": senderName})
} else {
// mentions
data.Props["Title"] = translateFunc("app.notification.body.mention.title", map[string]any{"SenderName": senderName})
data.Props["SubTitle"] = translateFunc("app.notification.body.mention.subTitle", map[string]any{"SenderName": senderName, "ChannelName": channelName})
pData.ChannelName = channelName
}
// Override title and subtile for replies with CRT enabled
if a.IsCRTEnabledForUser(c, recipient.Id) && post.RootId != "" {
// Title is the same in all cases
data.Props["Title"] = translateFunc("app.notification.body.thread.title", map[string]any{"SenderName": senderName})
if channel.Type == model.ChannelTypeDirect {
// Direct Reply
data.Props["SubTitle"] = translateFunc("app.notification.body.thread_dm.subTitle", map[string]any{"SenderName": senderName})
} else if channel.Type == model.ChannelTypeGroup {
// Group Reply
data.Props["SubTitle"] = translateFunc("app.notification.body.thread_gm.subTitle", map[string]any{"SenderName": senderName})
} else if emailNotificationContentsType == model.EmailNotificationContentsFull {
// Channel Reply with full content
data.Props["SubTitle"] = translateFunc("app.notification.body.thread_channel_full.subTitle", map[string]any{"SenderName": senderName, "ChannelName": channelName})
} else {
// Channel Reply with generic content
data.Props["SubTitle"] = translateFunc("app.notification.body.thread_channel.subTitle", map[string]any{"SenderName": senderName})
}
}
// only include posts in notification email if email notification contents type is set to full
if emailNotificationContentsType == model.EmailNotificationContentsFull {
data.Props["Posts"] = []postData{pData}
} else {
data.Props["Posts"] = []postData{}
}
return a.Srv().TemplatesContainer().RenderToString("messages_notification", data)
}
type formattedPostTime struct {
Time time.Time
Year string
Month string
Day string
Hour string
Minute string
TimeZone string
}
func getFormattedPostTime(user *model.User, post *model.Post, useMilitaryTime bool, translateFunc i18n.TranslateFunc) formattedPostTime {
preferredTimezone := user.GetPreferredTimezone()
postTime := time.Unix(post.CreateAt/1000, 0)
zone, _ := postTime.Zone()
localTime := postTime
if preferredTimezone != "" {
loc, _ := time.LoadLocation(preferredTimezone)
if loc != nil {
localTime = postTime.In(loc)
zone, _ = localTime.Zone()
}
}
hour := localTime.Format("15")
period := ""
if !useMilitaryTime {
hour = localTime.Format("3")
period = " " + localTime.Format("PM")
}
return formattedPostTime{
Time: localTime,
Year: fmt.Sprintf("%d", localTime.Year()),
Month: translateFunc(localTime.Month().String()),
Day: fmt.Sprintf("%d", localTime.Day()),
Hour: hour,
Minute: fmt.Sprintf("%02d"+period, localTime.Minute()),
TimeZone: zone,
}
}
func (a *App) generateHyperlinkForChannels(c request.CTX, postMessage, teamName, teamURL string) (string, *model.AppError) {
team, err := a.GetTeamByName(teamName)
if err != nil {
return "", err
}
channelNames := model.ChannelMentions(postMessage)
if len(channelNames) == 0 {
return postMessage, nil
}
channels, err := a.GetChannelsByNames(c, channelNames, team.Id)
if err != nil {
return "", err
}
visited := make(map[string]bool)
for _, ch := range channels {
if !visited[ch.Id] && ch.Type == model.ChannelTypeOpen {
channelURL := teamURL + "/channels/" + ch.Name
channelHyperLink := fmt.Sprintf("<a href='%s'>%s</a>", channelURL, "~"+ch.Name)
postMessage = strings.Replace(postMessage, "~"+ch.Name, channelHyperLink, -1)
visited[ch.Id] = true
}
}
return postMessage, nil
}
func (a *App) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string {
return a.Srv().EmailService.GetMessageForNotification(post, translateFunc)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"runtime"
"strings"
"sync"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type notificationType string
const (
notificationTypeClear notificationType = "clear"
notificationTypeMessage notificationType = "message"
notificationTypeUpdateBadge notificationType = "update_badge"
notificationTypeDummy notificationType = "dummy"
)
type PushNotificationsHub struct {
notificationsChan chan PushNotification
app *App // XXX: This will go away once push notifications move to their own package.
sema chan struct{}
stopChan chan struct{}
wg *sync.WaitGroup
semaWg *sync.WaitGroup
buffer int
}
type PushNotification struct {
notificationType notificationType
currentSessionId string
userID string
channelID string
rootID string
post *model.Post
user *model.User
channel *model.Channel
senderName string
channelName string
explicitMention bool
channelWideMention bool
replyToThreadType string
}
func (a *App) sendPushNotificationSync(c request.CTX, post *model.Post, user *model.User, channel *model.Channel, channelName string, senderName string,
explicitMention bool, channelWideMention bool, replyToThreadType string) *model.AppError {
cfg := a.Config()
msg, appErr := a.BuildPushNotificationMessage(
c,
*cfg.EmailSettings.PushNotificationContents,
post,
user,
channel,
channelName,
senderName,
explicitMention,
channelWideMention,
replyToThreadType,
)
if appErr != nil {
return appErr
}
return a.sendPushNotificationToAllSessions(msg, user.Id, "")
}
func (a *App) sendPushNotificationToAllSessions(msg *model.PushNotification, userID string, skipSessionId string) *model.AppError {
sessions, err := a.getMobileAppSessions(userID)
if err != nil {
return err
}
if msg == nil {
return model.NewAppError(
"pushNotification",
"api.push_notifications.message.parse.app_error",
nil,
"",
http.StatusBadRequest,
)
}
for _, session := range sessions {
// Don't send notifications to this session if it's expired or we want to skip it
if session.IsExpired() || (skipSessionId != "" && skipSessionId == session.Id) {
continue
}
// We made a copy to avoid decoding and parsing all the time
tmpMessage := msg.DeepCopy()
tmpMessage.SetDeviceIdAndPlatform(session.DeviceId)
tmpMessage.AckId = model.NewId()
err := a.sendToPushProxy(tmpMessage, session)
if err != nil {
a.NotificationsLog().Error("Notification error",
mlog.String("ackId", tmpMessage.AckId),
mlog.String("type", tmpMessage.Type),
mlog.String("userId", session.UserId),
mlog.String("postId", tmpMessage.PostId),
mlog.String("channelId", tmpMessage.ChannelId),
mlog.String("deviceId", tmpMessage.DeviceId),
mlog.String("status", err.Error()),
)
continue
}
a.NotificationsLog().Info("Notification sent",
mlog.String("ackId", tmpMessage.AckId),
mlog.String("type", tmpMessage.Type),
mlog.String("userId", session.UserId),
mlog.String("postId", tmpMessage.PostId),
mlog.String("channelId", tmpMessage.ChannelId),
mlog.String("deviceId", tmpMessage.DeviceId),
mlog.String("status", model.PushSendSuccess),
)
if a.Metrics() != nil {
a.Metrics().IncrementPostSentPush()
}
}
return nil
}
func (a *App) sendPushNotification(notification *PostNotification, user *model.User, explicitMention, channelWideMention bool, replyToThreadType string) {
cfg := a.Config()
channel := notification.Channel
post := notification.Post
nameFormat := a.GetNotificationNameFormat(user)
channelName := notification.GetChannelName(nameFormat, user.Id)
senderName := notification.GetSenderName(nameFormat, *cfg.ServiceSettings.EnablePostUsernameOverride)
select {
case a.Srv().PushNotificationsHub.notificationsChan <- PushNotification{
notificationType: notificationTypeMessage,
post: post,
user: user,
channel: channel,
senderName: senderName,
channelName: channelName,
explicitMention: explicitMention,
channelWideMention: channelWideMention,
replyToThreadType: replyToThreadType,
}:
case <-a.Srv().PushNotificationsHub.stopChan:
return
}
}
func (a *App) getPushNotificationMessage(contentsConfig, postMessage string, explicitMention, channelWideMention,
hasFiles bool, senderName string, channelType model.ChannelType, replyToThreadType string, userLocale i18n.TranslateFunc) string {
// If the post only has images then push an appropriate message
if postMessage == "" && hasFiles {
if channelType == model.ChannelTypeDirect {
return strings.Trim(userLocale("api.post.send_notifications_and_forget.push_image_only"), " ")
}
return senderName + userLocale("api.post.send_notifications_and_forget.push_image_only")
}
if contentsConfig == model.FullNotification {
if channelType == model.ChannelTypeDirect && replyToThreadType != model.CommentsNotifyCRT {
return model.ClearMentionTags(postMessage)
}
return senderName + ": " + model.ClearMentionTags(postMessage)
}
if channelType == model.ChannelTypeDirect {
if replyToThreadType == model.CommentsNotifyCRT {
if contentsConfig == model.GenericNoChannelNotification {
return senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_crt_thread")
}
return senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_crt_thread_dm")
}
return userLocale("api.post.send_notifications_and_forget.push_message")
}
if replyToThreadType == model.CommentsNotifyCRT {
return senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_crt_thread")
}
if channelWideMention {
return senderName + userLocale("api.post.send_notification_and_forget.push_channel_mention")
}
if explicitMention {
return senderName + userLocale("api.post.send_notifications_and_forget.push_explicit_mention")
}
if replyToThreadType == model.CommentsNotifyRoot {
return senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_post")
}
if replyToThreadType == model.CommentsNotifyAny {
return senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_thread")
}
if replyToThreadType == model.UserNotifyAll {
return senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_crt_thread")
}
return senderName + userLocale("api.post.send_notifications_and_forget.push_general_message")
}
func (a *App) getUserBadgeCount(userID string, isCRTEnabled bool) (int, *model.AppError) {
unreadCount, err := a.Srv().Store().User().GetUnreadCount(userID, isCRTEnabled)
if err != nil {
return 0, model.NewAppError("getUserBadgeCount", "app.user.get_unread_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
badgeCount := int(unreadCount)
if isCRTEnabled {
threadUnreadMentions, err := a.Srv().Store().Thread().GetTotalUnreadMentions(userID, "", model.GetUserThreadsOpts{})
if err != nil {
return 0, model.NewAppError("getUserBadgeCount", "app.user.get_thread_count_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
badgeCount += int(threadUnreadMentions)
}
return badgeCount, nil
}
func (a *App) clearPushNotificationSync(c request.CTX, currentSessionId, userID, channelID, rootID string) *model.AppError {
isCRTEnabled := a.IsCRTEnabledForUser(c, userID)
badgeCount, err := a.getUserBadgeCount(userID, isCRTEnabled)
if err != nil {
return model.NewAppError("clearPushNotificationSync", "app.user.get_badge_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
msg := &model.PushNotification{
Type: model.PushTypeClear,
Version: model.PushMessageV2,
ChannelId: channelID,
RootId: rootID,
ContentAvailable: 1,
Badge: badgeCount,
IsCRTEnabled: isCRTEnabled,
}
return a.sendPushNotificationToAllSessions(msg, userID, currentSessionId)
}
func (a *App) clearPushNotification(currentSessionId, userID, channelID, rootID string) {
select {
case a.Srv().PushNotificationsHub.notificationsChan <- PushNotification{
notificationType: notificationTypeClear,
currentSessionId: currentSessionId,
userID: userID,
channelID: channelID,
rootID: rootID,
}:
case <-a.Srv().PushNotificationsHub.stopChan:
return
}
}
func (a *App) updateMobileAppBadgeSync(c request.CTX, userID string) *model.AppError {
badgeCount, err := a.getUserBadgeCount(userID, a.IsCRTEnabledForUser(c, userID))
if err != nil {
return model.NewAppError("updateMobileAppBadgeSync", "app.user.get_badge_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
msg := &model.PushNotification{
Type: model.PushTypeUpdateBadge,
Version: model.PushMessageV2,
Sound: "none",
ContentAvailable: 1,
Badge: badgeCount,
}
return a.sendPushNotificationToAllSessions(msg, userID, "")
}
func (a *App) UpdateMobileAppBadge(userID string) {
select {
case a.Srv().PushNotificationsHub.notificationsChan <- PushNotification{
notificationType: notificationTypeUpdateBadge,
userID: userID,
}:
case <-a.Srv().PushNotificationsHub.stopChan:
return
}
}
func (s *Server) createPushNotificationsHub(c request.CTX) {
buffer := *s.platform.Config().EmailSettings.PushNotificationBuffer
hub := PushNotificationsHub{
notificationsChan: make(chan PushNotification, buffer),
app: New(ServerConnector(s.Channels())),
wg: new(sync.WaitGroup),
semaWg: new(sync.WaitGroup),
sema: make(chan struct{}, runtime.NumCPU()*8), // numCPU * 8 is a good amount of concurrency.
stopChan: make(chan struct{}),
buffer: buffer,
}
go hub.start(c)
s.PushNotificationsHub = hub
}
func (hub *PushNotificationsHub) start(c request.CTX) {
hub.wg.Add(1)
defer hub.wg.Done()
for {
select {
case notification := <-hub.notificationsChan:
// We just ignore dummy notifications.
// These are used to pump out any remaining notifications
// before we stop the hub.
if notification.notificationType == notificationTypeDummy {
continue
}
// Adding to the waitgroup first.
hub.semaWg.Add(1)
// Get token.
hub.sema <- struct{}{}
go func(notification PushNotification) {
defer func() {
// Release token.
<-hub.sema
// Now marking waitgroup as done.
hub.semaWg.Done()
}()
var err *model.AppError
switch notification.notificationType {
case notificationTypeClear:
err = hub.app.clearPushNotificationSync(c, notification.currentSessionId, notification.userID, notification.channelID, notification.rootID)
case notificationTypeMessage:
err = hub.app.sendPushNotificationSync(
c,
notification.post,
notification.user,
notification.channel,
notification.channelName,
notification.senderName,
notification.explicitMention,
notification.channelWideMention,
notification.replyToThreadType,
)
case notificationTypeUpdateBadge:
err = hub.app.updateMobileAppBadgeSync(c, notification.userID)
default:
mlog.Debug("Invalid notification type", mlog.String("notification_type", string(notification.notificationType)))
}
if err != nil {
mlog.Error("Unable to send push notification", mlog.String("notification_type", string(notification.notificationType)), mlog.Err(err))
}
}(notification)
case <-hub.stopChan:
return
}
}
}
func (hub *PushNotificationsHub) stop() {
// Drain the channel.
for i := 0; i < hub.buffer+1; i++ {
hub.notificationsChan <- PushNotification{
notificationType: notificationTypeDummy,
}
}
close(hub.stopChan)
// We need to wait for the outer for loop to exit first.
// We cannot just send struct{}{} to stopChan because there are
// other listeners to the channel. And sending just once
// will cause a race.
hub.wg.Wait()
// And then we wait for the semaphore to finish.
hub.semaWg.Wait()
}
func (s *Server) StopPushNotificationsHubWorkers() {
s.PushNotificationsHub.stop()
}
func (a *App) rawSendToPushProxy(msg *model.PushNotification) (model.PushResponse, error) {
msgJSON, err := json.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("failed to encode to JSON: %w", err)
}
url := strings.TrimRight(*a.Config().EmailSettings.PushNotificationServer, "/") + model.APIURLSuffixV1 + "/send_push"
request, err := http.NewRequest("POST", url, bytes.NewReader(msgJSON))
if err != nil {
return nil, err
}
resp, err := a.Srv().pushNotificationClient.Do(request)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var pushResponse model.PushResponse
if err := json.NewDecoder(resp.Body).Decode(&pushResponse); err != nil {
return nil, fmt.Errorf("failed to decode from JSON: %w", err)
}
return pushResponse, nil
}
func (a *App) sendToPushProxy(msg *model.PushNotification, session *model.Session) error {
msg.ServerId = a.TelemetryId()
a.NotificationsLog().Info("Notification will be sent",
mlog.String("ackId", msg.AckId),
mlog.String("type", msg.Type),
mlog.String("userId", session.UserId),
mlog.String("postId", msg.PostId),
mlog.String("status", model.PushSendPrepare),
)
pushResponse, err := a.rawSendToPushProxy(msg)
if err != nil {
return err
}
switch pushResponse[model.PushStatus] {
case model.PushStatusRemove:
a.AttachDeviceId(session.Id, "", session.ExpiresAt)
a.ClearSessionCacheForUser(session.UserId)
return errors.New("device was reported as removed")
case model.PushStatusFail:
return errors.New(pushResponse[model.PushStatusErrorMsg])
}
return nil
}
func (a *App) SendAckToPushProxy(ack *model.PushNotificationAck) error {
if ack == nil {
return nil
}
a.NotificationsLog().Info("Notification received",
mlog.String("ackId", ack.Id),
mlog.String("type", ack.NotificationType),
mlog.String("deviceType", ack.ClientPlatform),
mlog.Int64("receivedAt", ack.ClientReceivedAt),
mlog.String("status", model.PushReceived),
)
ackJSON, err := json.Marshal(ack)
if err != nil {
return fmt.Errorf("failed to encode to JSON: %w", err)
}
request, err := http.NewRequest(
"POST",
strings.TrimRight(*a.Config().EmailSettings.PushNotificationServer, "/")+model.APIURLSuffixV1+"/ack",
bytes.NewReader(ackJSON),
)
if err != nil {
return err
}
resp, err := a.Srv().pushNotificationClient.Do(request)
if err != nil {
return err
}
defer resp.Body.Close()
// Reading the body to completion.
_, err = io.Copy(io.Discard, resp.Body)
return err
}
func (a *App) getMobileAppSessions(userID string) ([]*model.Session, *model.AppError) {
sessions, err := a.Srv().Store().Session().GetSessionsWithActiveDeviceIds(userID)
if err != nil {
return nil, model.NewAppError("getMobileAppSessions", "app.session.get_sessions.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return sessions, nil
}
func ShouldSendPushNotification(user *model.User, channelNotifyProps model.StringMap, wasMentioned bool, status *model.Status, post *model.Post) bool {
return DoesNotifyPropsAllowPushNotification(user, channelNotifyProps, post, wasMentioned) &&
DoesStatusAllowPushNotification(user.NotifyProps, status, post.ChannelId)
}
func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps model.StringMap, post *model.Post, wasMentioned bool) bool {
userNotifyProps := user.NotifyProps
userNotify := userNotifyProps[model.PushNotifyProp]
channelNotify, ok := channelNotifyProps[model.PushNotifyProp]
if !ok || channelNotify == "" {
channelNotify = model.ChannelNotifyDefault
}
// If the channel is muted do not send push notifications
if channelNotifyProps[model.MarkUnreadNotifyProp] == model.ChannelMarkUnreadMention {
return false
}
if post.IsSystemMessage() {
return false
}
if channelNotify == model.UserNotifyNone {
return false
}
if channelNotify == model.ChannelNotifyMention && !wasMentioned {
return false
}
if userNotify == model.UserNotifyMention && channelNotify == model.ChannelNotifyDefault && !wasMentioned {
return false
}
if (userNotify == model.UserNotifyAll || channelNotify == model.ChannelNotifyAll) &&
(post.UserId != user.Id || post.GetProp("from_webhook") == "true") {
return true
}
if userNotify == model.UserNotifyNone &&
channelNotify == model.ChannelNotifyDefault {
return false
}
return true
}
func DoesStatusAllowPushNotification(userNotifyProps model.StringMap, status *model.Status, channelID string) bool {
// If User status is DND or OOO return false right away
if status.Status == model.StatusDnd || status.Status == model.StatusOutOfOffice {
return false
}
pushStatus, ok := userNotifyProps[model.PushStatusNotifyProp]
if (pushStatus == model.StatusOnline || !ok) && (status.ActiveChannel != channelID || model.GetMillis()-status.LastActivityAt > model.StatusChannelTimeout) {
return true
}
if pushStatus == model.StatusAway && (status.Status == model.StatusAway || status.Status == model.StatusOffline) {
return true
}
if pushStatus == model.StatusOffline && status.Status == model.StatusOffline {
return true
}
return false
}
func (a *App) BuildPushNotificationMessage(c request.CTX, contentsConfig string, post *model.Post, user *model.User, channel *model.Channel, channelName string, senderName string,
explicitMention bool, channelWideMention bool, replyToThreadType string) (*model.PushNotification, *model.AppError) {
var msg *model.PushNotification
notificationInterface := a.ch.Notification
if (notificationInterface == nil || notificationInterface.CheckLicense() != nil) && contentsConfig == model.IdLoadedNotification {
contentsConfig = model.GenericNotification
}
if contentsConfig == model.IdLoadedNotification {
msg = a.buildIdLoadedPushNotificationMessage(c, channel, post, user)
} else {
msg = a.buildFullPushNotificationMessage(c, contentsConfig, post, user, channel, channelName, senderName, explicitMention, channelWideMention, replyToThreadType)
}
badgeCount, err := a.getUserBadgeCount(user.Id, a.IsCRTEnabledForUser(c, user.Id))
if err != nil {
return nil, model.NewAppError("BuildPushNotificationMessage", "app.user.get_badge_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
msg.Badge = badgeCount
return msg, nil
}
func (a *App) SendTestPushNotification(deviceID string) string {
if !a.canSendPushNotifications() {
return "false"
}
msg := &model.PushNotification{
Version: "2",
Type: model.PushTypeTest,
ServerId: a.TelemetryId(),
Badge: -1,
}
msg.SetDeviceIdAndPlatform(deviceID)
pushResponse, err := a.rawSendToPushProxy(msg)
if err != nil {
a.NotificationsLog().Error("Notification error",
mlog.String("type", msg.Type),
mlog.String("deviceId", msg.DeviceId),
mlog.String("status", err.Error()),
)
return "unknown"
}
switch pushResponse[model.PushStatus] {
case model.PushStatusRemove:
return "false"
case model.PushStatusFail:
a.NotificationsLog().Error("Notification error",
mlog.String("type", msg.Type),
mlog.String("deviceId", msg.DeviceId),
mlog.String("status", pushResponse[model.PushStatusErrorMsg]),
)
return "unknown"
}
return "true"
}
func (a *App) buildIdLoadedPushNotificationMessage(c request.CTX, channel *model.Channel, post *model.Post, user *model.User) *model.PushNotification {
userLocale := i18n.GetUserTranslations(user.Locale)
msg := &model.PushNotification{
PostId: post.Id,
ChannelId: post.ChannelId,
RootId: post.RootId,
IsCRTEnabled: a.IsCRTEnabledForUser(c, user.Id),
Category: model.CategoryCanReply,
Version: model.PushMessageV2,
TeamId: channel.TeamId,
Type: model.PushTypeMessage,
IsIdLoaded: true,
SenderId: user.Id,
Message: userLocale("api.push_notification.id_loaded.default_message"),
}
return msg
}
func (a *App) buildFullPushNotificationMessage(c request.CTX, contentsConfig string, post *model.Post, user *model.User, channel *model.Channel, channelName string, senderName string,
explicitMention bool, channelWideMention bool, replyToThreadType string) *model.PushNotification {
msg := &model.PushNotification{
Category: model.CategoryCanReply,
Version: model.PushMessageV2,
Type: model.PushTypeMessage,
TeamId: channel.TeamId,
ChannelId: channel.Id,
PostId: post.Id,
RootId: post.RootId,
SenderId: post.UserId,
IsCRTEnabled: false,
IsIdLoaded: false,
}
userLocale := i18n.GetUserTranslations(user.Locale)
cfg := a.Config()
if contentsConfig != model.GenericNoChannelNotification || channel.Type == model.ChannelTypeDirect {
msg.ChannelName = channelName
}
if a.IsCRTEnabledForUser(c, user.Id) {
msg.IsCRTEnabled = true
if post.RootId != "" {
if contentsConfig != model.GenericNoChannelNotification {
props := map[string]any{"channelName": channelName}
msg.ChannelName = userLocale("api.push_notification.title.collapsed_threads", props)
if channel.Type == model.ChannelTypeDirect {
msg.ChannelName = userLocale("api.push_notification.title.collapsed_threads_dm")
}
}
}
}
msg.SenderName = senderName
if ou, ok := post.GetProp("override_username").(string); ok && *cfg.ServiceSettings.EnablePostUsernameOverride {
msg.OverrideUsername = ou
msg.SenderName = ou
}
if oi, ok := post.GetProp("override_icon_url").(string); ok && *cfg.ServiceSettings.EnablePostIconOverride {
msg.OverrideIconURL = oi
}
if fw, ok := post.GetProp("from_webhook").(string); ok {
msg.FromWebhook = fw
}
postMessage := post.Message
stripped, err := utils.StripMarkdown(postMessage)
if err != nil {
mlog.Warn("Failed parse to markdown", mlog.String("post_id", post.Id), mlog.Err(err))
} else {
postMessage = stripped
}
for _, attachment := range post.Attachments() {
if attachment.Fallback != "" {
postMessage += "\n" + attachment.Fallback
}
}
hasFiles := post.FileIds != nil && len(post.FileIds) > 0
msg.Message = a.getPushNotificationMessage(
contentsConfig,
postMessage,
explicitMention,
channelWideMention,
hasFiles,
msg.SenderName,
channel.Type,
replyToThreadType,
userLocale,
)
return msg
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const lastTrialNotificationTimeStamp = "LAST_TRIAL_NOTIFICATION_TIMESTAMP"
const lastUpgradeNotificationTimeStamp = "LAST_UPGRADE_NOTIFICATION_TIMESTAMP"
const defaultNotifyAdminCoolOffDays = 14
func (a *App) SaveAdminNotification(userId string, notifyData *model.NotifyAdminToUpgradeRequest) *model.AppError {
requiredFeature := notifyData.RequiredFeature
requiredPlan := notifyData.RequiredPlan
trial := notifyData.TrialNotification
isUserAlreadyNotified := a.UserAlreadyNotifiedOnRequiredFeature(userId, requiredFeature)
if isUserAlreadyNotified {
return model.NewAppError("app.SaveAdminNotification", "api.cloud.notify_admin_to_upgrade_error.already_notified", nil, "", http.StatusForbidden)
}
_, appErr := a.SaveAdminNotifyData(&model.NotifyAdminData{
UserId: userId,
RequiredPlan: requiredPlan,
RequiredFeature: requiredFeature,
Trial: trial,
})
if appErr != nil {
return appErr
}
return nil
}
func (a *App) DoCheckForAdminNotifications(trial bool) *model.AppError {
ctx := request.EmptyContext(a.Srv().Log())
currentSKU := "starter"
license := a.Srv().License()
if license != nil {
currentSKU = license.SkuShortName
}
workspaceName := ""
return a.SendNotifyAdminPosts(ctx, workspaceName, currentSKU, trial)
}
func (a *App) SaveAdminNotifyData(data *model.NotifyAdminData) (*model.NotifyAdminData, *model.AppError) {
d, err := a.Srv().Store().NotifyAdmin().Save(data)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("SaveAdminNotifyData", "app.notify_admin.save.app_error", nil, nfErr.Error(), http.StatusNotFound)
default:
return nil, model.NewAppError("SaveAdminNotifyData", "app.notify_admin.save.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return d, nil
}
func filterNotificationData(data []*model.NotifyAdminData, test func(*model.NotifyAdminData) bool) (ret []*model.NotifyAdminData) {
for _, d := range data {
if test(d) {
ret = append(ret, d)
}
}
return
}
func (a *App) SendNotifyAdminPosts(c *request.Context, workspaceName string, currentSKU string, trial bool) *model.AppError {
if !a.CanNotifyAdmin(trial) {
return model.NewAppError("SendNotifyAdminPosts", "app.notify_admin.send_notification_post.app_error", nil, "Cannot notify yet", http.StatusForbidden)
}
sysadmins, appErr := a.GetUsersFromProfiles(&model.UserGetOptions{
Page: 0,
PerPage: 100,
Role: model.SystemAdminRoleId,
Inactive: false,
})
if appErr != nil {
return appErr
}
systemBot, appErr := a.GetSystemBot()
if appErr != nil {
return appErr
}
now := model.GetMillis()
data, err := a.Srv().Store().NotifyAdmin().Get(trial)
if err != nil {
return model.NewAppError("SendNotifyAdminPosts", "app.notify_admin.send_notification_post.app_error", nil, err.Error(), http.StatusInternalServerError)
}
data = filterNotificationData(data, func(nad *model.NotifyAdminData) bool { return nad.RequiredPlan != currentSKU })
if len(data) == 0 {
a.Log().Warn("No notification data available")
return nil
}
userBasedPaidFeatureData, userBasedPluginData := a.groupNotifyAdminByUser(data)
featureBasedData := a.groupNotifyAdminByPaidFeature(data)
pluginBasedData := a.groupNotifyAdminByPlugin(data)
for _, admin := range sysadmins {
if len(userBasedPaidFeatureData) > 0 && len(featureBasedData) > 0 {
a.upgradePlanAdminNotifyPost(c, workspaceName, userBasedPaidFeatureData, featureBasedData, systemBot, admin, trial)
}
if len(userBasedPluginData) > 0 {
a.pluginInstallAdminNotifyPost(c, userBasedPluginData, pluginBasedData, systemBot, admin)
}
}
a.FinishSendAdminNotifyPost(trial, now, pluginBasedData)
return nil
}
func (a *App) pluginInstallAdminNotifyPost(c *request.Context, userBasedData map[string][]*model.NotifyAdminData, pluginBasedPluginData map[string][]*model.NotifyAdminData, systemBot *model.Bot, admin *model.User) {
props := make(model.StringInterface)
channel, appErr := a.GetOrCreateDirectChannel(c, systemBot.UserId, admin.Id)
if appErr != nil {
a.Log().Warn("Error getting direct channel", mlog.Err(appErr))
return
}
post := &model.Post{
UserId: systemBot.UserId,
ChannelId: channel.Id,
Type: fmt.Sprintf("%spl_notification", model.PostCustomTypePrefix), // webapp will have to create renderer for this custom post type
}
props["requested_plugins_by_plugin_ids"] = pluginBasedPluginData
props["requested_plugins_by_user_ids"] = userBasedData
post.SetProps(props)
_, appErr = a.CreatePost(c, post, channel, false, true)
if appErr != nil {
a.Log().Warn("Error creating post", mlog.Err(appErr))
}
}
func (a *App) upgradePlanAdminNotifyPost(c *request.Context, workspaceName string, userBasedData map[string][]*model.NotifyAdminData, featureBasedData map[model.MattermostFeature][]*model.NotifyAdminData, systemBot *model.Bot, admin *model.User, trial bool) {
props := make(model.StringInterface)
T := i18n.GetUserTranslations(admin.Locale)
message := T("app.cloud.upgrade_plan_bot_message", map[string]interface{}{"UsersNum": len(userBasedData), "WorkspaceName": workspaceName})
if len(userBasedData) == 1 {
message = T("app.cloud.upgrade_plan_bot_message_single", map[string]interface{}{"UsersNum": len(userBasedData), "WorkspaceName": workspaceName}) // todo (allan): investigate if translations library can do this
}
if trial {
message = T("app.cloud.trial_plan_bot_message", map[string]interface{}{"UsersNum": len(userBasedData), "WorkspaceName": workspaceName})
if len(userBasedData) == 1 {
message = T("app.cloud.trial_plan_bot_message_single", map[string]interface{}{"UsersNum": len(userBasedData), "WorkspaceName": workspaceName})
}
}
channel, appErr := a.GetOrCreateDirectChannel(c, systemBot.UserId, admin.Id)
if appErr != nil {
a.Log().Warn("Error getting direct channel", mlog.Err(appErr))
return
}
post := &model.Post{
Message: message,
UserId: systemBot.UserId,
ChannelId: channel.Id,
Type: fmt.Sprintf("%sup_notification", model.PostCustomTypePrefix), // webapp will have to create renderer for this custom post type
}
props["requested_features"] = featureBasedData
props["trial"] = trial
post.SetProps(props)
_, appErr = a.CreatePost(c, post, channel, false, true)
if appErr != nil {
a.Log().Warn("Error creating post", mlog.Err(appErr))
}
}
func (a *App) UserAlreadyNotifiedOnRequiredFeature(user string, feature model.MattermostFeature) bool {
data, err := a.Srv().Store().NotifyAdmin().GetDataByUserIdAndFeature(user, feature)
if err != nil {
return false
}
if len(data) > 0 {
return true // if we find data, it means this user already notified on the need for this feature
}
return false
}
func (a *App) CanNotifyAdmin(trial bool) bool {
systemVarName := lastUpgradeNotificationTimeStamp
if trial {
systemVarName = lastTrialNotificationTimeStamp
}
sysVal, sysValErr := a.Srv().Store().System().GetByName(systemVarName)
if sysValErr != nil {
var nfErr *store.ErrNotFound
if errors.As(sysValErr, &nfErr) { // if no timestamps have been recorded before, system is free to notify
return true
}
a.Log().Error("Cannot notify", mlog.Err(sysValErr))
return false
}
lastNotificationTimestamp, err := strconv.ParseFloat(sysVal.Value, 64)
if err != nil {
a.Log().Error("Cannot notify", mlog.Err(err))
return false
}
coolOffPeriodDaysEnv := os.Getenv("MM_NOTIFY_ADMIN_COOL_OFF_DAYS")
coolOffPeriodDays, parseError := strconv.ParseFloat(coolOffPeriodDaysEnv, 64)
if parseError != nil {
coolOffPeriodDays = defaultNotifyAdminCoolOffDays
}
daysToMillis := coolOffPeriodDays * 24 * 60 * 60 * 1000
timeDiff := model.GetMillis() - int64(lastNotificationTimestamp)
return timeDiff >= int64(daysToMillis)
}
func (a *App) FinishSendAdminNotifyPost(trial bool, now int64, pluginBasedData map[string][]*model.NotifyAdminData) {
systemVarName := lastUpgradeNotificationTimeStamp
if trial {
systemVarName = lastTrialNotificationTimeStamp
}
val := strconv.FormatInt(model.GetMillis(), 10)
sysVar := &model.System{Name: systemVarName, Value: val}
if err := a.Srv().Store().System().SaveOrUpdate(sysVar); err != nil {
a.Log().Error("Unable to finish send admin notify post job", mlog.Err(err))
}
// All the requested features notifications are now sent in a post and can safely be removed except
// the plugin notify admin. We keep it as we do not want the same user to send the notification for the same plugin.
// We update the NotifyAdmin SentAt to keep track of it.
for pluginId := range pluginBasedData {
notifications := pluginBasedData[pluginId]
for _, notification := range notifications {
requiredFeature := notification.RequiredFeature
requiredPlan := notification.RequiredPlan
userId := notification.UserId
if err := a.Srv().Store().NotifyAdmin().Update(userId, requiredPlan, requiredFeature, now); err != nil {
a.Log().Error("Unable to update SentAt for work template feature", mlog.Err(err))
}
}
}
if err := a.Srv().Store().NotifyAdmin().DeleteBefore(trial, now); err != nil {
a.Log().Error("Unable to finish send admin notify post job", mlog.Err(err))
}
}
func (a *App) groupNotifyAdminByUser(data []*model.NotifyAdminData) (map[string][]*model.NotifyAdminData, map[string][]*model.NotifyAdminData) {
userBasedPaidFeatureData := make(map[string][]*model.NotifyAdminData)
userBasedPluginData := make(map[string][]*model.NotifyAdminData)
for _, d := range data {
if strings.HasPrefix(string(d.RequiredFeature), string(model.PluginFeature)) {
userBasedPluginData[d.UserId] = append(userBasedPluginData[d.UserId], d)
} else {
userBasedPaidFeatureData[d.UserId] = append(userBasedPaidFeatureData[d.UserId], d)
}
}
return userBasedPaidFeatureData, userBasedPluginData
}
func (a *App) groupNotifyAdminByPaidFeature(data []*model.NotifyAdminData) map[model.MattermostFeature][]*model.NotifyAdminData {
myMap := make(map[model.MattermostFeature][]*model.NotifyAdminData)
for _, d := range data {
if strings.HasPrefix(string(d.RequiredFeature), string(model.PluginFeature)) {
continue
}
myMap[d.RequiredFeature] = append(myMap[d.RequiredFeature], d)
}
return myMap
}
func (a *App) groupNotifyAdminByPlugin(data []*model.NotifyAdminData) map[string][]*model.NotifyAdminData {
myMap := make(map[string][]*model.NotifyAdminData)
for _, d := range data {
if strings.HasPrefix(string(d.RequiredFeature), string(model.PluginFeature)) {
plugins := strings.Split(d.RequiredPlan, ",")
for _, plugin := range plugins {
myMap[plugin] = append(myMap[plugin], d)
}
}
}
return myMap
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"context"
b64 "encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
OAuthCookieMaxAgeSeconds = 30 * 60 // 30 minutes
CookieOAuth = "MMOAUTH"
OpenIDScope = "openid"
)
func (a *App) CreateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("CreateOAuthApp", "api.oauth.register_oauth_app.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
app.ClientSecret = model.NewId()
oauthApp, err := a.Srv().Store().OAuth().SaveApp(app)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateOAuthApp", "app.oauth.save_app.existing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("CreateOAuthApp", "app.oauth.save_app.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return oauthApp, nil
}
func (a *App) GetOAuthApp(appID string) (*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
oauthApp, err := a.Srv().Store().OAuth().GetApp(appID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetOAuthApp", "app.oauth.get_app.find.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetOAuthApp", "app.oauth.get_app.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return oauthApp, nil
}
func (a *App) UpdateOAuthApp(oldApp, updatedApp *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("UpdateOAuthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
updatedApp.Id = oldApp.Id
updatedApp.CreatorId = oldApp.CreatorId
updatedApp.CreateAt = oldApp.CreateAt
updatedApp.ClientSecret = oldApp.ClientSecret
oauthApp, err := a.Srv().Store().OAuth().UpdateApp(updatedApp)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("UpdateOAuthApp", "app.oauth.update_app.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpdateOAuthApp", "app.oauth.update_app.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return oauthApp, nil
}
func (a *App) DeleteOAuthApp(appID string) *model.AppError {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return model.NewAppError("DeleteOAuthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
if err := a.Srv().Store().OAuth().DeleteApp(appID); err != nil {
return model.NewAppError("DeleteOAuthApp", "app.oauth.delete_app.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().InvalidateAllCaches(); err != nil {
mlog.Warn("error in invalidating cache", mlog.Err(err))
}
return nil
}
func (a *App) GetOAuthApps(page, perPage int) ([]*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthApps", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
oauthApps, err := a.Srv().Store().OAuth().GetApps(page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetOAuthApps", "app.oauth.get_apps.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return oauthApps, nil
}
func (a *App) GetOAuthAppsByCreator(userID string, page, perPage int) ([]*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthAppsByUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
oauthApps, err := a.Srv().Store().OAuth().GetAppByUser(userID, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetOAuthAppsByCreator", "app.oauth.get_app_by_user.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return oauthApps, nil
}
func (a *App) GetOAuthImplicitRedirect(userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError) {
session, err := a.GetOAuthAccessTokenForImplicitFlow(userID, authRequest)
if err != nil {
return "", err
}
values := &url.Values{}
values.Add("access_token", session.Token)
values.Add("token_type", "bearer")
values.Add("expires_in", strconv.FormatInt((session.ExpiresAt-model.GetMillis())/1000, 10))
values.Add("scope", authRequest.Scope)
values.Add("state", authRequest.State)
return fmt.Sprintf("%s#%s", authRequest.RedirectURI, values.Encode()), nil
}
func (a *App) GetOAuthCodeRedirect(userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError) {
authData := &model.AuthData{UserId: userID, ClientId: authRequest.ClientId, CreateAt: model.GetMillis(), RedirectUri: authRequest.RedirectURI, State: authRequest.State, Scope: authRequest.Scope}
authData.Code = model.NewId() + model.NewId()
// parse authRequest.RedirectURI to handle query parameters see: https://mattermost.atlassian.net/browse/MM-46216
uri, err := url.Parse(authRequest.RedirectURI)
if err != nil {
return authRequest.RedirectURI + "?error=redirect_uri_parse_error&state=" + authRequest.State, nil
}
queryParams := uri.Query()
if _, err := a.Srv().Store().OAuth().SaveAuthData(authData); err != nil {
queryParams.Set("error", "server_error")
queryParams.Set("state", authRequest.State)
uri.RawQuery = queryParams.Encode()
return uri.String(), nil
}
queryParams.Set("code", url.QueryEscape(authData.Code))
queryParams.Set("state", url.QueryEscape(authData.State))
uri.RawQuery = queryParams.Encode()
return uri.String(), nil
}
func (a *App) AllowOAuthAppAccessToUser(userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return "", model.NewAppError("AllowOAuthAppAccessToUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
if authRequest.Scope == "" {
authRequest.Scope = model.DefaultScope
}
oauthApp, nErr := a.Srv().Store().OAuth().GetApp(authRequest.ClientId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return "", model.NewAppError("AllowOAuthAppAccessToUser", "app.oauth.get_app.find.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return "", model.NewAppError("AllowOAuthAppAccessToUser", "app.oauth.get_app.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if !oauthApp.IsValidRedirectURL(authRequest.RedirectURI) {
return "", model.NewAppError("AllowOAuthAppAccessToUser", "api.oauth.allow_oauth.redirect_callback.app_error", nil, "", http.StatusBadRequest)
}
var redirectURI string
var err *model.AppError
switch authRequest.ResponseType {
case model.AuthCodeResponseType:
redirectURI, err = a.GetOAuthCodeRedirect(userID, authRequest)
case model.ImplicitResponseType:
redirectURI, err = a.GetOAuthImplicitRedirect(userID, authRequest)
default:
return authRequest.RedirectURI + "?error=unsupported_response_type&state=" + authRequest.State, nil
}
if err != nil {
mlog.Warn("error getting oauth redirect uri", mlog.Err(err))
return authRequest.RedirectURI + "?error=server_error&state=" + authRequest.State, nil
}
// This saves the OAuth2 app as authorized
authorizedApp := model.Preference{
UserId: userID,
Category: model.PreferenceCategoryAuthorizedOAuthApp,
Name: authRequest.ClientId,
Value: authRequest.Scope,
}
if nErr := a.Srv().Store().Preference().Save(model.Preferences{authorizedApp}); nErr != nil {
mlog.Warn("error saving store preference", mlog.Err(nErr))
return authRequest.RedirectURI + "?error=server_error&state=" + authRequest.State, nil
}
return redirectURI, nil
}
func (a *App) GetOAuthAccessTokenForImplicitFlow(userID string, authRequest *model.AuthorizeRequest) (*model.Session, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.disabled.app_error", nil, "", http.StatusNotImplemented)
}
oauthApp, err := a.GetOAuthApp(authRequest.ClientId)
if err != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "", http.StatusNotFound)
}
user, err := a.GetUser(userID)
if err != nil {
return nil, err
}
session, err := a.newSession(oauthApp, user)
if err != nil {
return nil, err
}
accessData := &model.AccessData{ClientId: authRequest.ClientId, UserId: user.Id, Token: session.Token, RefreshToken: "", RedirectUri: authRequest.RedirectURI, ExpiresAt: session.ExpiresAt, Scope: authRequest.Scope}
if _, err := a.Srv().Store().OAuth().SaveAccessData(accessData); err != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_saving.app_error", nil, "", http.StatusInternalServerError)
}
return session, nil
}
func (a *App) GetOAuthAccessTokenForCodeFlow(clientId, grantType, redirectURI, code, secret, refreshToken string) (*model.AccessResponse, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.disabled.app_error", nil, "", http.StatusNotImplemented)
}
oauthApp, nErr := a.Srv().Store().OAuth().GetApp(clientId)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "", http.StatusNotFound)
}
if oauthApp.ClientSecret != secret {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "", http.StatusForbidden)
}
var accessData *model.AccessData
var accessRsp *model.AccessResponse
var user *model.User
if grantType == model.AccessTokenGrantType {
var authData *model.AuthData
authData, nErr = a.Srv().Store().OAuth().GetAuthData(code)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "", http.StatusBadRequest)
}
if authData.IsExpired() {
if nErr = a.Srv().Store().OAuth().RemoveAuthData(authData.Code); nErr != nil {
mlog.Warn("unable to remove auth data", mlog.Err(nErr))
}
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "", http.StatusForbidden)
}
if authData.RedirectUri != redirectURI {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.redirect_uri.app_error", nil, "", http.StatusBadRequest)
}
user, nErr = a.Srv().Store().User().Get(context.Background(), authData.UserId)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "", http.StatusNotFound)
}
accessData, nErr = a.Srv().Store().OAuth().GetPreviousAccessData(user.Id, clientId)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal.app_error", nil, "", http.StatusBadRequest)
}
if accessData != nil {
if accessData.IsExpired() {
var access *model.AccessResponse
access, err := a.newSessionUpdateToken(oauthApp, accessData, user)
if err != nil {
return nil, err
}
accessRsp = access
} else {
// Return the same token and no need to create a new session
accessRsp = &model.AccessResponse{
AccessToken: accessData.Token,
TokenType: model.AccessTokenType,
RefreshToken: accessData.RefreshToken,
ExpiresInSeconds: int32((accessData.ExpiresAt - model.GetMillis()) / 1000),
}
}
} else {
var session *model.Session
// Create a new session and return new access token
session, err := a.newSession(oauthApp, user)
if err != nil {
return nil, err
}
accessData = &model.AccessData{ClientId: clientId, UserId: user.Id, Token: session.Token, RefreshToken: model.NewId(), RedirectUri: redirectURI, ExpiresAt: session.ExpiresAt, Scope: authData.Scope}
if _, nErr = a.Srv().Store().OAuth().SaveAccessData(accessData); nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_saving.app_error", nil, "", http.StatusInternalServerError)
}
accessRsp = &model.AccessResponse{
AccessToken: session.Token,
TokenType: model.AccessTokenType,
RefreshToken: accessData.RefreshToken,
ExpiresInSeconds: int32(*a.Config().ServiceSettings.SessionLengthSSOInHours * 60 * 60),
}
}
if nErr = a.Srv().Store().OAuth().RemoveAuthData(authData.Code); nErr != nil {
mlog.Warn("unable to remove auth data", mlog.Err(nErr))
}
} else {
// When grantType is refresh_token
accessData, nErr = a.Srv().Store().OAuth().GetAccessDataByRefreshToken(refreshToken)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.refresh_token.app_error", nil, "", http.StatusNotFound)
}
user, nErr := a.Srv().Store().User().Get(context.Background(), accessData.UserId)
if nErr != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "", http.StatusNotFound)
}
access, err := a.newSessionUpdateToken(oauthApp, accessData, user)
if err != nil {
return nil, err
}
accessRsp = access
}
return accessRsp, nil
}
func (a *App) newSession(app *model.OAuthApp, user *model.User) (*model.Session, *model.AppError) {
// Set new token an session
session := &model.Session{UserId: user.Id, Roles: user.Roles, IsOAuth: true}
session.GenerateCSRF()
a.ch.srv.platform.SetSessionExpireInHours(session, *a.Config().ServiceSettings.SessionLengthSSOInHours)
session.AddProp(model.SessionPropPlatform, app.Name)
session.AddProp(model.SessionPropOAuthAppID, app.Id)
session.AddProp(model.SessionPropMattermostAppID, app.MattermostAppID)
session.AddProp(model.SessionPropOs, "OAuth2")
session.AddProp(model.SessionPropBrowser, "OAuth2")
session, err := a.Srv().Store().Session().Save(session)
if err != nil {
return nil, model.NewAppError("newSession", "api.oauth.get_access_token.internal_session.app_error", nil, "", http.StatusInternalServerError)
}
a.ch.srv.platform.AddSessionToCache(session)
return session, nil
}
func (a *App) newSessionUpdateToken(app *model.OAuthApp, accessData *model.AccessData, user *model.User) (*model.AccessResponse, *model.AppError) {
// Remove the previous session
if err := a.Srv().Store().Session().Remove(accessData.Token); err != nil {
mlog.Warn("error removing access data token from session", mlog.Err(err))
}
session, err := a.newSession(app, user)
if err != nil {
return nil, err
}
accessData.Token = session.Token
accessData.RefreshToken = model.NewId()
accessData.ExpiresAt = session.ExpiresAt
if _, err := a.Srv().Store().OAuth().UpdateAccessData(accessData); err != nil {
return nil, model.NewAppError("newSessionUpdateToken", "web.get_access_token.internal_saving.app_error", nil, "", http.StatusInternalServerError)
}
accessRsp := &model.AccessResponse{
AccessToken: session.Token,
RefreshToken: accessData.RefreshToken,
TokenType: model.AccessTokenType,
ExpiresInSeconds: int32(*a.Config().ServiceSettings.SessionLengthSSOInHours * 60 * 60),
}
return accessRsp, nil
}
func (a *App) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service, teamID, action, redirectTo, loginHint string, isMobile bool) (string, *model.AppError) {
stateProps := map[string]string{}
stateProps["action"] = action
if teamID != "" {
stateProps["team_id"] = teamID
}
if redirectTo != "" {
stateProps["redirect_to"] = redirectTo
}
stateProps[model.UserAuthServiceIsMobile] = strconv.FormatBool(isMobile)
authURL, err := a.GetAuthorizationCode(w, r, service, stateProps, loginHint)
if err != nil {
return "", err
}
return authURL, nil
}
func (a *App) GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service, teamID string) (string, *model.AppError) {
stateProps := map[string]string{}
stateProps["action"] = model.OAuthActionSignup
if teamID != "" {
stateProps["team_id"] = teamID
}
authURL, err := a.GetAuthorizationCode(w, r, service, stateProps, "")
if err != nil {
return "", err
}
return authURL, nil
}
func (a *App) GetAuthorizedAppsForUser(userID string, page, perPage int) ([]*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetAuthorizedAppsForUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
apps, err := a.Srv().Store().OAuth().GetAuthorizedApps(userID, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetAuthorizedAppsForUser", "app.oauth.get_apps.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for k, a := range apps {
a.Sanitize()
apps[k] = a
}
return apps, nil
}
func (a *App) DeauthorizeOAuthAppForUser(userID, appID string) *model.AppError {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return model.NewAppError("DeauthorizeOAuthAppForUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
// Revoke app sessions
accessData, err := a.Srv().Store().OAuth().GetAccessDataByUserForApp(userID, appID)
if err != nil {
return model.NewAppError("DeauthorizeOAuthAppForUser", "app.oauth.get_access_data_by_user_for_app.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, ad := range accessData {
if err := a.RevokeAccessToken(ad.Token); err != nil {
return err
}
if err := a.Srv().Store().OAuth().RemoveAccessData(ad.Token); err != nil {
return model.NewAppError("DeauthorizeOAuthAppForUser", "app.oauth.remove_access_data.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if err := a.Srv().Store().OAuth().RemoveAuthDataByClientId(appID, userID); err != nil {
return model.NewAppError("DeauthorizeOAuthAppForUser", "app.oauth.remove_auth_data_by_client_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Deauthorize the app
if err := a.Srv().Store().Preference().Delete(userID, model.PreferenceCategoryAuthorizedOAuthApp, appID); err != nil {
return model.NewAppError("DeauthorizeOAuthAppForUser", "app.preference.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) RegenerateOAuthAppSecret(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("RegenerateOAuthAppSecret", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
app.ClientSecret = model.NewId()
if _, err := a.Srv().Store().OAuth().UpdateApp(app); err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("RegenerateOAuthAppSecret", "app.oauth.update_app.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("RegenerateOAuthAppSecret", "app.oauth.update_app.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return app, nil
}
func (a *App) RevokeAccessToken(token string) *model.AppError {
if err := a.ch.srv.platform.RevokeAccessToken(token); err != nil {
switch {
case errors.Is(err, platform.GetTokenError):
return model.NewAppError("RevokeAccessToken", "api.oauth.revoke_access_token.get.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.Is(err, platform.DeleteTokenError):
return model.NewAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_token.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
case errors.Is(err, platform.DeleteSessionError):
return model.NewAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_session.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return nil
}
func (a *App) CompleteOAuth(c *request.Context, service string, body io.ReadCloser, teamID string, props map[string]string, tokenUser *model.User) (*model.User, *model.AppError) {
defer body.Close()
action := props["action"]
switch action {
case model.OAuthActionSignup:
return a.CreateOAuthUser(c, service, body, teamID, tokenUser)
case model.OAuthActionLogin:
return a.LoginByOAuth(c, service, body, teamID, tokenUser)
case model.OAuthActionEmailToSSO:
return a.CompleteSwitchWithOAuth(service, body, props["email"], tokenUser)
case model.OAuthActionSSOToEmail:
return a.LoginByOAuth(c, service, body, teamID, tokenUser)
default:
return a.LoginByOAuth(c, service, body, teamID, tokenUser)
}
}
func (a *App) getSSOProvider(service string) (einterfaces.OAuthProvider, *model.AppError) {
sso := a.Config().GetSSOService(service)
if sso == nil || !*sso.Enable {
return nil, model.NewAppError("getSSOProvider", "api.user.authorize_oauth_user.unsupported.app_error", nil, "service="+service, http.StatusNotImplemented)
}
providerType := service
if strings.Contains(*sso.Scope, OpenIDScope) {
providerType = model.ServiceOpenid
}
provider := einterfaces.GetOAuthProvider(providerType)
if provider == nil {
return nil, model.NewAppError("getSSOProvider", "api.user.login_by_oauth.not_available.app_error",
map[string]any{"Service": strings.Title(service)}, "", http.StatusNotImplemented)
}
return provider, nil
}
func (a *App) LoginByOAuth(c *request.Context, service string, userData io.Reader, teamID string, tokenUser *model.User) (*model.User, *model.AppError) {
provider, e := a.getSSOProvider(service)
if e != nil {
return nil, e
}
buf := bytes.Buffer{}
if _, err := buf.ReadFrom(userData); err != nil {
return nil, model.NewAppError("LoginByOAuth2", "api.user.login_by_oauth.parse.app_error",
map[string]any{"Service": service}, "", http.StatusBadRequest)
}
authUser, err1 := provider.GetUserFromJSON(bytes.NewReader(buf.Bytes()), tokenUser)
if err1 != nil {
return nil, model.NewAppError("LoginByOAuth", "api.user.login_by_oauth.parse.app_error",
map[string]any{"Service": service}, "", http.StatusBadRequest).Wrap(err1)
}
if *authUser.AuthData == "" {
return nil, model.NewAppError("LoginByOAuth3", "api.user.login_by_oauth.parse.app_error",
map[string]any{"Service": service}, "", http.StatusBadRequest)
}
user, err := a.GetUserByAuth(model.NewString(*authUser.AuthData), service)
if err != nil {
if err.Id == MissingAuthAccountError {
user, err = a.CreateOAuthUser(c, service, bytes.NewReader(buf.Bytes()), teamID, tokenUser)
} else {
return nil, err
}
} else {
// OAuth doesn't run through CheckUserPreflightAuthenticationCriteria, so prevent bot login
// here manually. Technically, the auth data above will fail to match a bot in the first
// place, but explicit is always better.
if user.IsBot {
return nil, model.NewAppError("loginByOAuth", "api.user.login_by_oauth.bot_login_forbidden.app_error", nil, "", http.StatusForbidden)
}
if err = a.UpdateOAuthUserAttrs(bytes.NewReader(buf.Bytes()), user, provider, service, tokenUser); err != nil {
return nil, err
}
if teamID != "" {
err = a.AddUserToTeamByTeamId(c, teamID, user)
}
}
if err != nil {
return nil, err
}
return user, nil
}
func (a *App) CompleteSwitchWithOAuth(service string, userData io.Reader, email string, tokenUser *model.User) (*model.User, *model.AppError) {
provider, e := a.getSSOProvider(service)
if e != nil {
return nil, e
}
if email == "" {
return nil, model.NewAppError("CompleteSwitchWithOAuth", "api.user.complete_switch_with_oauth.blank_email.app_error", nil, "", http.StatusBadRequest)
}
ssoUser, err1 := provider.GetUserFromJSON(userData, tokenUser)
if err1 != nil {
return nil, model.NewAppError("CompleteSwitchWithOAuth", "api.user.complete_switch_with_oauth.parse.app_error",
map[string]any{"Service": service}, "", http.StatusBadRequest).Wrap(err1)
}
if *ssoUser.AuthData == "" {
return nil, model.NewAppError("CompleteSwitchWithOAuth", "api.user.complete_switch_with_oauth.parse.app_error",
map[string]any{"Service": service}, "", http.StatusBadRequest)
}
user, nErr := a.Srv().Store().User().GetByEmail(email)
if nErr != nil {
return nil, model.NewAppError("CompleteSwitchWithOAuth", MissingAccountError, nil, "", http.StatusInternalServerError).Wrap(nErr)
}
if err := a.RevokeAllSessions(user.Id); err != nil {
return nil, err
}
if _, nErr := a.Srv().Store().User().UpdateAuthData(user.Id, service, ssoUser.AuthData, ssoUser.Email, true); nErr != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(nErr, &invErr):
return nil, model.NewAppError("importUser", "app.user.update_auth_data.email_exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
default:
return nil, model.NewAppError("importUser", "app.user.update_auth_data.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
a.Srv().Go(func() {
if err := a.Srv().EmailService.SendSignInChangeEmail(user.Email, strings.Title(service)+" SSO", user.Locale, a.GetSiteURL()); err != nil {
mlog.Error("error sending signin change email", mlog.Err(err))
}
})
return user, nil
}
func (a *App) CreateOAuthStateToken(extra string) (*model.Token, *model.AppError) {
token := model.NewToken(model.TokenTypeOAuth, extra)
if err := a.Srv().Store().Token().Save(token); err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CreateOAuthStateToken", "app.recover.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return token, nil
}
func (a *App) GetOAuthStateToken(token string) (*model.Token, *model.AppError) {
mToken, err := a.Srv().Store().Token().GetByToken(token)
if err != nil {
return nil, model.NewAppError("GetOAuthStateToken", "api.oauth.invalid_state_token.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if mToken.Type != model.TokenTypeOAuth {
return nil, model.NewAppError("GetOAuthStateToken", "api.oauth.invalid_state_token.app_error", nil, "", http.StatusBadRequest)
}
return mToken, nil
}
func (a *App) GetAuthorizationCode(w http.ResponseWriter, r *http.Request, service string, props map[string]string, loginHint string) (string, *model.AppError) {
provider, e := a.getSSOProvider(service)
if e != nil {
return "", e
}
sso, e2 := provider.GetSSOSettings(a.Config(), service)
if e2 != nil {
return "", model.NewAppError("GetAuthorizationCode.GetSSOSettings", "api.user.get_authorization_code.endpoint.app_error", nil, "", http.StatusNotImplemented).Wrap(e2)
}
secure := false
if GetProtocol(r) == "https" {
secure = true
}
cookieValue := model.NewId()
subpath, _ := utils.GetSubpathFromConfig(a.Config())
expiresAt := time.Unix(model.GetMillis()/1000+int64(OAuthCookieMaxAgeSeconds), 0)
oauthCookie := &http.Cookie{
Name: CookieOAuth,
Value: cookieValue,
Path: subpath,
MaxAge: OAuthCookieMaxAgeSeconds,
Expires: expiresAt,
HttpOnly: true,
Secure: secure,
}
http.SetCookie(w, oauthCookie)
clientId := *sso.Id
endpoint := *sso.AuthEndpoint
scope := *sso.Scope
tokenExtra := generateOAuthStateTokenExtra(props["email"], props["action"], cookieValue)
stateToken, err := a.CreateOAuthStateToken(tokenExtra)
if err != nil {
return "", err
}
props["token"] = stateToken.Token
state := b64.StdEncoding.EncodeToString([]byte(model.MapToJSON(props)))
siteURL := a.GetSiteURL()
if strings.TrimSpace(siteURL) == "" {
siteURL = GetProtocol(r) + "://" + r.Host
}
redirectURI := siteURL + "/signup/" + service + "/complete"
authURL := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectURI) + "&state=" + url.QueryEscape(state)
if scope != "" {
authURL += "&scope=" + utils.URLEncode(scope)
}
if loginHint != "" {
authURL += "&login_hint=" + utils.URLEncode(loginHint)
}
return authURL, nil
}
func (a *App) AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service, code, state, redirectURI string) (io.ReadCloser, string, map[string]string, *model.User, *model.AppError) {
provider, e := a.getSSOProvider(service)
if e != nil {
return nil, "", nil, nil, e
}
sso, e2 := provider.GetSSOSettings(a.Config(), service)
if e2 != nil {
return nil, "", nil, nil, model.NewAppError("AuthorizeOAuthUser.GetSSOSettings", "api.user.get_authorization_code.endpoint.app_error", nil, "", http.StatusNotImplemented).Wrap(e2)
}
b, strErr := b64.StdEncoding.DecodeString(state)
if strErr != nil {
return nil, "", nil, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest).Wrap(strErr)
}
stateStr := string(b)
stateProps := model.MapFromJSON(strings.NewReader(stateStr))
expectedToken, appErr := a.GetOAuthStateToken(stateProps["token"])
if appErr != nil {
return nil, "", stateProps, nil, appErr
}
stateEmail := stateProps["email"]
stateAction := stateProps["action"]
if stateAction == model.OAuthActionEmailToSSO && stateEmail == "" {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest)
}
cookie, cookieErr := r.Cookie(CookieOAuth)
if cookieErr != nil {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest)
}
expectedTokenExtra := generateOAuthStateTokenExtra(stateEmail, stateAction, cookie.Value)
if expectedTokenExtra != expectedToken.Extra {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusBadRequest)
}
appErr = a.DeleteToken(expectedToken)
if appErr != nil {
mlog.Warn("error deleting token", mlog.Err(appErr))
}
subpath, _ := utils.GetSubpathFromConfig(a.Config())
httpCookie := &http.Cookie{
Name: CookieOAuth,
Value: "",
Path: subpath,
MaxAge: -1,
HttpOnly: true,
}
http.SetCookie(w, httpCookie)
teamID := stateProps["team_id"]
p := url.Values{}
p.Set("client_id", *sso.Id)
p.Set("client_secret", *sso.Secret)
p.Set("code", code)
p.Set("grant_type", model.AccessTokenGrantType)
p.Set("redirect_uri", redirectURI)
req, requestErr := http.NewRequest("POST", *sso.TokenEndpoint, strings.NewReader(p.Encode()))
if requestErr != nil {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, "", http.StatusInternalServerError).Wrap(requestErr)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
resp, err := a.HTTPService().MakeClient(true).Do(req)
if err != nil {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
defer resp.Body.Close()
var buf bytes.Buffer
tee := io.TeeReader(resp.Body, &buf)
var ar *model.AccessResponse
err = json.NewDecoder(tee).Decode(&ar)
if err != nil || resp.StatusCode != http.StatusOK {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_response.app_error", nil, fmt.Sprintf("response_body=%s, status_code=%d, error=%v", buf.String(), resp.StatusCode, err), http.StatusInternalServerError).Wrap(err)
}
if strings.ToLower(ar.TokenType) != model.AccessTokenType {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_token.app_error", nil, "token_type="+ar.TokenType+", response_body="+buf.String(), http.StatusInternalServerError)
}
if ar.AccessToken == "" {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.missing.app_error", nil, "response_body="+buf.String(), http.StatusInternalServerError)
}
p = url.Values{}
p.Set("access_token", ar.AccessToken)
var userFromToken *model.User
if ar.IdToken != "" {
userFromToken, err = provider.GetUserFromIdToken(ar.IdToken)
if err != nil {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
req, requestErr = http.NewRequest("GET", *sso.UserAPIEndpoint, strings.NewReader(""))
if requestErr != nil {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.service.app_error", map[string]any{"Service": service}, "", http.StatusInternalServerError).Wrap(requestErr)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+ar.AccessToken)
resp, err = a.HTTPService().MakeClient(true).Do(req)
if err != nil {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.service.app_error", map[string]any{"Service": service}, "", http.StatusInternalServerError).Wrap(err)
} else if resp.StatusCode != http.StatusOK {
defer resp.Body.Close()
// Ignore the error below because the resulting string will just be the empty string if bodyBytes is nil
bodyBytes, _ := io.ReadAll(resp.Body)
bodyString := string(bodyBytes)
mlog.Error("Error getting OAuth user", mlog.Int("response", resp.StatusCode), mlog.String("body_string", bodyString))
if service == model.ServiceGitlab && resp.StatusCode == http.StatusForbidden && strings.Contains(bodyString, "Terms of Service") {
url, err := url.Parse(*sso.UserAPIEndpoint)
if err != nil {
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(errors.Wrapf(err, "error parsing %s", *sso.UserAPIEndpoint))
}
// Return a nicer error when the user hasn't accepted GitLab's terms of service
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "oauth.gitlab.tos.error", map[string]any{"URL": url.Hostname()}, "", http.StatusBadRequest)
}
return nil, "", stateProps, nil, model.NewAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.response.app_error", nil, "response_body="+bodyString, http.StatusInternalServerError)
}
// Note that resp.Body is not closed here, so it must be closed by the caller
return resp.Body, teamID, stateProps, userFromToken, nil
}
func (a *App) SwitchEmailToOAuth(w http.ResponseWriter, r *http.Request, email, password, code, service string) (string, *model.AppError) {
if a.Srv().License() != nil && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
return "", model.NewAppError("emailToOAuth", "api.user.email_to_oauth.not_available.app_error", nil, "", http.StatusForbidden)
}
user, err := a.GetUserByEmail(email)
if err != nil {
return "", err
}
if err = a.CheckPasswordAndAllCriteria(user, password, code); err != nil {
return "", err
}
stateProps := map[string]string{}
stateProps["action"] = model.OAuthActionEmailToSSO
stateProps["email"] = email
if service == model.UserAuthServiceSaml {
return a.GetSiteURL() + "/login/sso/saml?action=" + model.OAuthActionEmailToSSO + "&email=" + utils.URLEncode(email), nil
}
authURL, err := a.GetAuthorizationCode(w, r, service, stateProps, "")
if err != nil {
return "", err
}
return authURL, nil
}
func (a *App) SwitchOAuthToEmail(email, password, requesterId string) (string, *model.AppError) {
if a.Srv().License() != nil && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
return "", model.NewAppError("oauthToEmail", "api.user.oauth_to_email.not_available.app_error", nil, "", http.StatusForbidden)
}
user, err := a.GetUserByEmail(email)
if err != nil {
return "", err
}
if user.Id != requesterId {
return "", model.NewAppError("SwitchOAuthToEmail", "api.user.oauth_to_email.context.app_error", nil, "", http.StatusForbidden)
}
if err := a.UpdatePassword(user, password); err != nil {
return "", err
}
T := i18n.GetUserTranslations(user.Locale)
a.Srv().Go(func() {
if err := a.Srv().EmailService.SendSignInChangeEmail(user.Email, T("api.templates.signin_change_email.body.method_email"), user.Locale, a.GetSiteURL()); err != nil {
mlog.Error("error sending signin change email", mlog.Err(err))
}
})
if err := a.RevokeAllSessions(requesterId); err != nil {
return "", err
}
return "/login?extra=signin_change", nil
}
func generateOAuthStateTokenExtra(email, action, cookie string) string {
return email + ":" + action + ":" + cookie
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) markAdminOnboardingComplete(c *request.Context) *model.AppError {
firstAdminCompleteSetupObj := model.System{
Name: model.SystemFirstAdminSetupComplete,
Value: "true",
}
if err := a.Srv().Store().System().SaveOrUpdate(&firstAdminCompleteSetupObj); err != nil {
return model.NewAppError("setFirstAdminCompleteSetup", "api.error_set_first_admin_complete_setup", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnboardingRequest) *model.AppError {
pluginsEnvironment := a.Channels().GetPluginsEnvironment()
if pluginsEnvironment == nil {
return a.markAdminOnboardingComplete(c)
}
pluginContext := pluginContext(c)
for _, pluginID := range request.InstallPlugins {
go func(id string) {
installRequest := &model.InstallMarketplacePluginRequest{
Id: id,
}
_, appErr := a.Channels().InstallMarketplacePlugin(installRequest)
if appErr != nil {
mlog.Error("Failed to install plugin for onboarding", mlog.String("id", id), mlog.Err(appErr))
return
}
appErr = a.EnablePlugin(id)
if appErr != nil {
mlog.Error("Failed to enable plugin for onboarding", mlog.String("id", id), mlog.Err(appErr))
return
}
hooks, err := a.ch.HooksForPluginOrProduct(id)
if err != nil {
mlog.Warn("Getting hooks for plugin failed", mlog.String("plugin_id", id), mlog.Err(err))
return
}
event := model.OnInstallEvent{
UserId: c.Session().UserId,
}
if err = hooks.OnInstall(pluginContext, event); err != nil {
mlog.Error("Plugin OnInstall hook failed", mlog.String("plugin_id", id), mlog.Err(err))
}
}(pluginID)
}
return a.markAdminOnboardingComplete(c)
}
func (a *App) GetOnboarding() (*model.System, *model.AppError) {
firstAdminCompleteSetupObj, err := a.Srv().Store().System().GetByName(model.SystemFirstAdminSetupComplete)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return &model.System{
Name: model.SystemFirstAdminSetupComplete,
Value: "false",
}, nil
default:
return nil, model.NewAppError("getFirstAdminCompleteSetup", "api.error_get_first_admin_complete_setup", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return firstAdminCompleteSetupObj, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"html"
"io"
"net/url"
"time"
"github.com/dyatlov/go-opengraph/opengraph"
"golang.org/x/net/html/charset"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
MaxOpenGraphResponseSize = 1024 * 1024 * 50
openGraphMetadataCacheSize = 10000
)
func (a *App) GetOpenGraphMetadata(requestURL string) ([]byte, error) {
var ogJSONGeneric []byte
err := a.Srv().openGraphDataCache.Get(requestURL, &ogJSONGeneric)
if err == nil {
return ogJSONGeneric, nil
}
res, err := a.HTTPService().MakeClient(false).Get(requestURL)
if err != nil {
return nil, err
}
defer res.Body.Close()
graph := a.parseOpenGraphMetadata(requestURL, res.Body, res.Header.Get("Content-Type"))
ogJSON, err := graph.ToJSON()
if err != nil {
return nil, err
}
err = a.Srv().openGraphDataCache.SetWithExpiry(requestURL, ogJSON, 1*time.Hour)
if err != nil {
return nil, err
}
return ogJSON, nil
}
func (a *App) parseOpenGraphMetadata(requestURL string, body io.Reader, contentType string) *opengraph.OpenGraph {
og := opengraph.NewOpenGraph()
body = forceHTMLEncodingToUTF8(io.LimitReader(body, MaxOpenGraphResponseSize), contentType)
if err := og.ProcessHTML(body); err != nil {
mlog.Warn("parseOpenGraphMetadata processing failed", mlog.String("requestURL", requestURL), mlog.Err(err))
}
makeOpenGraphURLsAbsolute(og, requestURL)
openGraphDecodeHTMLEntities(og)
// If image proxy enabled modify open graph data to feed though proxy
if toProxyURL := a.ImageProxyAdder(); toProxyURL != nil {
og = openGraphDataWithProxyAddedToImageURLs(og, toProxyURL)
}
// The URL should be the link the user provided in their message, not a redirected one.
if og.URL != "" {
og.URL = requestURL
}
return og
}
func forceHTMLEncodingToUTF8(body io.Reader, contentType string) io.Reader {
r, err := charset.NewReader(body, contentType)
if err != nil {
mlog.Warn("forceHTMLEncodingToUTF8 failed to convert", mlog.String("contentType", contentType), mlog.Err(err))
return body
}
return r
}
func makeOpenGraphURLsAbsolute(og *opengraph.OpenGraph, requestURL string) {
parsedRequestURL, err := url.Parse(requestURL)
if err != nil {
mlog.Warn("makeOpenGraphURLsAbsolute failed to parse url", mlog.String("requestURL", requestURL), mlog.Err(err))
return
}
makeURLAbsolute := func(resultURL string) string {
if resultURL == "" {
return resultURL
}
parsedResultURL, err := url.Parse(resultURL)
if err != nil {
mlog.Warn("makeOpenGraphURLsAbsolute failed to parse result", mlog.String("requestURL", requestURL), mlog.Err(err))
return resultURL
}
if parsedResultURL.IsAbs() {
return resultURL
}
return parsedRequestURL.ResolveReference(parsedResultURL).String()
}
og.URL = makeURLAbsolute(og.URL)
for _, image := range og.Images {
image.URL = makeURLAbsolute(image.URL)
image.SecureURL = makeURLAbsolute(image.SecureURL)
}
for _, audio := range og.Audios {
audio.URL = makeURLAbsolute(audio.URL)
audio.SecureURL = makeURLAbsolute(audio.SecureURL)
}
for _, video := range og.Videos {
video.URL = makeURLAbsolute(video.URL)
video.SecureURL = makeURLAbsolute(video.SecureURL)
}
}
func openGraphDataWithProxyAddedToImageURLs(ogdata *opengraph.OpenGraph, toProxyURL func(string) string) *opengraph.OpenGraph {
for _, image := range ogdata.Images {
var url string
if image.SecureURL != "" {
url = image.SecureURL
} else {
url = image.URL
}
image.URL = ""
image.SecureURL = toProxyURL(url)
}
return ogdata
}
func openGraphDecodeHTMLEntities(og *opengraph.OpenGraph) {
og.Title = html.UnescapeString(og.Title)
og.Description = html.UnescapeString(og.Description)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Code generated by "make app-layers"
// DO NOT EDIT
package opentracing
import (
"archive/zip"
"bytes"
"context"
"crypto/ecdsa"
"io"
"mime/multipart"
"net/http"
"net/url"
"reflect"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/app/worktemplates"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/httpservice"
"github.com/mattermost/mattermost-server/v6/server/platform/services/imageproxy"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
"github.com/mattermost/mattermost-server/v6/server/platform/services/timezones"
"github.com/mattermost/mattermost-server/v6/server/platform/services/tracing"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/opentracing/opentracing-go/ext"
spanlog "github.com/opentracing/opentracing-go/log"
)
type OpenTracingAppLayer struct {
app app.AppIface
srv *app.Server
log *mlog.Logger
notificationsLog *mlog.Logger
accountMigration einterfaces.AccountMigrationInterface
cluster einterfaces.ClusterInterface
compliance einterfaces.ComplianceInterface
dataRetention einterfaces.DataRetentionInterface
searchEngine *searchengine.Broker
ldap einterfaces.LdapInterface
messageExport einterfaces.MessageExportInterface
metrics einterfaces.MetricsInterface
notification einterfaces.NotificationInterface
saml einterfaces.SamlInterface
httpService httpservice.HTTPService
imageProxy *imageproxy.ImageProxy
timezones *timezones.Timezones
ctx context.Context
}
func (a *OpenTracingAppLayer) ActivateMfa(userID string, token string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ActivateMfa")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ActivateMfa(userID, token)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddChannelMember(c request.CTX, userID string, channel *model.Channel, opts app.ChannelMemberOpts) (*model.ChannelMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddChannelMember")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AddChannelMember(c, userID, channel, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AddChannelsToRetentionPolicy(policyID string, channelIDs []string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddChannelsToRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddChannelsToRetentionPolicy(policyID, channelIDs)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddConfigListener(listener func(*model.Config, *model.Config)) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddConfigListener")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddConfigListener(listener)
return resultVar0
}
func (a *OpenTracingAppLayer) AddCursorIdsForPostList(originalList *model.PostList, afterPost string, beforePost string, since int64, page int, perPage int, collapsedThreads bool) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddCursorIdsForPostList")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.AddCursorIdsForPostList(originalList, afterPost, beforePost, since, page, perPage, collapsedThreads)
}
func (a *OpenTracingAppLayer) AddDirectChannels(c request.CTX, teamID string, user *model.User) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddDirectChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddDirectChannels(c, teamID, user)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddLdapPrivateCertificate(fileData *multipart.FileHeader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddLdapPrivateCertificate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddLdapPrivateCertificate(fileData)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddLdapPublicCertificate(fileData *multipart.FileHeader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddLdapPublicCertificate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddLdapPublicCertificate(fileData)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddPublicKey(name string, key io.Reader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddPublicKey")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddPublicKey(name, key)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddRemoteCluster(rc *model.RemoteCluster) (*model.RemoteCluster, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddRemoteCluster")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AddRemoteCluster(rc)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AddSamlIdpCertificate(fileData *multipart.FileHeader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddSamlIdpCertificate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddSamlIdpCertificate(fileData)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddSamlPrivateCertificate(fileData *multipart.FileHeader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddSamlPrivateCertificate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddSamlPrivateCertificate(fileData)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddSamlPublicCertificate(fileData *multipart.FileHeader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddSamlPublicCertificate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddSamlPublicCertificate(fileData)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddSessionToCache(session *model.Session) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddSessionToCache")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.AddSessionToCache(session)
}
func (a *OpenTracingAppLayer) AddTeamMember(c request.CTX, teamID string, userID string) (*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddTeamMember")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AddTeamMember(c, teamID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AddTeamMemberByInviteId(c *request.Context, inviteId string, userID string) (*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddTeamMemberByInviteId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AddTeamMemberByInviteId(c, inviteId, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AddTeamMemberByToken(c *request.Context, userID string, tokenID string) (*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddTeamMemberByToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AddTeamMemberByToken(c, userID, tokenID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AddTeamMembers(c *request.Context, teamID string, userIDs []string, userRequestorId string, graceful bool) ([]*model.TeamMemberWithError, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddTeamMembers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AddTeamMembers(c, teamID, userIDs, userRequestorId, graceful)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AddTeamsToRetentionPolicy(policyID string, teamIDs []string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddTeamsToRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddTeamsToRetentionPolicy(policyID, teamIDs)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddUserToChannel(c request.CTX, user *model.User, channel *model.Channel, skipTeamMemberIntegrityCheck bool) (*model.ChannelMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddUserToChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AddUserToChannel(c, user, channel, skipTeamMemberIntegrityCheck)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AddUserToTeam(c request.CTX, teamID string, userID string, userRequestorId string) (*model.Team, *model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddUserToTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.AddUserToTeam(c, teamID, userID, userRequestorId)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) AddUserToTeamByInviteId(c *request.Context, inviteId string, userID string) (*model.Team, *model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddUserToTeamByInviteId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.AddUserToTeamByInviteId(c, inviteId, userID)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) AddUserToTeamByTeamId(c *request.Context, teamID string, user *model.User) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddUserToTeamByTeamId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AddUserToTeamByTeamId(c, teamID, user)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AddUserToTeamByToken(c *request.Context, userID string, tokenID string) (*model.Team, *model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddUserToTeamByToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.AddUserToTeamByToken(c, userID, tokenID)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) AdjustImage(file io.Reader) (*bytes.Buffer, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AdjustImage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AdjustImage(file)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AdjustInProductLimits(limits *model.ProductLimits, subscription *model.Subscription) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AdjustInProductLimits")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AdjustInProductLimits(limits, subscription)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AdjustTeamsFromProductLimits(teamLimits *model.TeamsLimits) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AdjustTeamsFromProductLimits")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AdjustTeamsFromProductLimits(teamLimits)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AllowOAuthAppAccessToUser(userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AllowOAuthAppAccessToUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AllowOAuthAppAccessToUser(userID, authRequest)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AppendFile(fr io.Reader, path string) (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AppendFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AppendFile(fr, path)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AsymmetricSigningKey() *ecdsa.PrivateKey {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AsymmetricSigningKey")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AsymmetricSigningKey()
return resultVar0
}
func (a *OpenTracingAppLayer) AttachCloudSessionCookie(c *request.Context, w http.ResponseWriter, r *http.Request) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AttachCloudSessionCookie")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.AttachCloudSessionCookie(c, w, r)
}
func (a *OpenTracingAppLayer) AttachDeviceId(sessionID string, deviceID string, expiresAt int64) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AttachDeviceId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AttachDeviceId(sessionID, deviceID, expiresAt)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AttachSessionCookies(c *request.Context, w http.ResponseWriter, r *http.Request) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AttachSessionCookies")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.AttachSessionCookies(c, w, r)
}
func (a *OpenTracingAppLayer) AuthenticateUserForLogin(c *request.Context, id string, loginId string, password string, mfaToken string, cwsToken string, ldapOnly bool) (user *model.User, err *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AuthenticateUserForLogin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AuthenticateUserForLogin(c, id, loginId, password, mfaToken, cwsToken, ldapOnly)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service string, code string, state string, redirectURI string) (io.ReadCloser, string, map[string]string, *model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AuthorizeOAuthUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2, resultVar3, resultVar4 := a.app.AuthorizeOAuthUser(w, r, service, code, state, redirectURI)
if resultVar4 != nil {
span.LogFields(spanlog.Error(resultVar4))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2, resultVar3, resultVar4
}
func (a *OpenTracingAppLayer) AutocompleteChannels(c request.CTX, userID string, term string) (model.ChannelListWithTeamData, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AutocompleteChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AutocompleteChannels(c, userID, term)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AutocompleteChannelsForSearch(c request.CTX, teamID string, userID string, term string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AutocompleteChannelsForSearch")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AutocompleteChannelsForSearch(c, teamID, userID, term)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AutocompleteChannelsForTeam(c request.CTX, teamID string, userID string, term string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AutocompleteChannelsForTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AutocompleteChannelsForTeam(c, teamID, userID, term)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AutocompleteUsersInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AutocompleteUsersInChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AutocompleteUsersInChannel(teamID, channelID, term, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AutocompleteUsersInTeam(teamID string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInTeam, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AutocompleteUsersInTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.AutocompleteUsersInTeam(teamID, term, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) BuildPostReactions(ctx request.CTX, postID string) (*[]app.ReactionImportData, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.BuildPostReactions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.BuildPostReactions(ctx, postID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) BuildPushNotificationMessage(c request.CTX, contentsConfig string, post *model.Post, user *model.User, channel *model.Channel, channelName string, senderName string, explicitMention bool, channelWideMention bool, replyToThreadType string) (*model.PushNotification, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.BuildPushNotificationMessage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.BuildPushNotificationMessage(c, contentsConfig, post, user, channel, channelName, senderName, explicitMention, channelWideMention, replyToThreadType)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) BuildSamlMetadataObject(idpMetadata []byte) (*model.SamlMetadataResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.BuildSamlMetadataObject")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.BuildSamlMetadataObject(idpMetadata)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) BulkExport(ctx request.CTX, writer io.Writer, outPath string, job *model.Job, opts model.BulkExportOpts) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.BulkExport")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.BulkExport(ctx, writer, outPath, job, opts)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) BulkImport(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int) (*model.AppError, int) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.BulkImport")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.BulkImport(c, jsonlReader, attachmentsReader, dryRun, workers)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) BulkImportWithPath(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int, importPath string) (*model.AppError, int) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.BulkImportWithPath")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.BulkImportWithPath(c, jsonlReader, attachmentsReader, dryRun, workers, importPath)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CanNotifyAdmin(trial bool) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CanNotifyAdmin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CanNotifyAdmin(trial)
return resultVar0
}
func (a *OpenTracingAppLayer) CancelJob(jobId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CancelJob")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CancelJob(jobId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ChannelMembersMinusGroupMembers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.ChannelMembersMinusGroupMembers(channelID, groupIDs, page, perPage)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) ChannelMembersToAdd(since int64, channelID *string, includeRemovedMembers bool) ([]*model.UserChannelIDPair, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ChannelMembersToAdd")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ChannelMembersToAdd(since, channelID, includeRemovedMembers)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ChannelMembersToRemove(teamID *string) ([]*model.ChannelMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ChannelMembersToRemove")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ChannelMembersToRemove(teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) Channels() *app.Channels {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.Channels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.Channels()
return resultVar0
}
func (a *OpenTracingAppLayer) CheckCanInviteToSharedChannel(channelId string) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckCanInviteToSharedChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckCanInviteToSharedChannel(channelId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CheckForClientSideCert(r *http.Request) (string, string, string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckForClientSideCert")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.CheckForClientSideCert(r)
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) CheckIntegrity() <-chan model.IntegrityCheckResult {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckIntegrity")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckIntegrity()
return resultVar0
}
func (a *OpenTracingAppLayer) CheckMandatoryS3Fields(settings *model.FileSettings) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckMandatoryS3Fields")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckMandatoryS3Fields(settings)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CheckPasswordAndAllCriteria(user *model.User, password string, mfaToken string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckPasswordAndAllCriteria")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckPasswordAndAllCriteria(user, password, mfaToken)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CheckPostReminders() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckPostReminders")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.CheckPostReminders()
}
func (a *OpenTracingAppLayer) CheckProviderAttributes(user *model.User, patch *model.UserPatch) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckProviderAttributes")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckProviderAttributes(user, patch)
return resultVar0
}
func (a *OpenTracingAppLayer) CheckRolesExist(roleNames []string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckRolesExist")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckRolesExist(roleNames)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CheckUserAllAuthenticationCriteria(user *model.User, mfaToken string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckUserAllAuthenticationCriteria")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckUserAllAuthenticationCriteria(user, mfaToken)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CheckUserMfa(user *model.User, token string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckUserMfa")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckUserMfa(user, token)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CheckUserPostflightAuthenticationCriteria(user *model.User) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckUserPostflightAuthenticationCriteria")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckUserPostflightAuthenticationCriteria(user)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CheckUserPreflightAuthenticationCriteria(user *model.User, mfaToken string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckUserPreflightAuthenticationCriteria")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckUserPreflightAuthenticationCriteria(user, mfaToken)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CheckWebConn(userID string, connectionID string) *platform.CheckConnResult {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckWebConn")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CheckWebConn(userID, connectionID)
return resultVar0
}
func (a *OpenTracingAppLayer) ClearChannelMembersCache(c request.CTX, channelID string) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ClearChannelMembersCache")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ClearChannelMembersCache(c, channelID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ClearLatestVersionCache() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ClearLatestVersionCache")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.ClearLatestVersionCache()
}
func (a *OpenTracingAppLayer) ClearSessionCacheForAllUsers() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ClearSessionCacheForAllUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.ClearSessionCacheForAllUsers()
}
func (a *OpenTracingAppLayer) ClearSessionCacheForAllUsersSkipClusterSend() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ClearSessionCacheForAllUsersSkipClusterSend")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.ClearSessionCacheForAllUsersSkipClusterSend()
}
func (a *OpenTracingAppLayer) ClearSessionCacheForUser(userID string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ClearSessionCacheForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.ClearSessionCacheForUser(userID)
}
func (a *OpenTracingAppLayer) ClearSessionCacheForUserSkipClusterSend(userID string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ClearSessionCacheForUserSkipClusterSend")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.ClearSessionCacheForUserSkipClusterSend(userID)
}
func (a *OpenTracingAppLayer) ClearTeamMembersCache(teamID string) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ClearTeamMembersCache")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ClearTeamMembersCache(teamID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ClientConfig() map[string]string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ClientConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ClientConfig()
return resultVar0
}
func (a *OpenTracingAppLayer) ClientConfigHash() string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ClientConfigHash")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ClientConfigHash()
return resultVar0
}
func (a *OpenTracingAppLayer) Cloud() einterfaces.CloudInterface {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.Cloud")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.Cloud()
return resultVar0
}
func (a *OpenTracingAppLayer) CommandsForTeam(teamID string) []*model.Command {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CommandsForTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CommandsForTeam(teamID)
return resultVar0
}
func (a *OpenTracingAppLayer) CompareAndDeletePluginKey(pluginID string, key string, oldValue []byte) (bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CompareAndDeletePluginKey")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CompareAndDeletePluginKey(pluginID, key, oldValue)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CompareAndSetPluginKey(pluginID string, key string, oldValue []byte, newValue []byte) (bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CompareAndSetPluginKey")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CompareAndSetPluginKey(pluginID, key, oldValue, newValue)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CompleteOAuth(c *request.Context, service string, body io.ReadCloser, teamID string, props map[string]string, tokenUser *model.User) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CompleteOAuth")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CompleteOAuth(c, service, body, teamID, props, tokenUser)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CompleteOnboarding(c *request.Context, request *model.CompleteOnboardingRequest) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CompleteOnboarding")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CompleteOnboarding(c, request)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CompleteSwitchWithOAuth(service string, userData io.Reader, email string, tokenUser *model.User) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CompleteSwitchWithOAuth")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CompleteSwitchWithOAuth(service, userData, email, tokenUser)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ComputeLastAccessibleFileTime() error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ComputeLastAccessibleFileTime")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ComputeLastAccessibleFileTime()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ComputeLastAccessiblePostTime() error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ComputeLastAccessiblePostTime")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ComputeLastAccessiblePostTime()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) Config() *model.Config {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.Config")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.Config()
return resultVar0
}
func (a *OpenTracingAppLayer) ConvertBotToUser(c request.CTX, bot *model.Bot, userPatch *model.UserPatch, sysadmin bool) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ConvertBotToUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ConvertBotToUser(c, bot, userPatch, sysadmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ConvertUserToBot(user *model.User) (*model.Bot, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ConvertUserToBot")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ConvertUserToBot(user)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CopyFileInfos(userID string, fileIDs []string) ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CopyFileInfos")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CopyFileInfos(userID, fileIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateBot(c request.CTX, bot *model.Bot) (*model.Bot, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateBot")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateBot(c, bot)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateChannel(c request.CTX, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateChannel(c, channel, addMember)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateChannelScheme(c request.CTX, channel *model.Channel) (*model.Scheme, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateChannelScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateChannelScheme(c, channel)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateChannelWithUser(c request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateChannelWithUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateChannelWithUser(c, channel, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateCommand(cmd *model.Command) (*model.Command, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateCommand")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateCommand(cmd)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateCommandPost(c request.CTX, post *model.Post, teamID string, response *model.CommandResponse, skipSlackParsing bool) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateCommandPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
span.SetTag("teamID", teamID)
span.SetTag("skipSlackParsing", skipSlackParsing)
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateCommandPost(c, post, teamID, response, skipSlackParsing)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateCommandWebhook(commandID string, args *model.CommandArgs) (*model.CommandWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateCommandWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateCommandWebhook(commandID, args)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateDefaultMemberships(c *request.Context, params model.CreateDefaultMembershipParams) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateDefaultMemberships")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CreateDefaultMemberships(c, params)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CreateDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateDraft")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateDraft(c, draft, connectionID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateEmoji(c request.CTX, sessionUserId string, emoji *model.Emoji, multiPartImageData *multipart.Form) (*model.Emoji, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateEmoji")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateEmoji(c, sessionUserId, emoji, multiPartImageData)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateGroup(group *model.Group) (*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateGroup(group)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateGroupChannel(c request.CTX, userIDs []string, creatorId string) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateGroupChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateGroupChannel(c, userIDs, creatorId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateGroupWithUserIds(group *model.GroupWithUserIds) (*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateGroupWithUserIds")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateGroupWithUserIds(group)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateGuest(c request.CTX, user *model.User) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateGuest")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateGuest(c, user)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateIncomingWebhookForChannel(creatorId string, channel *model.Channel, hook *model.IncomingWebhook) (*model.IncomingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateIncomingWebhookForChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateIncomingWebhookForChannel(creatorId, channel, hook)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateJob(job *model.Job) (*model.Job, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateJob")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateJob(job)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateOAuthApp")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateOAuthApp(app)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateOAuthStateToken(extra string) (*model.Token, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateOAuthStateToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateOAuthStateToken(extra)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateOAuthUser(c *request.Context, service string, userData io.Reader, teamID string, tokenUser *model.User) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateOAuthUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateOAuthUser(c, service, userData, teamID, tokenUser)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateOutgoingWebhook(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateOutgoingWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateOutgoingWebhook(hook)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreatePasswordRecoveryToken(userID string, email string) (*model.Token, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreatePasswordRecoveryToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreatePasswordRecoveryToken(userID, email)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreatePost(c request.CTX, post *model.Post, channel *model.Channel, triggerWebhooks bool, setOnline bool) (savedPost *model.Post, err *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreatePost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreatePost(c, post, channel, triggerWebhooks, setOnline)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreatePostAsUser(c request.CTX, post *model.Post, currentSessionId string, setOnline bool) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreatePostAsUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreatePostAsUser(c, post, currentSessionId, setOnline)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreatePostMissingChannel(c request.CTX, post *model.Post, triggerWebhooks bool, setOnline bool) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreatePostMissingChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreatePostMissingChannel(c, post, triggerWebhooks, setOnline)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateRetentionPolicy(policy *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateRetentionPolicy(policy)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateRole(role *model.Role) (*model.Role, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateRole")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateRole(role)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateScheme(scheme *model.Scheme) (*model.Scheme, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateScheme(scheme)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateSession(session *model.Session) (*model.Session, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateSession")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateSession(session)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateSidebarCategory(c request.CTX, userID string, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateSidebarCategory")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateSidebarCategory(c, userID, teamID, newCategory)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateTeam(c request.CTX, team *model.Team) (*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateTeam(c, team)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateTeamWithUser(c *request.Context, team *model.Team, userID string) (*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateTeamWithUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateTeamWithUser(c, team, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateTermsOfService(text string, userID string) (*model.TermsOfService, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateTermsOfService")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateTermsOfService(text, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateUploadSession(c request.CTX, us *model.UploadSession) (*model.UploadSession, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateUploadSession")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateUploadSession(c, us)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateUser(c request.CTX, user *model.User) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateUser(c, user)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateUserAccessToken(token *model.UserAccessToken) (*model.UserAccessToken, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateUserAccessToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateUserAccessToken(token)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateUserAsAdmin(c request.CTX, user *model.User, redirect string) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateUserAsAdmin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateUserAsAdmin(c, user, redirect)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateUserFromSignup(c request.CTX, user *model.User, redirect string) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateUserFromSignup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateUserFromSignup(c, user, redirect)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateUserWithInviteId(c request.CTX, user *model.User, inviteId string, redirect string) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateUserWithInviteId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateUserWithInviteId(c, user, inviteId, redirect)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateUserWithToken(c request.CTX, user *model.User, token *model.Token) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateUserWithToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateUserWithToken(c, user, token)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateWebhookPost(c request.CTX, userID string, channel *model.Channel, text string, overrideUsername string, overrideIconURL string, overrideIconEmoji string, props model.StringInterface, postType string, postRootId string) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateWebhookPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.CreateWebhookPost(c, userID, channel, text, overrideUsername, overrideIconURL, overrideIconEmoji, props, postType, postRootId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CreateZipFileAndAddFiles(fileBackend filestore.FileBackend, fileDatas []model.FileData, zipFileName string, directory string) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateZipFileAndAddFiles")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CreateZipFileAndAddFiles(fileBackend, fileDatas, zipFileName, directory)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DBHealthCheckDelete() error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DBHealthCheckDelete")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DBHealthCheckDelete()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DBHealthCheckWrite() error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DBHealthCheckWrite")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DBHealthCheckWrite()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeactivateGuests(c *request.Context) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeactivateGuests")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeactivateGuests(c)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeactivateMfa(userID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeactivateMfa")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeactivateMfa(userID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeauthorizeOAuthAppForUser(userID string, appID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeauthorizeOAuthAppForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeauthorizeOAuthAppForUser(userID, appID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DefaultChannelNames(c request.CTX) []string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DefaultChannelNames")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DefaultChannelNames(c)
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteAcknowledgementForPost(c *request.Context, postID string, userID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteAcknowledgementForPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteAcknowledgementForPost(c, postID, userID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteAllExpiredPluginKeys() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteAllExpiredPluginKeys")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteAllExpiredPluginKeys()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteAllKeysForPlugin(pluginID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteAllKeysForPlugin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteAllKeysForPlugin(pluginID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteBrandImage() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteBrandImage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteBrandImage()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteChannel(c request.CTX, channel *model.Channel, userID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteChannel(c, channel, userID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteChannelScheme(c request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteChannelScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteChannelScheme(c, channel)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteCommand(commandID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteCommand")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteCommand(commandID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteDraft(userID string, channelID string, rootID string, connectionID string) (*model.Draft, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteDraft")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteDraft(userID, channelID, rootID, connectionID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteEmoji(c request.CTX, emoji *model.Emoji) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteEmoji")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteEmoji(c, emoji)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteEphemeralPost(userID string, postID string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteEphemeralPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.DeleteEphemeralPost(userID, postID)
}
func (a *OpenTracingAppLayer) DeleteExport(name string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteExport")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteExport(name)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteGroup(groupID string) (*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteGroup(groupID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteGroupConstrainedMemberships(c *request.Context) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteGroupConstrainedMemberships")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteGroupConstrainedMemberships(c)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteGroupMember")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteGroupMember(groupID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteGroupMembers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteGroupMembers(groupID, userIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteGroupSyncable")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteGroupSyncable(groupID, syncableID, syncableType)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteIncomingWebhook(hookID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteIncomingWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteIncomingWebhook(hookID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteOAuthApp(appID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteOAuthApp")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteOAuthApp(appID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteOutgoingWebhook(hookID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteOutgoingWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteOutgoingWebhook(hookID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeletePluginKey(pluginID string, key string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeletePluginKey")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeletePluginKey(pluginID, key)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeletePost(c request.CTX, postID string, deleteByID string) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeletePost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeletePost(c, postID, deleteByID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeletePreferences(userID string, preferences model.Preferences) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeletePreferences")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeletePreferences(userID, preferences)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeletePublicKey(name string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeletePublicKey")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeletePublicKey(name)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteReactionForPost(c *request.Context, reaction *model.Reaction) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteReactionForPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteReactionForPost(c, reaction)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteRemoteCluster(remoteClusterId string) (bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteRemoteCluster")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteRemoteCluster(remoteClusterId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteRetentionPolicy(policyID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteRetentionPolicy(policyID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteScheme(schemeId string) (*model.Scheme, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteScheme(schemeId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteSharedChannel(channelID string) (bool, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteSharedChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteSharedChannel(channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteSharedChannelRemote(id string) (bool, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteSharedChannelRemote")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteSharedChannelRemote(id)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteSidebarCategory(c request.CTX, userID string, teamID string, categoryId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteSidebarCategory")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteSidebarCategory(c, userID, teamID, categoryId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteToken(token *model.Token) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteToken(token)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DemoteUserToGuest(c request.CTX, user *model.User) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DemoteUserToGuest")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DemoteUserToGuest(c, user)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DisableAutoResponder(c request.CTX, userID string, asAdmin bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DisableAutoResponder")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DisableAutoResponder(c, userID, asAdmin)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DisablePlugin(id string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DisablePlugin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DisablePlugin(id)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DisableUserAccessToken(token *model.UserAccessToken) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DisableUserAccessToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DisableUserAccessToken(token)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DoActionRequest(c *request.Context, rawURL string, body []byte) (*http.Response, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoActionRequest")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DoActionRequest(c, rawURL, body)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DoAdvancedPermissionsMigration() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoAdvancedPermissionsMigration")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.DoAdvancedPermissionsMigration()
}
func (a *OpenTracingAppLayer) DoAppMigrations() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoAppMigrations")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.DoAppMigrations()
}
func (a *OpenTracingAppLayer) DoCheckForAdminNotifications(trial bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoCheckForAdminNotifications")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DoCheckForAdminNotifications(trial)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DoCommandRequest(cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoCommandRequest")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.DoCommandRequest(cmd, p)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) DoEmojisPermissionsMigration() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoEmojisPermissionsMigration")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.DoEmojisPermissionsMigration()
}
func (a *OpenTracingAppLayer) DoGuestRolesCreationMigration() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoGuestRolesCreationMigration")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.DoGuestRolesCreationMigration()
}
func (a *OpenTracingAppLayer) DoLocalRequest(c *request.Context, rawURL string, body []byte) (*http.Response, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoLocalRequest")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DoLocalRequest(c, rawURL, body)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DoLogin(c *request.Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceID string, isMobile bool, isOAuthUser bool, isSaml bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoLogin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DoLogin(c, w, r, user, deviceID, isMobile, isOAuthUser, isSaml)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DoPermissionsMigrations() error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoPermissionsMigrations")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DoPermissionsMigrations()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DoPostAction(c *request.Context, postID string, actionId string, userID string, selectedOption string) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoPostAction")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DoPostAction(c, postID, actionId, userID, selectedOption)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DoPostActionWithCookie(c *request.Context, postID string, actionId string, userID string, selectedOption string, cookie *model.PostActionCookie) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoPostActionWithCookie")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DoPostActionWithCookie(c, postID, actionId, userID, selectedOption, cookie)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DoSystemConsoleRolesCreationMigration() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoSystemConsoleRolesCreationMigration")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.DoSystemConsoleRolesCreationMigration()
}
func (a *OpenTracingAppLayer) DoUploadFile(c request.CTX, now time.Time, rawTeamId string, rawChannelId string, rawUserId string, rawFilename string, data []byte) (*model.FileInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoUploadFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DoUploadFile(c, now, rawTeamId, rawChannelId, rawUserId, rawFilename, data)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DoUploadFileExpectModification(c request.CTX, now time.Time, rawTeamId string, rawChannelId string, rawUserId string, rawFilename string, data []byte) (*model.FileInfo, []byte, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoUploadFileExpectModification")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.DoUploadFileExpectModification(c, now, rawTeamId, rawChannelId, rawUserId, rawFilename, data)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) DoubleCheckPassword(user *model.User, password string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoubleCheckPassword")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DoubleCheckPassword(user, password)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DownloadFromURL(downloadURL string) ([]byte, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DownloadFromURL")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DownloadFromURL(downloadURL)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) EnablePlugin(id string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.EnablePlugin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.EnablePlugin(id)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) EnableUserAccessToken(token *model.UserAccessToken) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.EnableUserAccessToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.EnableUserAccessToken(token)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) EnsureBot(c request.CTX, productID string, bot *model.Bot) (string, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.EnsureBot")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.EnsureBot(c, productID, bot)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) EnvironmentConfig(filter func(reflect.StructField) bool) map[string]any {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.EnvironmentConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.EnvironmentConfig(filter)
return resultVar0
}
func (a *OpenTracingAppLayer) ExecuteCommand(c request.CTX, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ExecuteCommand")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
span.SetTag("args", args)
defer span.Finish()
resultVar0, resultVar1 := a.app.ExecuteCommand(c, args)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ExecuteWorkTemplate(c *request.Context, wtcr *worktemplates.ExecutionRequest, installPlugins bool) (*app.WorkTemplateExecutionResult, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ExecuteWorkTemplate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ExecuteWorkTemplate(c, wtcr, installPlugins)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ExportPermissions(w io.Writer) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ExportPermissions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ExportPermissions(w)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ExtendSessionExpiryIfNeeded(session *model.Session) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ExtendSessionExpiryIfNeeded")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ExtendSessionExpiryIfNeeded(session)
return resultVar0
}
func (a *OpenTracingAppLayer) ExtractContentFromFileInfo(fileInfo *model.FileInfo) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ExtractContentFromFileInfo")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ExtractContentFromFileInfo(fileInfo)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) FetchSamlMetadataFromIdp(url string) ([]byte, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FetchSamlMetadataFromIdp")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.FetchSamlMetadataFromIdp(url)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) FileBackend() filestore.FileBackend {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FileBackend")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.FileBackend()
return resultVar0
}
func (a *OpenTracingAppLayer) FileExists(path string) (bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FileExists")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.FileExists(path)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) FileModTime(path string) (time.Time, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FileModTime")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.FileModTime(path)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) FileReader(path string) (filestore.ReadCloseSeeker, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FileReader")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.FileReader(path)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) FileSize(path string) (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FileSize")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.FileSize(path)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) FillInChannelProps(c request.CTX, channel *model.Channel) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FillInChannelProps")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.FillInChannelProps(c, channel)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) FillInChannelsProps(c request.CTX, channelList model.ChannelList) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FillInChannelsProps")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.FillInChannelsProps(c, channelList)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) FillInPostProps(c request.CTX, post *model.Post, channel *model.Channel) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FillInPostProps")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.FillInPostProps(c, post, channel)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) FilterNonGroupChannelMembers(userIDs []string, channel *model.Channel) ([]string, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FilterNonGroupChannelMembers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.FilterNonGroupChannelMembers(userIDs, channel)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) FilterNonGroupTeamMembers(userIDs []string, team *model.Team) ([]string, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FilterNonGroupTeamMembers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.FilterNonGroupTeamMembers(userIDs, team)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) FilterUsersByVisible(viewer *model.User, otherUsers []*model.User) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FilterUsersByVisible")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.FilterUsersByVisible(viewer, otherUsers)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) FindTeamByName(name string) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FindTeamByName")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.FindTeamByName(name)
return resultVar0
}
func (a *OpenTracingAppLayer) FinishSendAdminNotifyPost(trial bool, now int64, pluginBasedData map[string][]*model.NotifyAdminData) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FinishSendAdminNotifyPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.FinishSendAdminNotifyPost(trial, now, pluginBasedData)
}
func (a *OpenTracingAppLayer) GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GenerateMfaSecret")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GenerateMfaSecret(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GeneratePublicLink(siteURL string, info *model.FileInfo) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GeneratePublicLink")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GeneratePublicLink(siteURL, info)
return resultVar0
}
func (a *OpenTracingAppLayer) GenerateSupportPacket() []model.FileData {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GenerateSupportPacket")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GenerateSupportPacket()
return resultVar0
}
func (a *OpenTracingAppLayer) GetAcknowledgementsForPost(postID string) ([]*model.PostAcknowledgement, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAcknowledgementsForPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAcknowledgementsForPost(postID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAcknowledgementsForPostList(postList *model.PostList) (map[string][]*model.PostAcknowledgement, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAcknowledgementsForPostList")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAcknowledgementsForPostList(postList)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetActivePluginManifests")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetActivePluginManifests()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAllChannels(c request.CTX, page int, perPage int, opts model.ChannelSearchOpts) (model.ChannelListWithTeamData, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAllChannels(c, page, perPage, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAllChannelsCount(c request.CTX, opts model.ChannelSearchOpts) (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllChannelsCount")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAllChannelsCount(c, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAllLdapGroupsPage(page int, perPage int, opts model.LdapGroupSearchOpts) ([]*model.Group, int, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllLdapGroupsPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.GetAllLdapGroupsPage(page, perPage, opts)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetAllPrivateTeams() ([]*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllPrivateTeams")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAllPrivateTeams()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAllPublicTeams() ([]*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllPublicTeams")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAllPublicTeams()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAllRemoteClusters(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllRemoteClusters")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAllRemoteClusters(filter)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAllRoles() ([]*model.Role, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllRoles")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAllRoles()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAllTeams() ([]*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllTeams")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAllTeams()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAllTeamsPage(offset int, limit int, opts *model.TeamSearch) ([]*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllTeamsPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAllTeamsPage(offset, limit, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAllTeamsPageWithCount(offset int, limit int, opts *model.TeamSearch) (*model.TeamsWithCount, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAllTeamsPageWithCount")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAllTeamsPageWithCount(offset, limit, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAnalytics(name string, teamID string) (model.AnalyticsRows, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAnalytics")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAnalytics(name, teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAppliedSchemaMigrations() ([]model.AppliedMigration, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAppliedSchemaMigrations")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAppliedSchemaMigrations()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAudits(userID string, limit int) (model.Audits, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAudits")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAudits(userID, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAuditsPage(userID string, page int, perPage int) (model.Audits, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAuditsPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAuditsPage(userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAuthorizationCode(w http.ResponseWriter, r *http.Request, service string, props map[string]string, loginHint string) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAuthorizationCode")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAuthorizationCode(w, r, service, props, loginHint)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAuthorizedAppsForUser(userID string, page int, perPage int) ([]*model.OAuthApp, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAuthorizedAppsForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAuthorizedAppsForUser(userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetBot(botUserId string, includeDeleted bool) (*model.Bot, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetBot")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetBot(botUserId, includeDeleted)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetBots(options *model.BotGetOptions) (model.BotList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetBots")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetBots(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetBrandImage() ([]byte, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetBrandImage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetBrandImage()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetBulkReactionsForPosts(postIDs []string) (map[string][]*model.Reaction, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetBulkReactionsForPosts")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetBulkReactionsForPosts(postIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannel(c request.CTX, channelID string) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannel(c, channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelByName(c request.CTX, channelName string, teamID string, includeDeleted bool) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelByName")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelByName(c, channelName, teamID, includeDeleted)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelByNameForTeamName(c request.CTX, channelName string, teamName string, includeDeleted bool) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelByNameForTeamName")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelByNameForTeamName(c, channelName, teamName, includeDeleted)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelCounts(c request.CTX, teamID string, userID string) (*model.ChannelCounts, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelCounts")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelCounts(c, teamID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelFileCount(c request.CTX, channelID string) (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelFileCount")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelFileCount(c, channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelGroupUsers(channelID string) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelGroupUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelGroupUsers(channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelGuestCount(c request.CTX, channelID string) (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelGuestCount")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelGuestCount(c, channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelMember(c request.CTX, channelID string, userID string) (*model.ChannelMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelMember")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelMember(c, channelID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelMemberCount(c request.CTX, channelID string) (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelMemberCount")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelMemberCount(c, channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelMembersByIds(c request.CTX, channelID string, userIDs []string) (model.ChannelMembers, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelMembersByIds")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelMembersByIds(c, channelID, userIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelMembersForUser(c request.CTX, teamID string, userID string) (model.ChannelMembers, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelMembersForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelMembersForUser(c, teamID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelMembersForUserWithPagination(c request.CTX, userID string, page int, perPage int) ([]*model.ChannelMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelMembersForUserWithPagination")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelMembersForUserWithPagination(c, userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelMembersPage(c request.CTX, channelID string, page int, perPage int) (model.ChannelMembers, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelMembersPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelMembersPage(c, channelID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelMembersTimezones(c request.CTX, channelID string) ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelMembersTimezones")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelMembersTimezones(c, channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelMembersWithTeamDataForUserWithPagination(c request.CTX, userID string, page int, perPage int) (model.ChannelMembersWithTeamData, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelMembersWithTeamDataForUserWithPagination")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelMembersWithTeamDataForUserWithPagination(c, userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelModerationsForChannel(c request.CTX, channel *model.Channel) ([]*model.ChannelModeration, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelModerationsForChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelModerationsForChannel(c, channel)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelPinnedPostCount(c request.CTX, channelID string) (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelPinnedPostCount")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelPinnedPostCount(c, channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelPoliciesForUser(userID string, offset int, limit int) (*model.RetentionPolicyForChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelPoliciesForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelPoliciesForUser(userID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelUnread(c request.CTX, channelID string, userID string) (*model.ChannelUnread, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelUnread")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelUnread(c, channelID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannels(c request.CTX, channelIDs []string) ([]*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannels(c, channelIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelsByNames(c request.CTX, channelNames []string, teamID string) ([]*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelsByNames")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelsByNames(c, channelNames, teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelsForRetentionPolicy(policyID string, offset int, limit int) (*model.ChannelsWithCount, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelsForRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelsForRetentionPolicy(policyID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelsForScheme(scheme *model.Scheme, offset int, limit int) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelsForScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelsForScheme(scheme, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelsForSchemePage(scheme *model.Scheme, page int, perPage int) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelsForSchemePage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelsForSchemePage(scheme, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelsForTeamForUser(c request.CTX, teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelsForTeamForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelsForTeamForUser(c, teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelsForTeamForUserWithCursor(c request.CTX, teamID string, userID string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelsForTeamForUserWithCursor")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelsForTeamForUserWithCursor(c, teamID, userID, opts, afterChannelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelsForUser(c request.CTX, userID string, includeDeleted bool, lastDeleteAt int, pageSize int, fromChannelID string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelsForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelsForUser(c, userID, includeDeleted, lastDeleteAt, pageSize, fromChannelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetChannelsUserNotIn(c request.CTX, teamID string, userID string, offset int, limit int) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelsUserNotIn")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetChannelsUserNotIn(c, teamID, userID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetCloudSession(token string) (*model.Session, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetCloudSession")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetCloudSession(token)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetClusterId() string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetClusterId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetClusterId()
return resultVar0
}
func (a *OpenTracingAppLayer) GetClusterPluginStatuses() (model.PluginStatuses, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetClusterPluginStatuses")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetClusterPluginStatuses()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetClusterStatus() []*model.ClusterInfo {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetClusterStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetClusterStatus()
return resultVar0
}
func (a *OpenTracingAppLayer) GetCommand(commandID string) (*model.Command, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetCommand")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetCommand(commandID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetCommonTeamIDsForTwoUsers(userID string, otherUserID string) ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetCommonTeamIDsForTwoUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetCommonTeamIDsForTwoUsers(userID, otherUserID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetComplianceFile(job *model.Compliance) ([]byte, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetComplianceFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetComplianceFile(job)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetComplianceReport(reportId string) (*model.Compliance, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetComplianceReport")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetComplianceReport(reportId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetComplianceReports(page int, perPage int) (model.Compliances, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetComplianceReports")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetComplianceReports(page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetConfigFile(name string) ([]byte, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetConfigFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetConfigFile(name)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetCookieDomain() string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetCookieDomain")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetCookieDomain()
return resultVar0
}
func (a *OpenTracingAppLayer) GetCustomStatus(userID string) (*model.CustomStatus, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetCustomStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetCustomStatus(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetDefaultProfileImage(user *model.User) ([]byte, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetDefaultProfileImage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetDefaultProfileImage(user)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetDeletedChannels(c request.CTX, teamID string, offset int, limit int, userID string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetDeletedChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetDeletedChannels(c, teamID, offset, limit, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetDraft(userID string, channelID string, rootID string) (*model.Draft, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetDraft")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetDraft(userID, channelID, rootID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetDraftsForUser(userID string, teamID string) ([]*model.Draft, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetDraftsForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetDraftsForUser(userID, teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetEditHistoryForPost(postID string) ([]*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetEditHistoryForPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetEditHistoryForPost(postID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetEmoji(c request.CTX, emojiId string) (*model.Emoji, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetEmoji")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetEmoji(c, emojiId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetEmojiByName(c request.CTX, emojiName string) (*model.Emoji, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetEmojiByName")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetEmojiByName(c, emojiName)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetEmojiImage(c request.CTX, emojiId string) ([]byte, string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetEmojiImage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.GetEmojiImage(c, emojiId)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetEmojiList(c request.CTX, page int, perPage int, sort string) ([]*model.Emoji, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetEmojiList")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetEmojiList(c, page, perPage, sort)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetEmojiStaticURL(c request.CTX, emojiName string) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetEmojiStaticURL")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetEmojiStaticURL(c, emojiName)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetEnvironmentConfig(filter func(reflect.StructField) bool) map[string]any {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetEnvironmentConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetEnvironmentConfig(filter)
return resultVar0
}
func (a *OpenTracingAppLayer) GetFile(fileID string) ([]byte, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetFile(fileID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetFileInfo(fileID string) (*model.FileInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetFileInfo")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetFileInfo(fileID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetFileInfos(page int, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetFileInfos")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetFileInfos(page, perPage, opt)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetFileInfosForPost(postID string, fromMaster bool, includeDeleted bool) ([]*model.FileInfo, int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetFileInfosForPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.GetFileInfosForPost(postID, fromMaster, includeDeleted)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetFileInfosForPostWithMigration(postID string, includeDeleted bool) ([]*model.FileInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetFileInfosForPostWithMigration")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetFileInfosForPostWithMigration(postID, includeDeleted)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetFilteredUsersStats(options *model.UserCountOptions) (*model.UsersStats, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetFilteredUsersStats")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetFilteredUsersStats(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetFlaggedPosts(userID string, offset int, limit int) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetFlaggedPosts")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetFlaggedPosts(userID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetFlaggedPostsForChannel(userID string, channelID string, offset int, limit int) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetFlaggedPostsForChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetFlaggedPostsForChannel(userID, channelID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetFlaggedPostsForTeam(userID string, teamID string, offset int, limit int) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetFlaggedPostsForTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetFlaggedPostsForTeam(userID, teamID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGlobalRetentionPolicy() (*model.GlobalRetentionPolicy, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGlobalRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGlobalRetentionPolicy()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroup(id string, opts *model.GetGroupOpts, viewRestrictions *model.ViewUsersRestrictions) (*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroup(id, opts, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupByName(name string, opts model.GroupSearchOpts) (*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupByName")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupByName(name, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupByRemoteID")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupByRemoteID(remoteID, groupSource)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupChannel(c request.CTX, userIDs []string) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupChannel(c, userIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupMemberCount(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupMemberCount")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupMemberCount(groupID, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupMemberUsers(groupID string) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupMemberUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupMemberUsers(groupID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, int, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupMemberUsersPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.GetGroupMemberUsersPage(groupID, page, perPage, viewRestrictions)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetGroupMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, int, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupMemberUsersSortedPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.GetGroupMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, teammateNameDisplay)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupSyncable")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupSyncable(groupID, syncableID, syncableType)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupSyncables(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupSyncables")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupSyncables(groupID, syncableType)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroups(page int, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroups")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroups(page, perPage, opts, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupsAssociatedToChannelsByTeam(teamID string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupsAssociatedToChannelsByTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupsAssociatedToChannelsByTeam(teamID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupsByChannel(channelID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupsByChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.GetGroupsByChannel(channelID, opts)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetGroupsByIDs(groupIDs []string) ([]*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupsByIDs")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupsByIDs(groupIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupsBySource(groupSource model.GroupSource) ([]*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupsBySource")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupsBySource(groupSource)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupsByTeam(teamID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupsByTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.GetGroupsByTeam(teamID, opts)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetGroupsByUserId(userID string) ([]*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupsByUserId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupsByUserId(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetHubForUserId(userID string) *platform.Hub {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetHubForUserId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetHubForUserId(userID)
return resultVar0
}
func (a *OpenTracingAppLayer) GetIncomingWebhook(hookID string) (*model.IncomingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetIncomingWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetIncomingWebhook(hookID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetIncomingWebhooksForTeamPage(teamID string, page int, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetIncomingWebhooksForTeamPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetIncomingWebhooksForTeamPage(teamID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetIncomingWebhooksForTeamPageByUser(teamID string, userID string, page int, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetIncomingWebhooksForTeamPageByUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetIncomingWebhooksForTeamPageByUser(teamID, userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetIncomingWebhooksPage(page int, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetIncomingWebhooksPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetIncomingWebhooksPage(page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetIncomingWebhooksPageByUser(userID string, page int, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetIncomingWebhooksPageByUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetIncomingWebhooksPageByUser(userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetJob(id string) (*model.Job, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJob")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetJob(id)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetJobs(offset int, limit int) ([]*model.Job, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJobs")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetJobs(offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetJobsByType(jobType string, offset int, limit int) ([]*model.Job, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJobsByType")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetJobsByType(jobType, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetJobsByTypePage(jobType string, page int, perPage int) ([]*model.Job, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJobsByTypePage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetJobsByTypePage(jobType, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetJobsByTypes(jobTypes []string, offset int, limit int) ([]*model.Job, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJobsByTypes")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetJobsByTypes(jobTypes, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetJobsByTypesPage(jobType []string, page int, perPage int) ([]*model.Job, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJobsByTypesPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetJobsByTypesPage(jobType, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetJobsPage(page int, perPage int) ([]*model.Job, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJobsPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetJobsPage(page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetKnownUsers(userID string) ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetKnownUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetKnownUsers(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetLastAccessibleFileTime() (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetLastAccessibleFileTime")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetLastAccessibleFileTime()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetLastAccessiblePostTime() (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetLastAccessiblePostTime")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetLastAccessiblePostTime()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetLatestTermsOfService() (*model.TermsOfService, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetLatestTermsOfService")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetLatestTermsOfService()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetLatestVersion(latestVersionUrl string) (*model.GithubReleaseInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetLatestVersion")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetLatestVersion(latestVersionUrl)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetLdapGroup(ldapGroupID string) (*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetLdapGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetLdapGroup(ldapGroupID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetLogs(page int, perPage int) ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetLogs")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetLogs(page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetLogsSkipSend(page int, perPage int, logFilter *model.LogFilter) ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetLogsSkipSend")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetLogsSkipSend(page, perPage, logFilter)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetMarketplacePlugins(filter *model.MarketplacePluginFilter) ([]*model.MarketplacePlugin, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetMarketplacePlugins")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetMarketplacePlugins(filter)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetMemberCountsByGroup(ctx context.Context, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetMemberCountsByGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetMemberCountsByGroup(ctx, channelID, includeTimezones)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetMessageForNotification")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetMessageForNotification(post, translateFunc)
return resultVar0
}
func (a *OpenTracingAppLayer) GetMultipleEmojiByName(c request.CTX, names []string) ([]*model.Emoji, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetMultipleEmojiByName")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetMultipleEmojiByName(c, names)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetNewTeamMembersSince(c request.CTX, teamID string, opts *model.InsightsOpts) (*model.NewTeamMembersList, int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetNewTeamMembersSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.GetNewTeamMembersSince(c, teamID, opts)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetNewUsersForTeamPage(teamID string, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetNewUsersForTeamPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetNewUsersForTeamPage(teamID, page, perPage, asAdmin, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetNextPostIdFromPostList(postList *model.PostList, collapsedThreads bool) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetNextPostIdFromPostList")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetNextPostIdFromPostList(postList, collapsedThreads)
return resultVar0
}
func (a *OpenTracingAppLayer) GetNotificationNameFormat(user *model.User) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetNotificationNameFormat")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetNotificationNameFormat(user)
return resultVar0
}
func (a *OpenTracingAppLayer) GetNumberOfChannelsOnTeam(c request.CTX, teamID string) (int, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetNumberOfChannelsOnTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetNumberOfChannelsOnTeam(c, teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthAccessTokenForCodeFlow(clientId string, grantType string, redirectURI string, code string, secret string, refreshToken string) (*model.AccessResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthAccessTokenForCodeFlow")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthAccessTokenForCodeFlow(clientId, grantType, redirectURI, code, secret, refreshToken)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthAccessTokenForImplicitFlow(userID string, authRequest *model.AuthorizeRequest) (*model.Session, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthAccessTokenForImplicitFlow")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthAccessTokenForImplicitFlow(userID, authRequest)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthApp(appID string) (*model.OAuthApp, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthApp")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthApp(appID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthApps(page int, perPage int) ([]*model.OAuthApp, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthApps")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthApps(page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthAppsByCreator(userID string, page int, perPage int) ([]*model.OAuthApp, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthAppsByCreator")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthAppsByCreator(userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthCodeRedirect(userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthCodeRedirect")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthCodeRedirect(userID, authRequest)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthImplicitRedirect(userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthImplicitRedirect")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthImplicitRedirect(userID, authRequest)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service string, teamID string, action string, redirectTo string, loginHint string, isMobile bool) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthLoginEndpoint")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthLoginEndpoint(w, r, service, teamID, action, redirectTo, loginHint, isMobile)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service string, teamID string) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthSignupEndpoint")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthSignupEndpoint(w, r, service, teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOAuthStateToken(token string) (*model.Token, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthStateToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOAuthStateToken(token)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOnboarding() (*model.System, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOnboarding")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOnboarding()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOpenGraphMetadata(requestURL string) ([]byte, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOpenGraphMetadata")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOpenGraphMetadata(requestURL)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOrCreateDirectChannel(c request.CTX, userID string, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOrCreateDirectChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOrCreateDirectChannel(c, userID, otherUserID, channelOptions...)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOrCreateTrueUpReviewStatus() (*model.TrueUpReviewStatus, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOrCreateTrueUpReviewStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOrCreateTrueUpReviewStatus()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOutgoingWebhook(hookID string) (*model.OutgoingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOutgoingWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOutgoingWebhook(hookID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOutgoingWebhooksForChannelPageByUser(channelID string, userID string, page int, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOutgoingWebhooksForChannelPageByUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOutgoingWebhooksForChannelPageByUser(channelID, userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOutgoingWebhooksForTeamPage(teamID string, page int, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOutgoingWebhooksForTeamPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOutgoingWebhooksForTeamPage(teamID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOutgoingWebhooksForTeamPageByUser(teamID string, userID string, page int, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOutgoingWebhooksForTeamPageByUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOutgoingWebhooksForTeamPageByUser(teamID, userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOutgoingWebhooksPage(page int, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOutgoingWebhooksPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOutgoingWebhooksPage(page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOutgoingWebhooksPageByUser(userID string, page int, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOutgoingWebhooksPageByUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOutgoingWebhooksPageByUser(userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPasswordRecoveryToken(token string) (*model.Token, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPasswordRecoveryToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPasswordRecoveryToken(token)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPermalinkPost(c request.CTX, postID string, userID string) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPermalinkPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPermalinkPost(c, postID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPinnedPosts(c request.CTX, channelID string) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPinnedPosts")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPinnedPosts(c, channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPluginKey(pluginID string, key string) ([]byte, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPluginKey")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPluginKey(pluginID, key)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPluginStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPluginStatus(id)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPluginStatuses() (model.PluginStatuses, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPluginStatuses")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPluginStatuses()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPlugins() (*model.PluginsResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPlugins")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPlugins()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPluginsEnvironment() *plugin.Environment {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPluginsEnvironment")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetPluginsEnvironment()
return resultVar0
}
func (a *OpenTracingAppLayer) GetPostAfterTime(channelID string, time int64, collapsedThreads bool) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostAfterTime")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostAfterTime(channelID, time, collapsedThreads)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostIdAfterTime(channelID string, time int64, collapsedThreads bool) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostIdAfterTime")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostIdAfterTime(channelID, time, collapsedThreads)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostIdBeforeTime(channelID string, time int64, collapsedThreads bool) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostIdBeforeTime")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostIdBeforeTime(channelID, time, collapsedThreads)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostIfAuthorized(c request.CTX, postID string, session *model.Session, includeDeleted bool) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostIfAuthorized")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostIfAuthorized(c, postID, session, includeDeleted)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostInfo(c request.CTX, postID string) (*model.PostInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostInfo")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostInfo(c, postID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostThread(postID string, opts model.GetPostsOptions, userID string) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostThread")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostThread(postID, opts, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPosts(channelID string, offset int, limit int) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPosts")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPosts(channelID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostsAfterPost(options model.GetPostsOptions) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostsAfterPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostsAfterPost(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostsAroundPost(before bool, options model.GetPostsOptions) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostsAroundPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostsAroundPost(before, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostsBeforePost(options model.GetPostsOptions) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostsBeforePost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostsBeforePost(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostsByIds(postIDs []string) ([]*model.Post, int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostsByIds")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.GetPostsByIds(postIDs)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetPostsEtag(channelID string, collapsedThreads bool) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostsEtag")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetPostsEtag(channelID, collapsedThreads)
return resultVar0
}
func (a *OpenTracingAppLayer) GetPostsForChannelAroundLastUnread(c request.CTX, channelID string, userID string, limitBefore int, limitAfter int, skipFetchThreads bool, collapsedThreads bool, collapsedThreadsExtended bool) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostsForChannelAroundLastUnread")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostsForChannelAroundLastUnread(c, channelID, userID, limitBefore, limitAfter, skipFetchThreads, collapsedThreads, collapsedThreadsExtended)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostsPage(options model.GetPostsOptions) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostsPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostsPage(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostsSince(options model.GetPostsSinceOptions) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostsSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostsSince(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostsUsage() (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostsUsage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPostsUsage()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPreferenceByCategoryAndNameForUser(userID string, category string, preferenceName string) (*model.Preference, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPreferenceByCategoryAndNameForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPreferenceByCategoryAndNameForUser(userID, category, preferenceName)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPreferenceByCategoryForUser(userID string, category string) (model.Preferences, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPreferenceByCategoryForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPreferenceByCategoryForUser(userID, category)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPreferencesForUser(userID string) (model.Preferences, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPreferencesForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPreferencesForUser(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPrevPostIdFromPostList(postList *model.PostList, collapsedThreads bool) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPrevPostIdFromPostList")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetPrevPostIdFromPostList(postList, collapsedThreads)
return resultVar0
}
func (a *OpenTracingAppLayer) GetPriorityForPost(postId string) (*model.PostPriority, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPriorityForPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPriorityForPost(postId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPriorityForPostList(list *model.PostList) (map[string]*model.PostPriority, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPriorityForPostList")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPriorityForPostList(list)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPrivateChannelsForTeam(c request.CTX, teamID string, offset int, limit int) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPrivateChannelsForTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPrivateChannelsForTeam(c, teamID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetProductNotices(c *request.Context, userID string, teamID string, client model.NoticeClientType, clientVersion string, locale string) (model.NoticeMessages, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetProductNotices")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetProductNotices(c, userID, teamID, client, clientVersion, locale)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetProfileImage(user *model.User) ([]byte, bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetProfileImage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.GetProfileImage(user)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetPublicChannelsByIdsForTeam(c request.CTX, teamID string, channelIDs []string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPublicChannelsByIdsForTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPublicChannelsByIdsForTeam(c, teamID, channelIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPublicChannelsForTeam(c request.CTX, teamID string, offset int, limit int) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPublicChannelsForTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPublicChannelsForTeam(c, teamID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPublicKey(name string) ([]byte, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPublicKey")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetPublicKey(name)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetReactionsForPost(postID string) ([]*model.Reaction, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetReactionsForPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetReactionsForPost(postID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRecentSearchesForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRecentSearchesForUser(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRecentlyActiveUsersForTeam(teamID string) (map[string]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRecentlyActiveUsersForTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRecentlyActiveUsersForTeam(teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRecentlyActiveUsersForTeamPage(teamID string, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRecentlyActiveUsersForTeamPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRecentlyActiveUsersForTeamPage(teamID, page, perPage, asAdmin, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRemoteCluster(remoteClusterId string) (*model.RemoteCluster, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRemoteCluster")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRemoteCluster(remoteClusterId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRemoteClusterForUser(remoteID string, userID string) (*model.RemoteCluster, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRemoteClusterForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRemoteClusterForUser(remoteID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRemoteClusterService() (remotecluster.RemoteClusterServiceIFace, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRemoteClusterService")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRemoteClusterService()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRemoteClusterSession(token string, remoteId string) (*model.Session, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRemoteClusterSession")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRemoteClusterSession(token, remoteId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRetentionPolicies(offset int, limit int) (*model.RetentionPolicyWithTeamAndChannelCountsList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRetentionPolicies")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRetentionPolicies(offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRetentionPoliciesCount() (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRetentionPoliciesCount")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRetentionPoliciesCount()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRetentionPolicy(policyID string) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRetentionPolicy(policyID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRole(id string) (*model.Role, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRole")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRole(id)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRoleByName(ctx context.Context, name string) (*model.Role, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRoleByName")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRoleByName(ctx, name)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetRolesByNames(names []string) ([]*model.Role, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRolesByNames")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetRolesByNames(names)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSamlCertificateStatus() *model.SamlCertificateStatus {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSamlCertificateStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetSamlCertificateStatus()
return resultVar0
}
func (a *OpenTracingAppLayer) GetSamlMetadata() (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSamlMetadata")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSamlMetadata()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSamlMetadataFromIdp(idpMetadataURL string) (*model.SamlMetadataResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSamlMetadataFromIdp")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSamlMetadataFromIdp(idpMetadataURL)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSanitizeOptions(asAdmin bool) map[string]bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSanitizeOptions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetSanitizeOptions(asAdmin)
return resultVar0
}
func (a *OpenTracingAppLayer) GetSanitizedConfig() *model.Config {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSanitizedConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetSanitizedConfig()
return resultVar0
}
func (a *OpenTracingAppLayer) GetScheme(id string) (*model.Scheme, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetScheme(id)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSchemeByName(name string) (*model.Scheme, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSchemeByName")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSchemeByName(name)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSchemeRolesForChannel(c request.CTX, channelID string) (guestRoleName string, userRoleName string, adminRoleName string, err *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSchemeRolesForChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2, resultVar3 := a.app.GetSchemeRolesForChannel(c, channelID)
if resultVar3 != nil {
span.LogFields(spanlog.Error(resultVar3))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2, resultVar3
}
func (a *OpenTracingAppLayer) GetSchemeRolesForTeam(teamID string) (string, string, string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSchemeRolesForTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2, resultVar3 := a.app.GetSchemeRolesForTeam(teamID)
if resultVar3 != nil {
span.LogFields(spanlog.Error(resultVar3))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2, resultVar3
}
func (a *OpenTracingAppLayer) GetSchemes(scope string, offset int, limit int) ([]*model.Scheme, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSchemes")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSchemes(scope, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSchemesPage(scope string, page int, perPage int) ([]*model.Scheme, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSchemesPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSchemesPage(scope, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSession(token string) (*model.Session, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSession")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSession(token)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSessionById(sessionID string) (*model.Session, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSessionById")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSessionById(sessionID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSessionLengthInMillis(session *model.Session) int64 {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSessionLengthInMillis")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetSessionLengthInMillis(session)
return resultVar0
}
func (a *OpenTracingAppLayer) GetSessions(userID string) ([]*model.Session, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSessions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSessions(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSharedChannel(channelID string) (*model.SharedChannel, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSharedChannel(channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSharedChannelRemote(id string) (*model.SharedChannelRemote, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannelRemote")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSharedChannelRemote(id)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSharedChannelRemoteByIds(channelID string, remoteID string) (*model.SharedChannelRemote, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannelRemoteByIds")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSharedChannelRemoteByIds(channelID, remoteID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSharedChannelRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannelRemotes")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSharedChannelRemotes(opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSharedChannelRemotesStatus(channelID string) ([]*model.SharedChannelRemoteStatus, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannelRemotesStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSharedChannelRemotesStatus(channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSharedChannels(page int, perPage int, opts model.SharedChannelFilterOpts) ([]*model.SharedChannel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSharedChannels(page, perPage, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSharedChannelsCount(opts model.SharedChannelFilterOpts) (int64, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSharedChannelsCount")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSharedChannelsCount(opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSidebarCategories(c request.CTX, userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSidebarCategories")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSidebarCategories(c, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSidebarCategoriesForTeamForUser(c request.CTX, userID string, teamID string) (*model.OrderedSidebarCategories, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSidebarCategoriesForTeamForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSidebarCategoriesForTeamForUser(c, userID, teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSidebarCategory(c request.CTX, categoryId string) (*model.SidebarCategoryWithChannels, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSidebarCategory")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSidebarCategory(c, categoryId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSidebarCategoryOrder(c request.CTX, userID string, teamID string) ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSidebarCategoryOrder")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSidebarCategoryOrder(c, userID, teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSinglePost(postID string, includeDeleted bool) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSinglePost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSinglePost(postID, includeDeleted)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSiteURL() string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSiteURL")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetSiteURL()
return resultVar0
}
func (a *OpenTracingAppLayer) GetStatus(userID string) (*model.Status, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetStatus(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetStatusFromCache(userID string) *model.Status {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetStatusFromCache")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetStatusFromCache(userID)
return resultVar0
}
func (a *OpenTracingAppLayer) GetStorageUsage() (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetStorageUsage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetStorageUsage()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSuggestions(c *request.Context, commandArgs *model.CommandArgs, commands []*model.Command, roleID string) []model.AutocompleteSuggestion {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSuggestions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetSuggestions(c, commandArgs, commands, roleID)
return resultVar0
}
func (a *OpenTracingAppLayer) GetSystemBot() (*model.Bot, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSystemBot")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetSystemBot()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeam(teamID string) (*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeam(teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamByInviteId(inviteId string) (*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamByInviteId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamByInviteId(inviteId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamByName(name string) (*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamByName")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamByName(name)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamGroupUsers(teamID string) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamGroupUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamGroupUsers(teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamIcon(team *model.Team) ([]byte, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamIcon")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamIcon(team)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamIdFromQuery(query url.Values) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamIdFromQuery")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamIdFromQuery(query)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamMember(teamID string, userID string) (*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamMember")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamMember(teamID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamMembers(teamID string, offset int, limit int, teamMembersGetOptions *model.TeamMembersGetOptions) ([]*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamMembers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamMembers(teamID, offset, limit, teamMembersGetOptions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamMembersByIds(teamID string, userIDs []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamMembersByIds")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamMembersByIds(teamID, userIDs, restrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamMembersForUser(userID string, excludeTeamID string, includeDeleted bool) ([]*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamMembersForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamMembersForUser(userID, excludeTeamID, includeDeleted)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamMembersForUserWithPagination(userID string, page int, perPage int) ([]*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamMembersForUserWithPagination")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamMembersForUserWithPagination(userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamPoliciesForUser(userID string, offset int, limit int) (*model.RetentionPolicyForTeamList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamPoliciesForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamPoliciesForUser(userID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamSchemeChannelRoles(c request.CTX, teamID string) (guestRoleName string, userRoleName string, adminRoleName string, err *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamSchemeChannelRoles")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2, resultVar3 := a.app.GetTeamSchemeChannelRoles(c, teamID)
if resultVar3 != nil {
span.LogFields(spanlog.Error(resultVar3))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2, resultVar3
}
func (a *OpenTracingAppLayer) GetTeamStats(teamID string, restrictions *model.ViewUsersRestrictions) (*model.TeamStats, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamStats")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamStats(teamID, restrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamUnread(teamID string, userID string) (*model.TeamUnread, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamUnread")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamUnread(teamID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeams(teamIDs []string) ([]*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeams")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeams(teamIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamsForRetentionPolicy(policyID string, offset int, limit int) (*model.TeamsWithCount, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamsForRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamsForRetentionPolicy(policyID, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamsForScheme(scheme *model.Scheme, offset int, limit int) ([]*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamsForScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamsForScheme(scheme, offset, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamsForSchemePage(scheme *model.Scheme, page int, perPage int) ([]*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamsForSchemePage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamsForSchemePage(scheme, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamsForUser(userID string) ([]*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamsForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamsForUser(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamsUnreadForUser(excludeTeamId string, userID string, includeCollapsedThreads bool) ([]*model.TeamUnread, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamsUnreadForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamsUnreadForUser(excludeTeamId, userID, includeCollapsedThreads)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTeamsUsage() (*model.TeamsUsage, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTeamsUsage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTeamsUsage()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTermsOfService(id string) (*model.TermsOfService, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTermsOfService")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTermsOfService(id)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool) (*model.ThreadResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetThreadForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetThreadForUser(threadMembership, extended)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetThreadMembershipForUser(userId string, threadId string) (*model.ThreadMembership, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetThreadMembershipForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetThreadMembershipForUser(userId, threadId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetThreadMembershipsForUser(userID string, teamID string) ([]*model.ThreadMembership, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetThreadMembershipsForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetThreadMembershipsForUser(userID, teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetThreadsForUser(userID string, teamID string, options model.GetUserThreadsOpts) (*model.Threads, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetThreadsForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetThreadsForUser(userID, teamID, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTokenById(token string) (*model.Token, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTokenById")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTokenById(token)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopChannelsForTeamSince(c request.CTX, teamID string, userID string, opts *model.InsightsOpts) (*model.TopChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopChannelsForTeamSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopChannelsForTeamSince(c, teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopChannelsForUserSince(c request.CTX, userID string, teamID string, opts *model.InsightsOpts) (*model.TopChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopChannelsForUserSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopChannelsForUserSince(c, userID, teamID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopDMsForUserSince(userID string, opts *model.InsightsOpts) (*model.TopDMList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopDMsForUserSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopDMsForUserSince(userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopInactiveChannelsForTeamSince(c request.CTX, teamID string, userID string, opts *model.InsightsOpts) (*model.TopInactiveChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopInactiveChannelsForTeamSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopInactiveChannelsForTeamSince(c, teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopInactiveChannelsForUserSince(c request.CTX, teamID string, userID string, opts *model.InsightsOpts) (*model.TopInactiveChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopInactiveChannelsForUserSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopInactiveChannelsForUserSince(c, teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopReactionsForTeamSince(teamID string, userID string, opts *model.InsightsOpts) (*model.TopReactionList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopReactionsForTeamSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopReactionsForTeamSince(teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopReactionsForUserSince(userID string, teamID string, opts *model.InsightsOpts) (*model.TopReactionList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopReactionsForUserSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopReactionsForUserSince(userID, teamID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopThreadsForTeamSince(c request.CTX, teamID string, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopThreadsForTeamSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopThreadsForTeamSince(c, teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopThreadsForUserSince(c request.CTX, teamID string, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopThreadsForUserSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopThreadsForUserSince(c, teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTotalUsersStats(viewRestrictions *model.ViewUsersRestrictions) (*model.UsersStats, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTotalUsersStats")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTotalUsersStats(viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTrueUpProfile() (map[string]any, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTrueUpProfile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTrueUpProfile()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUploadSession(c request.CTX, uploadId string) (*model.UploadSession, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUploadSession")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUploadSession(c, uploadId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUploadSessionsForUser(userID string) ([]*model.UploadSession, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUploadSessionsForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUploadSessionsForUser(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUser(userID string) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUser(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUserAccessToken(tokenID string, sanitize bool) (*model.UserAccessToken, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserAccessToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUserAccessToken(tokenID, sanitize)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUserAccessTokens(page int, perPage int) ([]*model.UserAccessToken, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserAccessTokens")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUserAccessTokens(page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUserAccessTokensForUser(userID string, page int, perPage int) ([]*model.UserAccessToken, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserAccessTokensForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUserAccessTokensForUser(userID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUserByAuth(authData *string, authService string) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserByAuth")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUserByAuth(authData, authService)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUserByEmail(email string) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserByEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUserByEmail(email)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUserByUsername(username string) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserByUsername")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUserByUsername(username)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUserForLogin(id string, loginId string) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserForLogin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUserForLogin(id, loginId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUserStatusesByIds(userIDs []string) ([]*model.Status, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserStatusesByIds")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUserStatusesByIds(userIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUserTermsOfService(userID string) (*model.UserTermsOfService, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserTermsOfService")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUserTermsOfService(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsers(userIDs []string) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsers(userIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersByGroupChannelIds(c *request.Context, channelIDs []string, asAdmin bool) (map[string][]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersByGroupChannelIds")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersByGroupChannelIds(c, channelIDs, asAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersByIds(userIDs []string, options *store.UserGetByIdsOpts) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersByIds")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersByIds(userIDs, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersByUsernames(usernames []string, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersByUsernames")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersByUsernames(usernames, asAdmin, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersEtag(restrictionsHash string) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersEtag")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetUsersEtag(restrictionsHash)
return resultVar0
}
func (a *OpenTracingAppLayer) GetUsersFromProfiles(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersFromProfiles")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersFromProfiles(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersInChannel(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersInChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersInChannel(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersInChannelByAdmin(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersInChannelByAdmin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersInChannelByAdmin(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersInChannelByStatus(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersInChannelByStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersInChannelByStatus(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersInChannelMap(options *model.UserGetOptions, asAdmin bool) (map[string]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersInChannelMap")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersInChannelMap(options, asAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersInChannelPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersInChannelPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersInChannelPage(options, asAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersInChannelPageByAdmin(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersInChannelPageByAdmin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersInChannelPageByAdmin(options, asAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersInChannelPageByStatus(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersInChannelPageByStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersInChannelPageByStatus(options, asAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersInTeam(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersInTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersInTeam(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersInTeamEtag(teamID string, restrictionsHash string) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersInTeamEtag")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetUsersInTeamEtag(teamID, restrictionsHash)
return resultVar0
}
func (a *OpenTracingAppLayer) GetUsersInTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersInTeamPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersInTeamPage(options, asAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersNotInChannel(teamID string, channelID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersNotInChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersNotInChannel(teamID, channelID, groupConstrained, offset, limit, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersNotInChannelMap(teamID string, channelID string, groupConstrained bool, offset int, limit int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) (map[string]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersNotInChannelMap")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersNotInChannelMap(teamID, channelID, groupConstrained, offset, limit, asAdmin, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersNotInChannelPage(teamID string, channelID string, groupConstrained bool, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersNotInChannelPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersNotInChannelPage(teamID, channelID, groupConstrained, page, perPage, asAdmin, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersNotInGroupPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersNotInGroupPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersNotInGroupPage(groupID, page, perPage, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersNotInTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersNotInTeam(teamID, groupConstrained, offset, limit, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersNotInTeamEtag(teamID string, restrictionsHash string) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersNotInTeamEtag")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.GetUsersNotInTeamEtag(teamID, restrictionsHash)
return resultVar0
}
func (a *OpenTracingAppLayer) GetUsersNotInTeamPage(teamID string, groupConstrained bool, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersNotInTeamPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersNotInTeamPage(teamID, groupConstrained, page, perPage, asAdmin, viewRestrictions)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersPage(options, asAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersWithInvalidEmails(page int, perPage int) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersWithInvalidEmails")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersWithInvalidEmails(page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersWithoutTeam(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersWithoutTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersWithoutTeam(options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUsersWithoutTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersWithoutTeamPage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUsersWithoutTeamPage(options, asAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetVerifyEmailToken(token string) (*model.Token, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetVerifyEmailToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetVerifyEmailToken(token)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetViewUsersRestrictions(userID string) (*model.ViewUsersRestrictions, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetViewUsersRestrictions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetViewUsersRestrictions(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetWarnMetricsBot() (*model.Bot, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetWarnMetricsBot")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetWarnMetricsBot()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetWarnMetricsStatus() (map[string]*model.WarnMetricStatus, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetWarnMetricsStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetWarnMetricsStatus()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetWorkTemplateCategories(t i18n.TranslateFunc) ([]*model.WorkTemplateCategory, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetWorkTemplateCategories")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetWorkTemplateCategories(t)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetWorkTemplates(category string, featureFlags map[string]string, t i18n.TranslateFunc) ([]*model.WorkTemplate, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetWorkTemplates")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetWorkTemplates(category, featureFlags, t)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) Handle404(w http.ResponseWriter, r *http.Request) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.Handle404")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.Handle404(w, r)
}
func (a *OpenTracingAppLayer) HandleCommandResponse(c request.CTX, command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HandleCommandResponse")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.HandleCommandResponse(c, command, args, response, builtIn)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) HandleCommandResponsePost(c request.CTX, command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HandleCommandResponsePost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.HandleCommandResponsePost(c, command, args, response, builtIn)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) HandleCommandWebhook(c *request.Context, hookID string, response *model.CommandResponse) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HandleCommandWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.HandleCommandWebhook(c, hookID, response)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) HandleImages(previewPathList []string, thumbnailPathList []string, fileData [][]byte) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HandleImages")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.HandleImages(previewPathList, thumbnailPathList, fileData)
}
func (a *OpenTracingAppLayer) HandleIncomingWebhook(c *request.Context, hookID string, req *model.IncomingWebhookRequest) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HandleIncomingWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.HandleIncomingWebhook(c, hookID, req)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) HandleMessageExportConfig(cfg *model.Config, appCfg *model.Config) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HandleMessageExportConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.HandleMessageExportConfig(cfg, appCfg)
}
func (a *OpenTracingAppLayer) HasBoardProduct() (bool, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasBoardProduct")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.HasBoardProduct()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) HasPermissionTo(askingUserId string, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasPermissionTo")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.HasPermissionTo(askingUserId, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) HasPermissionToChannel(c request.CTX, askingUserId string, channelID string, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasPermissionToChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.HasPermissionToChannel(c, askingUserId, channelID, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) HasPermissionToChannelByPost(askingUserId string, postID string, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasPermissionToChannelByPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.HasPermissionToChannelByPost(askingUserId, postID, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) HasPermissionToReadChannel(c request.CTX, userID string, channel *model.Channel) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasPermissionToReadChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.HasPermissionToReadChannel(c, userID, channel)
return resultVar0
}
func (a *OpenTracingAppLayer) HasPermissionToTeam(askingUserId string, teamID string, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasPermissionToTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.HasPermissionToTeam(askingUserId, teamID, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) HasPermissionToUser(askingUserId string, userID string) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasPermissionToUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.HasPermissionToUser(askingUserId, userID)
return resultVar0
}
func (a *OpenTracingAppLayer) HasRemote(channelID string, remoteID string) (bool, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasRemote")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.HasRemote(channelID, remoteID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) HasSharedChannel(channelID string) (bool, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasSharedChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.HasSharedChannel(channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) HooksManager() *product.HooksManager {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HooksManager")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.HooksManager()
return resultVar0
}
func (a *OpenTracingAppLayer) HubRegister(webConn *platform.WebConn) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HubRegister")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.HubRegister(webConn)
}
func (a *OpenTracingAppLayer) HubUnregister(webConn *platform.WebConn) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HubUnregister")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.HubUnregister(webConn)
}
func (a *OpenTracingAppLayer) ImageProxyAdder() func(string) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ImageProxyAdder")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ImageProxyAdder()
return resultVar0
}
func (a *OpenTracingAppLayer) ImageProxyRemover() (f func(string) string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ImageProxyRemover")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ImageProxyRemover()
return resultVar0
}
func (a *OpenTracingAppLayer) ImportPermissions(jsonl io.Reader) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ImportPermissions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ImportPermissions(jsonl)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) InitPlugins(c *request.Context, pluginDir string, webappPluginDir string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InitPlugins")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.InitPlugins(c, pluginDir, webappPluginDir)
}
func (a *OpenTracingAppLayer) InstallPlugin(pluginFile io.ReadSeeker, replace bool) (*model.Manifest, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InstallPlugin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.InstallPlugin(pluginFile, replace)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) InvalidateAllEmailInvites() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InvalidateAllEmailInvites")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.InvalidateAllEmailInvites()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) InvalidateAllResendInviteEmailJobs() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InvalidateAllResendInviteEmailJobs")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.InvalidateAllResendInviteEmailJobs()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) InvalidateCacheForUser(userID string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InvalidateCacheForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.InvalidateCacheForUser(userID)
}
func (a *OpenTracingAppLayer) InviteGuestsToChannels(teamID string, guestsInvite *model.GuestsInvite, senderId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InviteGuestsToChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.InviteGuestsToChannels(teamID, guestsInvite, senderId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) InviteGuestsToChannelsGracefully(teamID string, guestsInvite *model.GuestsInvite, senderId string) ([]*model.EmailInviteWithError, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InviteGuestsToChannelsGracefully")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.InviteGuestsToChannelsGracefully(teamID, guestsInvite, senderId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) InviteNewUsersToTeam(emailList []string, teamID string, senderId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InviteNewUsersToTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.InviteNewUsersToTeam(emailList, teamID, senderId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) InviteNewUsersToTeamGracefully(memberInvite *model.MemberInvite, teamID string, senderId string, reminderInterval string) ([]*model.EmailInviteWithError, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InviteNewUsersToTeamGracefully")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.InviteNewUsersToTeamGracefully(memberInvite, teamID, senderId, reminderInterval)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) IsCRTEnabledForUser(c request.CTX, userID string) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsCRTEnabledForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.IsCRTEnabledForUser(c, userID)
return resultVar0
}
func (a *OpenTracingAppLayer) IsFirstAdmin(user *model.User) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsFirstAdmin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.IsFirstAdmin(user)
return resultVar0
}
func (a *OpenTracingAppLayer) IsFirstUserAccount() bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsFirstUserAccount")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.IsFirstUserAccount()
return resultVar0
}
func (a *OpenTracingAppLayer) IsLeader() bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsLeader")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.IsLeader()
return resultVar0
}
func (a *OpenTracingAppLayer) IsPasswordValid(password string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsPasswordValid")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.IsPasswordValid(password)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) IsPhase2MigrationCompleted() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsPhase2MigrationCompleted")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.IsPhase2MigrationCompleted()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) IsPluginActive(pluginName string) (bool, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsPluginActive")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.IsPluginActive(pluginName)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) IsUserSignUpAllowed() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsUserSignUpAllowed")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.IsUserSignUpAllowed()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) JoinChannel(c request.CTX, channel *model.Channel, userID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.JoinChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.JoinChannel(c, channel, userID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) JoinDefaultChannels(c request.CTX, teamID string, user *model.User, shouldBeAdmin bool, userRequestorId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.JoinDefaultChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.JoinDefaultChannels(c, teamID, user, shouldBeAdmin, userRequestorId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) JoinUserToTeam(c request.CTX, team *model.Team, user *model.User, userRequestorId string) (*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.JoinUserToTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.JoinUserToTeam(c, team, user, userRequestorId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) LeaveChannel(c request.CTX, channelID string, userID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.LeaveChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.LeaveChannel(c, channelID, userID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) LeaveTeam(c request.CTX, team *model.Team, user *model.User, requestorId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.LeaveTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.LeaveTeam(c, team, user, requestorId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) License() *model.License {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.License")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.License()
return resultVar0
}
func (a *OpenTracingAppLayer) LimitedClientConfig() map[string]string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.LimitedClientConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.LimitedClientConfig()
return resultVar0
}
func (a *OpenTracingAppLayer) ListAllCommands(teamID string, T i18n.TranslateFunc) ([]*model.Command, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListAllCommands")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ListAllCommands(teamID, T)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ListAutocompleteCommands(teamID string, T i18n.TranslateFunc) ([]*model.Command, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListAutocompleteCommands")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
span.SetTag("teamID", teamID)
defer span.Finish()
resultVar0, resultVar1 := a.app.ListAutocompleteCommands(teamID, T)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ListDirectory(path string) ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListDirectory")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ListDirectory(path)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ListDirectoryRecursively(path string) ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListDirectoryRecursively")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ListDirectoryRecursively(path)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ListExports() ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListExports")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ListExports()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ListImports() ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListImports")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ListImports()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ListPluginKeys(pluginID string, page int, perPage int) ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListPluginKeys")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ListPluginKeys(pluginID, page, perPage)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ListTeamCommands(teamID string) ([]*model.Command, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListTeamCommands")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ListTeamCommands(teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) LogAuditRec(rec *audit.Record, err error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.LogAuditRec")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.LogAuditRec(rec, err)
}
func (a *OpenTracingAppLayer) LogAuditRecWithLevel(rec *audit.Record, level mlog.Level, err error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.LogAuditRecWithLevel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.LogAuditRecWithLevel(rec, level, err)
}
func (a *OpenTracingAppLayer) LoginByOAuth(c *request.Context, service string, userData io.Reader, teamID string, tokenUser *model.User) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.LoginByOAuth")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.LoginByOAuth(c, service, userData, teamID, tokenUser)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) MakeAuditRecord(event string, initialStatus string) *audit.Record {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MakeAuditRecord")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.MakeAuditRecord(event, initialStatus)
return resultVar0
}
func (a *OpenTracingAppLayer) MakePermissionError(s *model.Session, permissions []*model.Permission) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MakePermissionError")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.MakePermissionError(s, permissions)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) MarkChannelAsUnreadFromPost(c request.CTX, postID string, userID string, collapsedThreadsSupported bool) (*model.ChannelUnreadAt, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MarkChannelAsUnreadFromPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.MarkChannelAsUnreadFromPost(c, postID, userID, collapsedThreadsSupported)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) MarkChannelsAsViewed(c request.CTX, channelIDs []string, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MarkChannelsAsViewed")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.MarkChannelsAsViewed(c, channelIDs, userID, currentSessionId, collapsedThreadsSupported)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) MaxPostSize() int {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MaxPostSize")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.MaxPostSize()
return resultVar0
}
func (a *OpenTracingAppLayer) MentionsToPublicChannels(c request.CTX, message string, teamID string) model.ChannelMentionMap {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MentionsToPublicChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.MentionsToPublicChannels(c, message, teamID)
return resultVar0
}
func (a *OpenTracingAppLayer) MentionsToTeamMembers(c request.CTX, message string, teamID string) model.UserMentionMap {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MentionsToTeamMembers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.MentionsToTeamMembers(c, message, teamID)
return resultVar0
}
func (a *OpenTracingAppLayer) MigrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MigrateFilenamesToFileInfos")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.MigrateFilenamesToFileInfos(post)
return resultVar0
}
func (a *OpenTracingAppLayer) MigrateIdLDAP(toAttribute string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MigrateIdLDAP")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.MigrateIdLDAP(toAttribute)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) MoveChannel(c request.CTX, team *model.Team, channel *model.Channel, user *model.User) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MoveChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.MoveChannel(c, team, channel, user)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) MoveCommand(team *model.Team, command *model.Command) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MoveCommand")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.MoveCommand(team, command)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) MoveFile(oldPath string, newPath string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MoveFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.MoveFile(oldPath, newPath)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) NewPluginAPI(c *request.Context, manifest *model.Manifest) plugin.API {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NewPluginAPI")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.NewPluginAPI(c, manifest)
return resultVar0
}
func (a *OpenTracingAppLayer) NewWebConn(cfg *platform.WebConnConfig) *platform.WebConn {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NewWebConn")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.NewWebConn(cfg)
return resultVar0
}
func (a *OpenTracingAppLayer) NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, forceAck bool, isBot bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NotifyAndSetWarnMetricAck")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.NotifyAndSetWarnMetricAck(warnMetricId, sender, forceAck, isBot)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) NotifySelfHostedSignupProgress(progress string, userId string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NotifySelfHostedSignupProgress")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.NotifySelfHostedSignupProgress(progress, userId)
}
func (a *OpenTracingAppLayer) NotifySessionsExpired() error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NotifySessionsExpired")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.NotifySessionsExpired()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) NotifySharedChannelUserUpdate(user *model.User) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NotifySharedChannelUserUpdate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.NotifySharedChannelUserUpdate(user)
}
func (a *OpenTracingAppLayer) OpenInteractiveDialog(request model.OpenDialogRequest) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.OpenInteractiveDialog")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.OpenInteractiveDialog(request)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) OriginChecker() func(*http.Request) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.OriginChecker")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.OriginChecker()
return resultVar0
}
func (a *OpenTracingAppLayer) OverrideIconURLIfEmoji(c request.CTX, post *model.Post) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.OverrideIconURLIfEmoji")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.OverrideIconURLIfEmoji(c, post)
}
func (a *OpenTracingAppLayer) PatchBot(botUserId string, botPatch *model.BotPatch) (*model.Bot, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchBot")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PatchBot(botUserId, botPatch)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PatchChannel(c request.CTX, channel *model.Channel, patch *model.ChannelPatch, userID string) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PatchChannel(c, channel, patch, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PatchChannelModerationsForChannel(c request.CTX, channel *model.Channel, channelModerationsPatch []*model.ChannelModerationPatch) ([]*model.ChannelModeration, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchChannelModerationsForChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PatchChannelModerationsForChannel(c, channel, channelModerationsPatch)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PatchPost(c *request.Context, postID string, patch *model.PostPatch) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PatchPost(c, postID, patch)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PatchRetentionPolicy(patch *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PatchRetentionPolicy(patch)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PatchRole(role *model.Role, patch *model.RolePatch) (*model.Role, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchRole")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PatchRole(role, patch)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PatchScheme(scheme *model.Scheme, patch *model.SchemePatch) (*model.Scheme, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PatchScheme(scheme, patch)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PatchTeam(teamID string, patch *model.TeamPatch) (*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PatchTeam(teamID, patch)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PatchUser(c request.CTX, userID string, patch *model.UserPatch, asAdmin bool) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PatchUser(c, userID, patch, asAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PermanentDeleteAllUsers(c *request.Context) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PermanentDeleteAllUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PermanentDeleteAllUsers(c)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PermanentDeleteBot(botUserId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PermanentDeleteBot")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PermanentDeleteBot(botUserId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PermanentDeleteChannel(c request.CTX, channel *model.Channel) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PermanentDeleteChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PermanentDeleteChannel(c, channel)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PermanentDeleteTeam(c request.CTX, team *model.Team) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PermanentDeleteTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PermanentDeleteTeam(c, team)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PermanentDeleteTeamId(c request.CTX, teamID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PermanentDeleteTeamId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PermanentDeleteTeamId(c, teamID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PermanentDeleteUser(c *request.Context, user *model.User) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PermanentDeleteUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PermanentDeleteUser(c, user)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PopulateWebConnConfig(s *model.Session, cfg *platform.WebConnConfig, seqVal string) (*platform.WebConnConfig, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PopulateWebConnConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PopulateWebConnConfig(s, cfg, seqVal)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PostActionCookieSecret() []byte {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PostActionCookieSecret")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PostActionCookieSecret()
return resultVar0
}
func (a *OpenTracingAppLayer) PostAddToChannelMessage(c request.CTX, user *model.User, addedUser *model.User, channel *model.Channel, postRootId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PostAddToChannelMessage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PostAddToChannelMessage(c, user, addedUser, channel, postRootId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PostCountsByDuration(c request.CTX, channelIDs []string, sinceUnixMillis int64, userID *string, grouping model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PostCountsByDuration")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PostCountsByDuration(c, channelIDs, sinceUnixMillis, userID, grouping, groupingLocation)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PostPatchWithProxyRemovedFromImageURLs(patch *model.PostPatch) *model.PostPatch {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PostPatchWithProxyRemovedFromImageURLs")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PostPatchWithProxyRemovedFromImageURLs(patch)
return resultVar0
}
func (a *OpenTracingAppLayer) PostUpdateChannelDisplayNameMessage(c request.CTX, userID string, channel *model.Channel, oldChannelDisplayName string, newChannelDisplayName string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PostUpdateChannelDisplayNameMessage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PostUpdateChannelDisplayNameMessage(c, userID, channel, oldChannelDisplayName, newChannelDisplayName)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PostUpdateChannelHeaderMessage(c request.CTX, userID string, channel *model.Channel, oldChannelHeader string, newChannelHeader string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PostUpdateChannelHeaderMessage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PostUpdateChannelHeaderMessage(c, userID, channel, oldChannelHeader, newChannelHeader)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PostUpdateChannelPurposeMessage(c request.CTX, userID string, channel *model.Channel, oldChannelPurpose string, newChannelPurpose string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PostUpdateChannelPurposeMessage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PostUpdateChannelPurposeMessage(c, userID, channel, oldChannelPurpose, newChannelPurpose)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PostWithProxyAddedToImageURLs(post *model.Post) *model.Post {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PostWithProxyAddedToImageURLs")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PostWithProxyAddedToImageURLs(post)
return resultVar0
}
func (a *OpenTracingAppLayer) PostWithProxyRemovedFromImageURLs(post *model.Post) *model.Post {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PostWithProxyRemovedFromImageURLs")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PostWithProxyRemovedFromImageURLs(post)
return resultVar0
}
func (a *OpenTracingAppLayer) PreparePostForClient(c request.CTX, originalPost *model.Post, isNewPost bool, isEditPost bool, includePriority bool) *model.Post {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PreparePostForClient")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PreparePostForClient(c, originalPost, isNewPost, isEditPost, includePriority)
return resultVar0
}
func (a *OpenTracingAppLayer) PreparePostForClientWithEmbedsAndImages(c request.CTX, originalPost *model.Post, isNewPost bool, isEditPost bool, includePriority bool) *model.Post {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PreparePostForClientWithEmbedsAndImages")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PreparePostForClientWithEmbedsAndImages(c, originalPost, isNewPost, isEditPost, includePriority)
return resultVar0
}
func (a *OpenTracingAppLayer) PreparePostListForClient(c request.CTX, originalList *model.PostList) *model.PostList {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PreparePostListForClient")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PreparePostListForClient(c, originalList)
return resultVar0
}
func (a *OpenTracingAppLayer) ProcessSlackAttachments(attachments []*model.SlackAttachment) []*model.SlackAttachment {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ProcessSlackAttachments")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ProcessSlackAttachments(attachments)
return resultVar0
}
func (a *OpenTracingAppLayer) ProcessSlackText(text string) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ProcessSlackText")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ProcessSlackText(text)
return resultVar0
}
func (a *OpenTracingAppLayer) PromoteGuestToUser(c *request.Context, user *model.User, requestorId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PromoteGuestToUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PromoteGuestToUser(c, user, requestorId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) Publish(message *model.WebSocketEvent) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.Publish")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.Publish(message)
}
func (a *OpenTracingAppLayer) PublishUserTyping(userID string, channelID string, parentId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PublishUserTyping")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PublishUserTyping(userID, channelID, parentId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PurgeBleveIndexes() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PurgeBleveIndexes")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PurgeBleveIndexes()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) PurgeElasticsearchIndexes() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PurgeElasticsearchIndexes")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.PurgeElasticsearchIndexes()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) QueryLogs(page int, perPage int, logFilter *model.LogFilter) (map[string][]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.QueryLogs")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.QueryLogs(page, perPage, logFilter)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ReadFile(path string) ([]byte, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ReadFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ReadFile(path)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RecycleDatabaseConnection() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RecycleDatabaseConnection")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.RecycleDatabaseConnection()
}
func (a *OpenTracingAppLayer) RegenCommandToken(cmd *model.Command) (*model.Command, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RegenCommandToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.RegenCommandToken(cmd)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RegenOutgoingWebhookToken(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RegenOutgoingWebhookToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.RegenOutgoingWebhookToken(hook)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RegenerateOAuthAppSecret(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RegenerateOAuthAppSecret")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.RegenerateOAuthAppSecret(app)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RegenerateTeamInviteId(teamID string) (*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RegenerateTeamInviteId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.RegenerateTeamInviteId(teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RegisterCollectionAndTopic(pluginID string, collectionType string, topicType string) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RegisterCollectionAndTopic")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RegisterCollectionAndTopic(pluginID, collectionType, topicType)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RegisterPluginCommand(pluginID string, command *model.Command) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RegisterPluginCommand")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RegisterPluginCommand(pluginID, command)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RegisterProductCommand(ProductID string, command *model.Command) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RegisterProductCommand")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RegisterProductCommand(ProductID, command)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ReloadConfig() error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ReloadConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ReloadConfig()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveAllDeactivatedMembersFromChannel(c request.CTX, channel *model.Channel) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveAllDeactivatedMembersFromChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveAllDeactivatedMembersFromChannel(c, channel)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveChannelsFromRetentionPolicy(policyID string, channelIDs []string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveChannelsFromRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveChannelsFromRetentionPolicy(policyID, channelIDs)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveConfigListener(id string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveConfigListener")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.RemoveConfigListener(id)
}
func (a *OpenTracingAppLayer) RemoveCustomStatus(c request.CTX, userID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveCustomStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveCustomStatus(c, userID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveDirectory(path string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveDirectory")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveDirectory(path)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveFile(path string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveFile(path)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveLdapPrivateCertificate() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveLdapPrivateCertificate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveLdapPrivateCertificate()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveLdapPublicCertificate() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveLdapPublicCertificate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveLdapPublicCertificate()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveRecentCustomStatus(userID string, status *model.CustomStatus) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveRecentCustomStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveRecentCustomStatus(userID, status)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveSamlIdpCertificate() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveSamlIdpCertificate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveSamlIdpCertificate()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveSamlPrivateCertificate() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveSamlPrivateCertificate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveSamlPrivateCertificate()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveSamlPublicCertificate() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveSamlPublicCertificate")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveSamlPublicCertificate()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveTeamIcon(teamID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveTeamIcon")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveTeamIcon(teamID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveTeamsFromRetentionPolicy(policyID string, teamIDs []string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveTeamsFromRetentionPolicy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveTeamsFromRetentionPolicy(policyID, teamIDs)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveUserFromChannel(c request.CTX, userIDToRemove string, removerUserId string, channel *model.Channel) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveUserFromChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveUserFromChannel(c, userIDToRemove, removerUserId, channel)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveUserFromTeam(c request.CTX, teamID string, userID string, requestorId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveUserFromTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveUserFromTeam(c, teamID, userID, requestorId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveUsersFromChannelNotMemberOfTeam(c request.CTX, remover *model.User, channel *model.Channel, team *model.Team) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveUsersFromChannelNotMemberOfTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveUsersFromChannelNotMemberOfTeam(c, remover, channel, team)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RenameChannel(c request.CTX, channel *model.Channel, newChannelName string, newDisplayName string) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RenameChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.RenameChannel(c, channel, newChannelName, newDisplayName)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RenameTeam(team *model.Team, newTeamName string, newDisplayName string) (*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RenameTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.RenameTeam(team, newTeamName, newDisplayName)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RequestLicenseAndAckWarnMetric(c *request.Context, warnMetricId string, isBot bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RequestLicenseAndAckWarnMetric")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RequestLicenseAndAckWarnMetric(c, warnMetricId, isBot)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ResetPasswordFromToken(c request.CTX, userSuppliedTokenString string, newPassword string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ResetPasswordFromToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ResetPasswordFromToken(c, userSuppliedTokenString, newPassword)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ResetPermissionsSystem() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ResetPermissionsSystem")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ResetPermissionsSystem()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ResetSamlAuthDataToEmail(includeDeleted bool, dryRun bool, userIDs []string) (numAffected int, appErr *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ResetSamlAuthDataToEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ResetSamlAuthDataToEmail(includeDeleted, dryRun, userIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RestoreChannel(c request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RestoreChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.RestoreChannel(c, channel, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RestoreGroup(groupID string) (*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RestoreGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.RestoreGroup(groupID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RestoreTeam(teamID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RestoreTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RestoreTeam(teamID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RestrictUsersGetByPermissions(userID string, options *model.UserGetOptions) (*model.UserGetOptions, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RestrictUsersGetByPermissions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.RestrictUsersGetByPermissions(userID, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) RestrictUsersSearchByPermissions(userID string, options *model.UserSearchOptions) (*model.UserSearchOptions, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RestrictUsersSearchByPermissions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.RestrictUsersSearchByPermissions(userID, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ReturnSessionToPool(session *model.Session) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ReturnSessionToPool")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.ReturnSessionToPool(session)
}
func (a *OpenTracingAppLayer) RevokeAccessToken(token string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RevokeAccessToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RevokeAccessToken(token)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RevokeAllSessions(userID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RevokeAllSessions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RevokeAllSessions(userID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RevokeSession(session *model.Session) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RevokeSession")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RevokeSession(session)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RevokeSessionById(sessionID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RevokeSessionById")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RevokeSessionById(sessionID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RevokeSessionsForDeviceId(userID string, deviceID string, currentSessionId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RevokeSessionsForDeviceId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RevokeSessionsForDeviceId(userID, deviceID, currentSessionId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RevokeSessionsFromAllUsers() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RevokeSessionsFromAllUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RevokeSessionsFromAllUsers()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RevokeUserAccessToken(token *model.UserAccessToken) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RevokeUserAccessToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RevokeUserAccessToken(token)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RolesGrantPermission(roleNames []string, permissionId string) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RolesGrantPermission")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RolesGrantPermission(roleNames, permissionId)
return resultVar0
}
func (a *OpenTracingAppLayer) SanitizePostListMetadataForUser(c request.CTX, postList *model.PostList, userID string) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SanitizePostListMetadataForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SanitizePostListMetadataForUser(c, postList, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SanitizePostMetadataForUser(c request.CTX, post *model.Post, userID string) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SanitizePostMetadataForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SanitizePostMetadataForUser(c, post, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SanitizeProfile(user *model.User, asAdmin bool) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SanitizeProfile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SanitizeProfile(user, asAdmin)
}
func (a *OpenTracingAppLayer) SanitizeTeam(session model.Session, team *model.Team) *model.Team {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SanitizeTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SanitizeTeam(session, team)
return resultVar0
}
func (a *OpenTracingAppLayer) SanitizeTeams(session model.Session, teams []*model.Team) []*model.Team {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SanitizeTeams")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SanitizeTeams(session, teams)
return resultVar0
}
func (a *OpenTracingAppLayer) SaveAcknowledgementForPost(c *request.Context, postID string, userID string) (*model.PostAcknowledgement, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveAcknowledgementForPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SaveAcknowledgementForPost(c, postID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SaveAdminNotification(userId string, notifyData *model.NotifyAdminToUpgradeRequest) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveAdminNotification")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SaveAdminNotification(userId, notifyData)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SaveAdminNotifyData(data *model.NotifyAdminData) (*model.NotifyAdminData, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveAdminNotifyData")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SaveAdminNotifyData(data)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SaveBrandImage(imageData *multipart.FileHeader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveBrandImage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SaveBrandImage(imageData)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SaveComplianceReport(job *model.Compliance) (*model.Compliance, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveComplianceReport")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SaveComplianceReport(job)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bool) (*model.Config, *model.Config, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.SaveConfig(newCfg, sendConfigChangeClusterMessage)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) SaveReactionForPost(c *request.Context, reaction *model.Reaction) (*model.Reaction, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveReactionForPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SaveReactionForPost(c, reaction)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SaveSharedChannel(c request.CTX, sc *model.SharedChannel) (*model.SharedChannel, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveSharedChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SaveSharedChannel(c, sc)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SaveSharedChannelRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveSharedChannelRemote")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SaveSharedChannelRemote(remote)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SaveUserTermsOfService(userID string, termsOfServiceId string, accepted bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveUserTermsOfService")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SaveUserTermsOfService(userID, termsOfServiceId, accepted)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SchemesIterator(scope string, batchSize int) func() []*model.Scheme {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SchemesIterator")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SchemesIterator(scope, batchSize)
return resultVar0
}
func (a *OpenTracingAppLayer) SearchAllChannels(c request.CTX, term string, opts model.ChannelSearchOpts) (model.ChannelListWithTeamData, int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchAllChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.SearchAllChannels(c, term, opts)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) SearchAllTeams(searchOpts *model.TeamSearch) ([]*model.Team, int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchAllTeams")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.SearchAllTeams(searchOpts)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) SearchArchivedChannels(c request.CTX, teamID string, term string, userID string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchArchivedChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchArchivedChannels(c, teamID, term, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchChannels(c request.CTX, teamID string, term string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchChannels(c, teamID, term)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchChannelsForUser(c request.CTX, userID string, teamID string, term string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchChannelsForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchChannelsForUser(c, userID, teamID, term)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchChannelsUserNotIn(c request.CTX, teamID string, userID string, term string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchChannelsUserNotIn")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchChannelsUserNotIn(c, teamID, userID, term)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchEmoji(c request.CTX, name string, prefixOnly bool, limit int) ([]*model.Emoji, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchEmoji")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchEmoji(c, name, prefixOnly, limit)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchEngine() *searchengine.Broker {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchEngine")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SearchEngine()
return resultVar0
}
func (a *OpenTracingAppLayer) SearchFilesInTeamForUser(c *request.Context, terms string, userId string, teamId string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page int, perPage int, modifier string) (*model.FileInfoList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchFilesInTeamForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchFilesInTeamForUser(c, terms, userId, teamId, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, modifier)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchGroupChannels(c request.CTX, userID string, term string) (model.ChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchGroupChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchGroupChannels(c, userID, term)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchPostsForUser(c *request.Context, terms string, userID string, teamID string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page int, perPage int, modifier string) (*model.PostSearchResults, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchPostsForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchPostsForUser(c, terms, userID, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, modifier)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchPostsInTeam(teamID string, paramsList []*model.SearchParams) (*model.PostList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchPostsInTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchPostsInTeam(teamID, paramsList)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchPrivateTeams(searchOpts *model.TeamSearch) ([]*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchPrivateTeams")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchPrivateTeams(searchOpts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchPublicTeams(searchOpts *model.TeamSearch) ([]*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchPublicTeams")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchPublicTeams(searchOpts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchUserAccessTokens(term string) ([]*model.UserAccessToken, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchUserAccessTokens")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchUserAccessTokens(term)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchUsers(props *model.UserSearch, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchUsers(props, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchUsersInChannel(channelID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchUsersInChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchUsersInChannel(channelID, term, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchUsersInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchUsersInGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchUsersInGroup(groupID, term, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchUsersInTeam(teamID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchUsersInTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchUsersInTeam(teamID, term, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchUsersNotInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchUsersNotInChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchUsersNotInChannel(teamID, channelID, term, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchUsersNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchUsersNotInGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchUsersNotInGroup(groupID, term, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchUsersNotInTeam(notInTeamId string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchUsersNotInTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchUsersNotInTeam(notInTeamId, term, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SearchUsersWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchUsersWithoutTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SearchUsersWithoutTeam(term, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SendAckToPushProxy(ack *model.PushNotificationAck) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendAckToPushProxy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SendAckToPushProxy(ack)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SendAutoResponse(c request.CTX, channel *model.Channel, receiver *model.User, post *model.Post) (bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendAutoResponse")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SendAutoResponse(c, channel, receiver, post)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SendAutoResponseIfNecessary(c request.CTX, channel *model.Channel, sender *model.User, post *model.Post) (bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendAutoResponseIfNecessary")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SendAutoResponseIfNecessary(c, channel, sender, post)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SendDelinquencyEmail(emailToSend model.DelinquencyEmail) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendDelinquencyEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SendDelinquencyEmail(emailToSend)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SendEmailVerification(user *model.User, newEmail string, redirect string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendEmailVerification")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SendEmailVerification(user, newEmail, redirect)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SendEphemeralPost(c request.CTX, userID string, post *model.Post) *model.Post {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendEphemeralPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SendEphemeralPost(c, userID, post)
return resultVar0
}
func (a *OpenTracingAppLayer) SendNoCardPaymentFailedEmail() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendNoCardPaymentFailedEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SendNoCardPaymentFailedEmail()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SendNotifications(c request.CTX, post *model.Post, team *model.Team, channel *model.Channel, sender *model.User, parentPostList *model.PostList, setOnline bool) ([]string, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendNotifications")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SendNotifications(c, post, team, channel, sender, parentPostList, setOnline)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SendNotifyAdminPosts(c *request.Context, workspaceName string, currentSKU string, trial bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendNotifyAdminPosts")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SendNotifyAdminPosts(c, workspaceName, currentSKU, trial)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SendPasswordReset(email string, siteURL string) (bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendPasswordReset")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SendPasswordReset(email, siteURL)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SendPaymentFailedEmail(failedPayment *model.FailedPayment) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendPaymentFailedEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SendPaymentFailedEmail(failedPayment)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SendSubscriptionHistoryEvent(userID string) (*model.SubscriptionHistory, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendSubscriptionHistoryEvent")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SendSubscriptionHistoryEvent(userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SendTestPushNotification(deviceID string) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendTestPushNotification")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SendTestPushNotification(deviceID)
return resultVar0
}
func (a *OpenTracingAppLayer) SendUpgradeConfirmationEmail(isYearly bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendUpgradeConfirmationEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SendUpgradeConfirmationEmail(isYearly)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ServeInterPluginRequest(w http.ResponseWriter, r *http.Request, sourcePluginId string, destinationPluginId string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ServeInterPluginRequest")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.ServeInterPluginRequest(w, r, sourcePluginId, destinationPluginId)
}
func (a *OpenTracingAppLayer) SessionHasPermissionTo(session model.Session, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionTo")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionTo(session, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToAny(session model.Session, permissions []*model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToAny")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToAny(session, permissions)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToCategory(c request.CTX, session model.Session, userID string, teamID string, categoryId string) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToCategory")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToCategory(c, session, userID, teamID, categoryId)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToChannel(c request.CTX, session model.Session, channelID string, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToChannel(c, session, channelID, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToChannelByPost(session model.Session, postID string, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToChannelByPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToChannelByPost(session, postID, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToChannels(c request.CTX, session model.Session, channelIDs []string, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToChannels(c, session, channelIDs, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToCreateJob(session model.Session, job *model.Job) (bool, *model.Permission) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToCreateJob")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SessionHasPermissionToCreateJob(session, job)
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SessionHasPermissionToGroup(session model.Session, groupID string, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToGroup(session, groupID, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToManageBot(session model.Session, botUserId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToManageBot")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToManageBot(session, botUserId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToReadJob(session model.Session, jobType string) (bool, *model.Permission) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToReadJob")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SessionHasPermissionToReadJob(session, jobType)
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SessionHasPermissionToTeam(session model.Session, teamID string, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToTeam(session, teamID, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToTeams(c request.CTX, session model.Session, teamIDs []string, permission *model.Permission) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToTeams")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToTeams(c, session, teamIDs, permission)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToUser(session model.Session, userID string) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToUser(session, userID)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionHasPermissionToUserOrBot(session model.Session, userID string) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionHasPermissionToUserOrBot")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionHasPermissionToUserOrBot(session, userID)
return resultVar0
}
func (a *OpenTracingAppLayer) SessionIsRegistered(session model.Session) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionIsRegistered")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SessionIsRegistered(session)
return resultVar0
}
func (a *OpenTracingAppLayer) SetActiveChannel(c request.CTX, userID string, channelID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetActiveChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetActiveChannel(c, userID, channelID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetAutoResponderStatus(user *model.User, oldNotifyProps model.StringMap) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetAutoResponderStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetAutoResponderStatus(user, oldNotifyProps)
}
func (a *OpenTracingAppLayer) SetChannels(ch *app.Channels) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetChannels(ch)
}
func (a *OpenTracingAppLayer) SetCustomStatus(c request.CTX, userID string, cs *model.CustomStatus) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetCustomStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetCustomStatus(c, userID, cs)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetDefaultProfileImage(c request.CTX, user *model.User) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetDefaultProfileImage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetDefaultProfileImage(c, user)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetPhase2PermissionsMigrationStatus(isComplete bool) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetPhase2PermissionsMigrationStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetPhase2PermissionsMigrationStatus(isComplete)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetPluginKey(pluginID string, key string, value []byte) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetPluginKey")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetPluginKey(pluginID, key, value)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetPluginKeyWithExpiry(pluginID string, key string, value []byte, expireInSeconds int64) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetPluginKeyWithExpiry")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetPluginKeyWithExpiry(pluginID, key, value, expireInSeconds)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetPluginKeyWithOptions(pluginID string, key string, value []byte, options model.PluginKVSetOptions) (bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetPluginKeyWithOptions")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SetPluginKeyWithOptions(pluginID, key, value, options)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SetPostReminder(postID string, userID string, targetTime int64) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetPostReminder")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetPostReminder(postID, userID, targetTime)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetProfileImage(c request.CTX, userID string, imageData *multipart.FileHeader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetProfileImage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetProfileImage(c, userID, imageData)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetProfileImageFromFile(c request.CTX, userID string, file io.Reader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetProfileImageFromFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetProfileImageFromFile(c, userID, file)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetProfileImageFromMultiPartFile(c request.CTX, userID string, file multipart.File) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetProfileImageFromMultiPartFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetProfileImageFromMultiPartFile(c, userID, file)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetRemoteClusterLastPingAt(remoteClusterId string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetRemoteClusterLastPingAt")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetRemoteClusterLastPingAt(remoteClusterId)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetSamlIdpCertificateFromMetadata(data []byte) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetSamlIdpCertificateFromMetadata")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetSamlIdpCertificateFromMetadata(data)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetSearchEngine(se *searchengine.Broker) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetSearchEngine")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetSearchEngine(se)
}
func (a *OpenTracingAppLayer) SetSessionExpireInHours(session *model.Session, hours int) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetSessionExpireInHours")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetSessionExpireInHours(session, hours)
}
func (a *OpenTracingAppLayer) SetStatusAwayIfNeeded(userID string, manual bool) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetStatusAwayIfNeeded")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetStatusAwayIfNeeded(userID, manual)
}
func (a *OpenTracingAppLayer) SetStatusDoNotDisturb(userID string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetStatusDoNotDisturb")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetStatusDoNotDisturb(userID)
}
func (a *OpenTracingAppLayer) SetStatusDoNotDisturbTimed(userId string, endtime int64) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetStatusDoNotDisturbTimed")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetStatusDoNotDisturbTimed(userId, endtime)
}
func (a *OpenTracingAppLayer) SetStatusLastActivityAt(userID string, activityAt int64) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetStatusLastActivityAt")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetStatusLastActivityAt(userID, activityAt)
}
func (a *OpenTracingAppLayer) SetStatusOffline(userID string, manual bool) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetStatusOffline")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetStatusOffline(userID, manual)
}
func (a *OpenTracingAppLayer) SetStatusOnline(userID string, manual bool) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetStatusOnline")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetStatusOnline(userID, manual)
}
func (a *OpenTracingAppLayer) SetStatusOutOfOffice(userID string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetStatusOutOfOffice")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SetStatusOutOfOffice(userID)
}
func (a *OpenTracingAppLayer) SetTeamIcon(teamID string, imageData *multipart.FileHeader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetTeamIcon")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetTeamIcon(teamID, imageData)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetTeamIconFromFile(team *model.Team, file io.Reader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetTeamIconFromFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetTeamIconFromFile(team, file)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SetTeamIconFromMultiPartFile(teamID string, file multipart.File) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetTeamIconFromMultiPartFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SetTeamIconFromMultiPartFile(teamID, file)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SlackImport(c *request.Context, fileData multipart.File, fileSize int64, teamID string) (*model.AppError, *bytes.Buffer) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SlackImport")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SlackImport(c, fileData, fileSize, teamID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SoftDeleteAllTeamsExcept(teamID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SoftDeleteAllTeamsExcept")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SoftDeleteAllTeamsExcept(teamID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SoftDeleteTeam(teamID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SoftDeleteTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SoftDeleteTeam(teamID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SubmitInteractiveDialog(c *request.Context, request model.SubmitDialogRequest) (*model.SubmitDialogResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SubmitInteractiveDialog")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SubmitInteractiveDialog(c, request)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SwitchEmailToLdap(email string, password string, code string, ldapLoginId string, ldapPassword string) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SwitchEmailToLdap")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SwitchEmailToLdap(email, password, code, ldapLoginId, ldapPassword)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SwitchEmailToOAuth(w http.ResponseWriter, r *http.Request, email string, password string, code string, service string) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SwitchEmailToOAuth")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SwitchEmailToOAuth(w, r, email, password, code, service)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SwitchLdapToEmail(ldapPassword string, code string, email string, newPassword string) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SwitchLdapToEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SwitchLdapToEmail(ldapPassword, code, email, newPassword)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SwitchOAuthToEmail(email string, password string, requesterId string) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SwitchOAuthToEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SwitchOAuthToEmail(email, password, requesterId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SyncLdap(includeRemovedMembers bool) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SyncLdap")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SyncLdap(includeRemovedMembers)
}
func (a *OpenTracingAppLayer) SyncPlugins() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SyncPlugins")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SyncPlugins()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SyncRolesAndMembership(c request.CTX, syncableID string, syncableType model.GroupSyncableType, includeRemovedMembers bool) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SyncRolesAndMembership")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.SyncRolesAndMembership(c, syncableID, syncableType, includeRemovedMembers)
}
func (a *OpenTracingAppLayer) SyncSyncableRoles(syncableID string, syncableType model.GroupSyncableType) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SyncSyncableRoles")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SyncSyncableRoles(syncableID, syncableType)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TeamMembersMinusGroupMembers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.TeamMembersMinusGroupMembers(teamID, groupIDs, page, perPage)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) TeamMembersToAdd(since int64, teamID *string, includeRemovedMembers bool) ([]*model.UserTeamIDPair, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TeamMembersToAdd")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.TeamMembersToAdd(since, teamID, includeRemovedMembers)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) TeamMembersToRemove(teamID *string) ([]*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TeamMembersToRemove")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.TeamMembersToRemove(teamID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) TelemetryId() string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TelemetryId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.TelemetryId()
return resultVar0
}
func (a *OpenTracingAppLayer) TestElasticsearch(cfg *model.Config) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TestElasticsearch")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.TestElasticsearch(cfg)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) TestEmail(userID string, cfg *model.Config) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TestEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.TestEmail(userID, cfg)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) TestFileStoreConnection() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TestFileStoreConnection")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.TestFileStoreConnection()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) TestFileStoreConnectionWithConfig(cfg *model.FileSettings) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TestFileStoreConnectionWithConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.TestFileStoreConnectionWithConfig(cfg)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) TestLdap() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TestLdap")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.TestLdap()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) TestSiteURL(siteURL string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TestSiteURL")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.TestSiteURL(siteURL)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ToggleMuteChannel(c request.CTX, channelID string, userID string) (*model.ChannelMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ToggleMuteChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ToggleMuteChannel(c, channelID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) TotalWebsocketConnections() int {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TotalWebsocketConnections")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.TotalWebsocketConnections()
return resultVar0
}
func (a *OpenTracingAppLayer) TriggerWebhook(c request.CTX, payload *model.OutgoingWebhookPayload, hook *model.OutgoingWebhook, post *model.Post, channel *model.Channel) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.TriggerWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.TriggerWebhook(c, payload, hook, post, channel)
}
func (a *OpenTracingAppLayer) UnregisterPluginCommand(pluginID string, teamID string, trigger string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UnregisterPluginCommand")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.UnregisterPluginCommand(pluginID, teamID, trigger)
}
func (a *OpenTracingAppLayer) UpdateActive(c request.CTX, user *model.User, active bool) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateActive")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateActive(c, user, active)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateBotActive(c request.CTX, botUserId string, active bool) (*model.Bot, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateBotActive")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateBotActive(c, botUserId, active)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateBotOwner(botUserId string, newOwnerId string) (*model.Bot, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateBotOwner")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateBotOwner(botUserId, newOwnerId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateChannel(c request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateChannel(c, channel)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateChannelMemberNotifyProps(c request.CTX, data map[string]string, channelID string, userID string) (*model.ChannelMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateChannelMemberNotifyProps")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateChannelMemberNotifyProps(c, data, channelID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateChannelMemberRoles(c request.CTX, channelID string, userID string, newRoles string) (*model.ChannelMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateChannelMemberRoles")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateChannelMemberRoles(c, channelID, userID, newRoles)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateChannelMemberSchemeRoles(c request.CTX, channelID string, userID string, isSchemeGuest bool, isSchemeUser bool, isSchemeAdmin bool) (*model.ChannelMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateChannelMemberSchemeRoles")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateChannelMemberSchemeRoles(c, channelID, userID, isSchemeGuest, isSchemeUser, isSchemeAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateChannelPrivacy(c request.CTX, oldChannel *model.Channel, user *model.User) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateChannelPrivacy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateChannelPrivacy(c, oldChannel, user)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateChannelScheme(c request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateChannelScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateChannelScheme(c, channel)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateCommand(oldCmd *model.Command, updatedCmd *model.Command) (*model.Command, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateCommand")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateCommand(oldCmd, updatedCmd)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateConfig(f func(*model.Config)) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateConfig")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.UpdateConfig(f)
}
func (a *OpenTracingAppLayer) UpdateDNDStatusOfUsers() {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateDNDStatusOfUsers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.UpdateDNDStatusOfUsers()
}
func (a *OpenTracingAppLayer) UpdateDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateDraft")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateDraft(c, draft, connectionID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateEphemeralPost(c request.CTX, userID string, post *model.Post) *model.Post {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateEphemeralPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateEphemeralPost(c, userID, post)
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateExpiredDNDStatuses() ([]*model.Status, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateExpiredDNDStatuses")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateExpiredDNDStatuses()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateGroup(group *model.Group) (*model.Group, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateGroup(group)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateGroupSyncable")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateGroupSyncable(groupSyncable)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateHashedPassword(user *model.User, newHashedPassword string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateHashedPassword")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateHashedPassword(user, newHashedPassword)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateHashedPasswordByUserId(userID string, newHashedPassword string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateHashedPasswordByUserId")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateHashedPasswordByUserId(userID, newHashedPassword)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateIncomingWebhook(oldHook *model.IncomingWebhook, updatedHook *model.IncomingWebhook) (*model.IncomingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateIncomingWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateIncomingWebhook(oldHook, updatedHook)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateMfa(c request.CTX, activate bool, userID string, token string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateMfa")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateMfa(c, activate, userID, token)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateMobileAppBadge(userID string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateMobileAppBadge")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.UpdateMobileAppBadge(userID)
}
func (a *OpenTracingAppLayer) UpdateOAuthApp(oldApp *model.OAuthApp, updatedApp *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateOAuthApp")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateOAuthApp(oldApp, updatedApp)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateOAuthUserAttrs(userData io.Reader, user *model.User, provider einterfaces.OAuthProvider, service string, tokenUser *model.User) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateOAuthUserAttrs")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateOAuthUserAttrs(userData, user, provider, service, tokenUser)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateOutgoingWebhook(c request.CTX, oldHook *model.OutgoingWebhook, updatedHook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateOutgoingWebhook")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateOutgoingWebhook(c, oldHook, updatedHook)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdatePassword(user *model.User, newPassword string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdatePassword")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdatePassword(user, newPassword)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdatePasswordAsUser(c request.CTX, userID string, currentPassword string, newPassword string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdatePasswordAsUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdatePasswordAsUser(c, userID, currentPassword, newPassword)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdatePasswordByUserIdSendEmail(c request.CTX, userID string, newPassword string, method string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdatePasswordByUserIdSendEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdatePasswordByUserIdSendEmail(c, userID, newPassword, method)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdatePasswordSendEmail(c request.CTX, user *model.User, newPassword string, method string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdatePasswordSendEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdatePasswordSendEmail(c, user, newPassword, method)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdatePost(c *request.Context, post *model.Post, safeUpdate bool) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdatePost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdatePost(c, post, safeUpdate)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdatePreferences(userID string, preferences model.Preferences) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdatePreferences")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdatePreferences(userID, preferences)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateProductNotices() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateProductNotices")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateProductNotices()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateRemoteCluster(rc *model.RemoteCluster) (*model.RemoteCluster, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateRemoteCluster")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateRemoteCluster(rc)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateRemoteClusterTopics(remoteClusterId string, topics string) (*model.RemoteCluster, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateRemoteClusterTopics")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateRemoteClusterTopics(remoteClusterId, topics)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateRole(role *model.Role) (*model.Role, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateRole")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateRole(role)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateScheme(scheme *model.Scheme) (*model.Scheme, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateScheme(scheme)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateSharedChannel(sc *model.SharedChannel) (*model.SharedChannel, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateSharedChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateSharedChannel(sc)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateSharedChannelRemoteCursor(id string, cursor model.GetPostsSinceForSyncCursor) error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateSharedChannelRemoteCursor")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateSharedChannelRemoteCursor(id, cursor)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateSidebarCategories(c request.CTX, userID string, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateSidebarCategories")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateSidebarCategories(c, userID, teamID, categories)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateSidebarCategoryOrder(c request.CTX, userID string, teamID string, categoryOrder []string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateSidebarCategoryOrder")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateSidebarCategoryOrder(c, userID, teamID, categoryOrder)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateTeam")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateTeam(team)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateTeamMemberRoles(teamID string, userID string, newRoles string) (*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateTeamMemberRoles")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateTeamMemberRoles(teamID, userID, newRoles)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateTeamMemberSchemeRoles(teamID string, userID string, isSchemeGuest bool, isSchemeUser bool, isSchemeAdmin bool) (*model.TeamMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateTeamMemberSchemeRoles")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateTeamMemberSchemeRoles(teamID, userID, isSchemeGuest, isSchemeUser, isSchemeAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateTeamPrivacy(teamID string, teamType string, allowOpenInvite bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateTeamPrivacy")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateTeamPrivacy(teamID, teamType, allowOpenInvite)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateTeamScheme(team *model.Team) (*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateTeamScheme")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateTeamScheme(team)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateThreadFollowForUser(userID string, teamID string, threadID string, state bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateThreadFollowForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateThreadFollowForUser(userID, teamID, threadID, state)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateThreadFollowForUserFromChannelAdd(c request.CTX, userID string, teamID string, threadID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateThreadFollowForUserFromChannelAdd")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateThreadFollowForUserFromChannelAdd(c, userID, teamID, threadID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateThreadReadForUser(c request.CTX, currentSessionId string, userID string, teamID string, threadID string, timestamp int64) (*model.ThreadResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateThreadReadForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateThreadReadForUser(c, currentSessionId, userID, teamID, threadID, timestamp)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateThreadReadForUserByPost(c request.CTX, currentSessionId string, userID string, teamID string, threadID string, postID string) (*model.ThreadResponse, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateThreadReadForUserByPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateThreadReadForUserByPost(c, currentSessionId, userID, teamID, threadID, postID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateThreadsReadForUser(userID string, teamID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateThreadsReadForUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateThreadsReadForUser(userID, teamID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateUser(c request.CTX, user *model.User, sendNotifications bool) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateUser(c, user, sendNotifications)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateUserActive(c request.CTX, userID string, active bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateUserActive")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateUserActive(c, userID, active)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateUserAsUser(c request.CTX, user *model.User, asAdmin bool) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateUserAsUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateUserAsUser(c, user, asAdmin)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateUserAuth(userID string, userAuth *model.UserAuth) (*model.UserAuth, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateUserAuth")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateUserAuth(userID, userAuth)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateUserRoles(c request.CTX, userID string, newRoles string, sendWebSocketEvent bool) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateUserRoles")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateUserRoles(c, userID, newRoles, sendWebSocketEvent)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateUserRolesWithUser(c request.CTX, user *model.User, newRoles string, sendWebSocketEvent bool) (*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateUserRolesWithUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateUserRolesWithUser(c, user, newRoles, sendWebSocketEvent)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateViewedProductNotices(userID string, noticeIds []string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateViewedProductNotices")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UpdateViewedProductNotices(userID, noticeIds)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UpdateViewedProductNoticesForNewUser(userID string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateViewedProductNoticesForNewUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.UpdateViewedProductNoticesForNewUser(userID)
}
func (a *OpenTracingAppLayer) UpdateWebConnUserActivity(session model.Session, activityAt int64) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateWebConnUserActivity")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.UpdateWebConnUserActivity(session, activityAt)
}
func (a *OpenTracingAppLayer) UploadData(c request.CTX, us *model.UploadSession, rd io.Reader) (*model.FileInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UploadData")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UploadData(c, us, rd)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UploadEmojiImage(c request.CTX, id string, imageData *multipart.FileHeader) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UploadEmojiImage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UploadEmojiImage(c, id, imageData)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) UploadFile(c request.CTX, data []byte, channelID string, filename string) (*model.FileInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UploadFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UploadFile(c, data, channelID, filename)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UploadFileX(c *request.Context, channelID string, name string, input io.Reader, opts ...func(*app.UploadFileTask)) (*model.FileInfo, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UploadFileX")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UploadFileX(c, channelID, name, input, opts...)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpsertDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpsertDraft")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpsertDraft(c, draft, connectionID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpsertGroupMember")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpsertGroupMember(groupID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpsertGroupMembers")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpsertGroupMembers(groupID, userIDs)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpsertGroupSyncable")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpsertGroupSyncable(groupSyncable)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UserAlreadyNotifiedOnRequiredFeature(user string, feature model.MattermostFeature) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UserAlreadyNotifiedOnRequiredFeature")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UserAlreadyNotifiedOnRequiredFeature(user, feature)
return resultVar0
}
func (a *OpenTracingAppLayer) UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UserCanSeeOtherUser")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UserCanSeeOtherUser(userID, otherUserId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UserIsFirstAdmin(user *model.User) bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UserIsFirstAdmin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.UserIsFirstAdmin(user)
return resultVar0
}
func (a *OpenTracingAppLayer) UserIsInAdminRoleGroup(userID string, syncableID string, syncableType model.GroupSyncableType) (bool, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UserIsInAdminRoleGroup")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UserIsInAdminRoleGroup(userID, syncableID, syncableType)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ValidateUserPermissionsOnChannels(c request.CTX, userId string, channelIds []string) []string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ValidateUserPermissionsOnChannels")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ValidateUserPermissionsOnChannels(c, userId, channelIds)
return resultVar0
}
func (a *OpenTracingAppLayer) VerifyEmailFromToken(c request.CTX, userSuppliedTokenString string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.VerifyEmailFromToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.VerifyEmailFromToken(c, userSuppliedTokenString)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) VerifyPlugin(plugin io.ReadSeeker, signature io.ReadSeeker) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.VerifyPlugin")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.VerifyPlugin(plugin, signature)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) VerifyUserEmail(userID string, email string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.VerifyUserEmail")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.VerifyUserEmail(userID, email)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) ViewChannel(c request.CTX, view *model.ChannelView, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ViewChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ViewChannel(c, view, userID, currentSessionId, collapsedThreadsSupported)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) WriteFile(fr io.Reader, path string) (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.WriteFile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.WriteFile(fr, path)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) WriteFileContext(ctx context.Context, fr io.Reader, path string) (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.WriteFileContext")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.WriteFileContext(ctx, fr, path)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func NewOpenTracingAppLayer(childApp app.AppIface, ctx context.Context) *OpenTracingAppLayer {
newApp := OpenTracingAppLayer{
app: childApp,
ctx: ctx,
}
newApp.srv = childApp.Srv()
newApp.log = childApp.Log()
newApp.notificationsLog = childApp.NotificationsLog()
newApp.accountMigration = childApp.AccountMigration()
newApp.cluster = childApp.Cluster()
newApp.compliance = childApp.Compliance()
newApp.dataRetention = childApp.DataRetention()
newApp.searchEngine = childApp.SearchEngine()
newApp.ldap = childApp.Ldap()
newApp.messageExport = childApp.MessageExport()
newApp.metrics = childApp.Metrics()
newApp.notification = childApp.Notification()
newApp.saml = childApp.Saml()
newApp.httpService = childApp.HTTPService()
newApp.imageProxy = childApp.ImageProxy()
newApp.timezones = childApp.Timezones()
return &newApp
}
func (a *OpenTracingAppLayer) Srv() *app.Server {
return a.srv
}
func (a *OpenTracingAppLayer) Log() *mlog.Logger {
return a.log
}
func (a *OpenTracingAppLayer) NotificationsLog() *mlog.Logger {
return a.notificationsLog
}
func (a *OpenTracingAppLayer) AccountMigration() einterfaces.AccountMigrationInterface {
return a.accountMigration
}
func (a *OpenTracingAppLayer) Cluster() einterfaces.ClusterInterface {
return a.cluster
}
func (a *OpenTracingAppLayer) Compliance() einterfaces.ComplianceInterface {
return a.compliance
}
func (a *OpenTracingAppLayer) DataRetention() einterfaces.DataRetentionInterface {
return a.dataRetention
}
func (a *OpenTracingAppLayer) Ldap() einterfaces.LdapInterface {
return a.ldap
}
func (a *OpenTracingAppLayer) MessageExport() einterfaces.MessageExportInterface {
return a.messageExport
}
func (a *OpenTracingAppLayer) Metrics() einterfaces.MetricsInterface {
return a.metrics
}
func (a *OpenTracingAppLayer) Notification() einterfaces.NotificationInterface {
return a.notification
}
func (a *OpenTracingAppLayer) Saml() einterfaces.SamlInterface {
return a.saml
}
func (a *OpenTracingAppLayer) HTTPService() httpservice.HTTPService {
return a.httpService
}
func (a *OpenTracingAppLayer) ImageProxy() *imageproxy.ImageProxy {
return a.imageProxy
}
func (a *OpenTracingAppLayer) Timezones() *timezones.Timezones {
return a.timezones
}
func (a *OpenTracingAppLayer) SetServer(srv *app.Server) {
a.srv = srv
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type Option func(s *Server) error
// By default, the app will use the store specified by the configuration. This allows you to
// construct an app with a different store.
//
// The override parameter must be either a store.Store or func(App) store.Store().
func StoreOverride(override any) Option {
return func(s *Server) error {
s.platformOptions = append(s.platformOptions, platform.StoreOverride(override))
return nil
}
}
func StoreOverrideWithCache(override store.Store) Option {
return func(s *Server) error {
s.platformOptions = append(s.platformOptions, platform.StoreOverrideWithCache(override))
return nil
}
}
// Config applies the given config dsn, whether a path to config.json
// or a database connection string. It receives as well a set of
// custom defaults that will be applied for any unset property of the
// config loaded from the dsn on top of the normal defaults
func Config(dsn string, readOnly bool, configDefaults *model.Config) Option {
return func(s *Server) error {
s.platformOptions = append(s.platformOptions, platform.Config(dsn, readOnly, configDefaults))
return nil
}
}
// ConfigStore applies the given config store, typically to replace the traditional sources with a memory store for testing.
func ConfigStore(configStore *config.Store) Option {
return func(s *Server) error {
s.platformOptions = append(s.platformOptions, platform.ConfigStore(configStore))
return nil
}
}
func SetFileStore(filestore filestore.FileBackend) Option {
return func(s *Server) error {
s.platformOptions = append(s.platformOptions, platform.SetFileStore(filestore))
return nil
}
}
func RunEssentialJobs(s *Server) error {
s.runEssentialJobs = true
return nil
}
func JoinCluster(s *Server) error {
s.joinCluster = true
return nil
}
func StartMetrics(s *Server) error {
s.platformOptions = append(s.platformOptions, platform.StartMetrics())
return nil
}
func WithLicense(license *model.License) Option {
return func(s *Server) error {
s.platformOptions = append(s.platformOptions, func(p *platform.PlatformService) error {
p.SetLicense(license)
return nil
})
return nil
}
}
// SetLogger requires platform service to be initialized before calling.
// If not, logger should be set after platform service are initialized.
func SetLogger(logger *mlog.Logger) Option {
return func(s *Server) error {
s.platformOptions = append(s.platformOptions, platform.SetLogger(logger))
return nil
}
}
func SkipPostInitialization() Option {
return func(s *Server) error {
s.skipPostInit = true
return nil
}
}
type AppOption func(a *App)
type AppOptionCreator func() []AppOption
func ServerConnector(ch *Channels) AppOption {
return func(a *App) {
a.ch = ch
}
}
func SetCluster(impl einterfaces.ClusterInterface) Option {
return func(s *Server) error {
s.platformOptions = append(s.platformOptions, platform.SetCluster(impl))
return nil
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
)
const permissionsExportBatchSize = 100
const systemSchemeName = "00000000-0000-0000-0000-000000000000" // Prevents collisions with user-created schemes.
// Ensure permissions service wrapper implements `product.PermissionService`
var _ product.PermissionService = (*permissionsServiceWrapper)(nil)
// permissionsServiceWrapper provides an implementation of `product.PermissionService` for use by products.
type permissionsServiceWrapper struct {
app AppIface
}
func (s *permissionsServiceWrapper) HasPermissionTo(userID string, permission *model.Permission) bool {
return s.app.HasPermissionTo(userID, permission)
}
func (s *permissionsServiceWrapper) HasPermissionToTeam(userID string, teamID string, permission *model.Permission) bool {
return s.app.HasPermissionToTeam(userID, teamID, permission)
}
func (s *permissionsServiceWrapper) HasPermissionToChannel(askingUserID string, channelID string, permission *model.Permission) bool {
return s.app.HasPermissionToChannel(request.EmptyContext(s.app.Log()), askingUserID, channelID, permission)
}
func (s *permissionsServiceWrapper) RolesGrantPermission(roleNames []string, permissionId string) bool {
return s.app.RolesGrantPermission(roleNames, permissionId)
}
func (a *App) ResetPermissionsSystem() *model.AppError {
// Reset all Teams to not have a scheme.
if err := a.Srv().Store().Team().ResetAllTeamSchemes(); err != nil {
return model.NewAppError("ResetPermissionsSystem", "app.team.reset_all_team_schemes.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Reset all Channels to not have a scheme.
if err := a.Srv().Store().Channel().ResetAllChannelSchemes(); err != nil {
return model.NewAppError("ResetPermissionsSystem", "app.channel.reset_all_channel_schemes.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Reset all Custom Role assignments to Users.
if err := a.Srv().Store().User().ClearAllCustomRoleAssignments(); err != nil {
return model.NewAppError("ResetPermissionsSystem", "app.user.clear_all_custom_role_assignments.select.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Reset all Custom Role assignments to TeamMembers.
if err := a.Srv().Store().Team().ClearAllCustomRoleAssignments(); err != nil {
return model.NewAppError("ResetPermissionsSystem", "app.team.clear_all_custom_role_assignments.select.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Reset all Custom Role assignments to ChannelMembers.
if err := a.Srv().Store().Channel().ClearAllCustomRoleAssignments(); err != nil {
return model.NewAppError("ResetPermissionsSystem", "app.channel.clear_all_custom_role_assignments.select.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Purge all schemes from the database.
if err := a.Srv().Store().Scheme().PermanentDeleteAll(); err != nil {
return model.NewAppError("ResetPermissionsSystem", "app.scheme.permanent_delete_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Purge all roles from the database.
if err := a.Srv().Store().Role().PermanentDeleteAll(); err != nil {
return model.NewAppError("ResetPermissionsSystem", "app.role.permanent_delete_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Remove the "System" table entry that marks the advanced permissions migration as done.
if _, err := a.Srv().Store().System().PermanentDeleteByName(model.AdvancedPermissionsMigrationKey); err != nil {
return model.NewAppError("ResetPermissionSystem", "app.system.permanent_delete_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Remove the "System" table entry that marks the emoji permissions migration as done.
if _, err := a.Srv().Store().System().PermanentDeleteByName(EmojisPermissionsMigrationKey); err != nil {
return model.NewAppError("ResetPermissionSystem", "app.system.permanent_delete_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Remove the "System" table entry that marks the guest roles permissions migration as done.
if _, err := a.Srv().Store().System().PermanentDeleteByName(GuestRolesCreationMigrationKey); err != nil {
return model.NewAppError("ResetPermissionSystem", "app.system.permanent_delete_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Now that the permissions system has been reset, re-run the migration to reinitialise it.
a.DoAppMigrations()
return nil
}
func (a *App) ExportPermissions(w io.Writer) error {
next := a.SchemesIterator("", permissionsExportBatchSize)
var schemeBatch []*model.Scheme
for schemeBatch = next(); len(schemeBatch) > 0; schemeBatch = next() {
for _, scheme := range schemeBatch {
roleNames := []string{
scheme.DefaultTeamAdminRole,
scheme.DefaultTeamUserRole,
scheme.DefaultTeamGuestRole,
scheme.DefaultChannelAdminRole,
scheme.DefaultChannelUserRole,
scheme.DefaultChannelGuestRole,
}
roles := []*model.Role{}
for _, roleName := range roleNames {
if roleName == "" {
continue
}
role, err := a.GetRoleByName(context.Background(), roleName)
if err != nil {
return err
}
roles = append(roles, role)
}
schemeExport, err := json.Marshal(&model.SchemeConveyor{
Name: scheme.Name,
DisplayName: scheme.DisplayName,
Description: scheme.Description,
Scope: scheme.Scope,
TeamAdmin: scheme.DefaultTeamAdminRole,
TeamUser: scheme.DefaultTeamUserRole,
TeamGuest: scheme.DefaultTeamGuestRole,
ChannelAdmin: scheme.DefaultChannelAdminRole,
ChannelUser: scheme.DefaultChannelUserRole,
ChannelGuest: scheme.DefaultChannelGuestRole,
Roles: roles,
})
if err != nil {
return err
}
schemeExport = append(schemeExport, []byte("\n")...)
_, err = w.Write(schemeExport)
if err != nil {
return err
}
}
}
defaultRoleNames := []string{}
for _, dr := range model.MakeDefaultRoles() {
defaultRoleNames = append(defaultRoleNames, dr.Name)
}
roles, appErr := a.GetRolesByNames(defaultRoleNames)
if appErr != nil {
return errors.New(appErr.Message)
}
schemeExport, err := json.Marshal(&model.SchemeConveyor{
Name: systemSchemeName,
Roles: roles,
})
if err != nil {
return err
}
schemeExport = append(schemeExport, []byte("\n")...)
_, err = w.Write(schemeExport)
return err
}
func (a *App) ImportPermissions(jsonl io.Reader) error {
createdSchemeIDs := []string{}
scanner := bufio.NewScanner(jsonl)
for scanner.Scan() {
var schemeConveyor *model.SchemeConveyor
err := json.Unmarshal(scanner.Bytes(), &schemeConveyor)
if err != nil {
rollback(a, createdSchemeIDs)
return err
}
if schemeConveyor.Name == systemSchemeName {
for _, roleIn := range schemeConveyor.Roles {
dbRole, err := a.GetRoleByName(context.Background(), roleIn.Name)
if err != nil {
rollback(a, createdSchemeIDs)
return errors.New(err.Message)
}
_, err = a.PatchRole(dbRole, &model.RolePatch{
Permissions: &roleIn.Permissions,
})
if err != nil {
rollback(a, createdSchemeIDs)
return err
}
}
continue
}
// Create the new Scheme. The new Roles are created automatically.
var appErr *model.AppError
schemeCreated, appErr := a.CreateScheme(schemeConveyor.Scheme())
if appErr != nil {
rollback(a, createdSchemeIDs)
return errors.New(appErr.Message)
}
createdSchemeIDs = append(createdSchemeIDs, schemeCreated.Id)
schemeIn := schemeConveyor.Scheme()
roleNameTuples := [][]string{
{schemeCreated.DefaultTeamAdminRole, schemeIn.DefaultTeamAdminRole},
{schemeCreated.DefaultTeamUserRole, schemeIn.DefaultTeamUserRole},
{schemeCreated.DefaultTeamGuestRole, schemeIn.DefaultTeamGuestRole},
{schemeCreated.DefaultChannelAdminRole, schemeIn.DefaultChannelAdminRole},
{schemeCreated.DefaultChannelUserRole, schemeIn.DefaultChannelUserRole},
{schemeCreated.DefaultChannelGuestRole, schemeIn.DefaultChannelGuestRole},
}
for _, roleNameTuple := range roleNameTuples {
if roleNameTuple[0] == "" || roleNameTuple[1] == "" {
continue
}
err = updateRole(a, schemeConveyor, roleNameTuple[0], roleNameTuple[1])
if err != nil {
// Delete the new Schemes. The new Roles are deleted automatically.
rollback(a, createdSchemeIDs)
return err
}
}
}
if err := scanner.Err(); err != nil {
rollback(a, createdSchemeIDs)
return err
}
return nil
}
func rollback(a *App, createdSchemeIDs []string) {
for _, schemeID := range createdSchemeIDs {
a.DeleteScheme(schemeID)
}
}
func updateRole(a *App, sc *model.SchemeConveyor, roleCreatedName, defaultRoleName string) error {
var err *model.AppError
roleCreated, err := a.GetRoleByName(context.Background(), roleCreatedName)
if err != nil {
return errors.New(err.Message)
}
var roleIn *model.Role
for _, role := range sc.Roles {
if role.Name == defaultRoleName {
roleIn = role
break
}
}
roleCreated.DisplayName = roleIn.DisplayName
roleCreated.Description = roleIn.Description
roleCreated.Permissions = roleIn.Permissions
_, err = a.UpdateRole(roleCreated)
if err != nil {
return errors.New(fmt.Sprintf("%v: %v\n", err.Message, err.DetailedError))
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
)
type permissionTransformation struct {
On func(*model.Role, map[string]map[string]bool) bool
Add []string
Remove []string
}
type permissionsMap []permissionTransformation
const (
PermissionManageSystem = "manage_system"
PermissionManageTeam = "manage_team"
PermissionManageEmojis = "manage_emojis"
PermissionManageOthersEmojis = "manage_others_emojis"
PermissionCreateEmojis = "create_emojis"
PermissionDeleteEmojis = "delete_emojis"
PermissionDeleteOthersEmojis = "delete_others_emojis"
PermissionManageWebhooks = "manage_webhooks"
PermissionManageOthersWebhooks = "manage_others_webhooks"
PermissionManageIncomingWebhooks = "manage_incoming_webhooks"
PermissionManageOthersIncomingWebhooks = "manage_others_incoming_webhooks"
PermissionManageOutgoingWebhooks = "manage_outgoing_webhooks"
PermissionManageOthersOutgoingWebhooks = "manage_others_outgoing_webhooks"
PermissionListPublicTeams = "list_public_teams"
PermissionListPrivateTeams = "list_private_teams"
PermissionJoinPublicTeams = "join_public_teams"
PermissionJoinPrivateTeams = "join_private_teams"
PermissionPermanentDeleteUser = "permanent_delete_user"
PermissionCreateBot = "create_bot"
PermissionReadBots = "read_bots"
PermissionReadOthersBots = "read_others_bots"
PermissionManageBots = "manage_bots"
PermissionManageOthersBots = "manage_others_bots"
PermissionDeletePublicChannel = "delete_public_channel"
PermissionDeletePrivateChannel = "delete_private_channel"
PermissionManagePublicChannelProperties = "manage_public_channel_properties"
PermissionManagePrivateChannelProperties = "manage_private_channel_properties"
PermissionConvertPublicChannelToPrivate = "convert_public_channel_to_private"
PermissionConvertPrivateChannelToPublic = "convert_private_channel_to_public"
PermissionViewMembers = "view_members"
PermissionInviteUser = "invite_user"
PermissionInviteGuest = "invite_guest"
PermissionPromoteGuest = "promote_guest"
PermissionDemoteToGuest = "demote_to_guest"
PermissionUseChannelMentions = "use_channel_mentions"
PermissionCreatePost = "create_post"
PermissionCreatePost_PUBLIC = "create_post_public"
PermissionUseGroupMentions = "use_group_mentions"
PermissionAddReaction = "add_reaction"
PermissionRemoveReaction = "remove_reaction"
PermissionManagePublicChannelMembers = "manage_public_channel_members"
PermissionManagePrivateChannelMembers = "manage_private_channel_members"
PermissionReadJobs = "read_jobs"
PermissionManageJobs = "manage_jobs"
PermissionReadOtherUsersTeams = "read_other_users_teams"
PermissionEditOtherUsers = "edit_other_users"
PermissionReadPublicChannelGroups = "read_public_channel_groups"
PermissionReadPrivateChannelGroups = "read_private_channel_groups"
PermissionEditBrand = "edit_brand"
PermissionManageSharedChannels = "manage_shared_channels"
PermissionManageSecureConnections = "manage_secure_connections"
PermissionManageRemoteClusters = "manage_remote_clusters" // deprecated; use `manage_secure_connections`
)
// Deprecated: This function should only be used if a case arises where team and/or channel scheme roles do not need to be migrated.
// Otherwise, use isRole.
func isExactRole(roleName string) func(*model.Role, map[string]map[string]bool) bool {
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
return role.Name == roleName
}
}
// isRole returns true if roleName matches a role's name field or if the a team
// or channel scheme role matches a "common name". A common name is one of the following role
// that is common among the system scheme and the team and/or channel schemes:
//
// TeamAdmin,
// TeamUser,
// TeamGuest,
// ChannelAdmin,
// ChannelUser,
// ChannelGuest,
// PlaybookAdmin,
// PlaybookMember,
// RunAdmin,
// RunMember
func isRole(roleName string) func(*model.Role, map[string]map[string]bool) bool {
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
if role.Name == roleName {
return true
}
return isSchemeRoleAssociatedToCommonName(roleName, role)
}
}
// Deprecated: use isNotRole instead.
func isNotExactRole(roleName string) func(*model.Role, map[string]map[string]bool) bool {
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
return role.Name != roleName
}
}
func isNotRole(roleName string) func(*model.Role, map[string]map[string]bool) bool {
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
return role.Name != roleName && !isSchemeRoleAssociatedToCommonName(roleName, role)
}
}
func isSchemeRoleAssociatedToCommonName(roleName string, role *model.Role) bool {
roleIDToSchemeRoleDisplayName := map[string]string{
model.TeamAdminRoleId: sqlstore.SchemeRoleDisplayNameTeamAdmin,
model.TeamUserRoleId: sqlstore.SchemeRoleDisplayNameTeamUser,
model.TeamGuestRoleId: sqlstore.SchemeRoleDisplayNameTeamGuest,
model.ChannelAdminRoleId: sqlstore.SchemeRoleDisplayNameChannelAdmin,
model.ChannelUserRoleId: sqlstore.SchemeRoleDisplayNameChannelUser,
model.ChannelGuestRoleId: sqlstore.SchemeRoleDisplayNameChannelGuest,
model.PlaybookAdminRoleId: sqlstore.SchemeRoleDisplayNamePlaybookAdmin,
model.PlaybookMemberRoleId: sqlstore.SchemeRoleDisplayNamePlaybookMember,
model.RunAdminRoleId: sqlstore.SchemeRoleDisplayNameRunAdmin,
model.RunMemberRoleId: sqlstore.SchemeRoleDisplayNameRunMember,
}
displayName, ok := roleIDToSchemeRoleDisplayName[roleName]
if !ok {
return false
}
return strings.HasPrefix(role.DisplayName, displayName)
}
func isNotSchemeRole(roleName string) func(*model.Role, map[string]map[string]bool) bool {
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
return !strings.Contains(role.DisplayName, roleName)
}
}
func permissionExists(permission string) func(*model.Role, map[string]map[string]bool) bool {
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
val, ok := permissionsMap[role.Name][permission]
return ok && val
}
}
func permissionNotExists(permission string) func(*model.Role, map[string]map[string]bool) bool {
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
val, ok := permissionsMap[role.Name][permission]
return !(ok && val)
}
}
func onOtherRole(otherRole string, function func(*model.Role, map[string]map[string]bool) bool) func(*model.Role, map[string]map[string]bool) bool {
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
return function(&model.Role{Name: otherRole}, permissionsMap)
}
}
func permissionOr(funcs ...func(*model.Role, map[string]map[string]bool) bool) func(*model.Role, map[string]map[string]bool) bool {
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
for _, f := range funcs {
if f(role, permissionsMap) {
return true
}
}
return false
}
}
func permissionAnd(funcs ...func(*model.Role, map[string]map[string]bool) bool) func(*model.Role, map[string]map[string]bool) bool {
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
for _, f := range funcs {
if !f(role, permissionsMap) {
return false
}
}
return true
}
}
func applyPermissionsMap(role *model.Role, roleMap map[string]map[string]bool, migrationMap permissionsMap) []string {
var result []string
roleName := role.Name
for _, transformation := range migrationMap {
if transformation.On(role, roleMap) {
for _, permission := range transformation.Add {
roleMap[roleName][permission] = true
}
for _, permission := range transformation.Remove {
roleMap[roleName][permission] = false
}
}
}
for key, active := range roleMap[roleName] {
if active {
result = append(result, key)
}
}
return result
}
func (s *Server) doPermissionsMigration(key string, migrationMap permissionsMap, roles []*model.Role) *model.AppError {
if _, err := s.Store().System().GetByName(key); err == nil {
return nil
}
roleMap := make(map[string]map[string]bool)
for _, role := range roles {
roleMap[role.Name] = make(map[string]bool)
for _, permission := range role.Permissions {
roleMap[role.Name][permission] = true
}
}
for _, role := range roles {
role.Permissions = applyPermissionsMap(role, roleMap, migrationMap)
if _, err := s.Store().Role().Save(role); err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return model.NewAppError("doPermissionsMigration", "app.role.save.invalid_role.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return model.NewAppError("doPermissionsMigration", "app.role.save.insert.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
if err := s.Store().System().SaveOrUpdate(&model.System{Name: key, Value: "true"}); err != nil {
return model.NewAppError("doPermissionsMigration", "app.system.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) getEmojisPermissionsSplitMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: permissionExists(PermissionManageEmojis),
Add: []string{PermissionCreateEmojis, PermissionDeleteEmojis},
Remove: []string{PermissionManageEmojis},
},
permissionTransformation{
On: permissionExists(PermissionManageOthersEmojis),
Add: []string{PermissionDeleteOthersEmojis},
Remove: []string{PermissionManageOthersEmojis},
},
}, nil
}
func (a *App) getWebhooksPermissionsSplitMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: permissionExists(PermissionManageWebhooks),
Add: []string{PermissionManageIncomingWebhooks, PermissionManageOutgoingWebhooks},
Remove: []string{PermissionManageWebhooks},
},
permissionTransformation{
On: permissionExists(PermissionManageOthersWebhooks),
Add: []string{PermissionManageOthersIncomingWebhooks, PermissionManageOthersOutgoingWebhooks},
Remove: []string{PermissionManageOthersWebhooks},
},
}, nil
}
func (a *App) getListJoinPublicPrivateTeamsPermissionsMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: []string{PermissionListPrivateTeams, PermissionJoinPrivateTeams},
Remove: []string{},
},
permissionTransformation{
On: isExactRole(model.SystemUserRoleId),
Add: []string{PermissionListPublicTeams, PermissionJoinPublicTeams},
Remove: []string{},
},
}, nil
}
func (a *App) removePermanentDeleteUserMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: permissionExists(PermissionPermanentDeleteUser),
Remove: []string{PermissionPermanentDeleteUser},
},
}, nil
}
func (a *App) getAddBotPermissionsMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: []string{PermissionCreateBot, PermissionReadBots, PermissionReadOthersBots, PermissionManageBots, PermissionManageOthersBots},
Remove: []string{},
},
}, nil
}
func (a *App) applyChannelManageDeleteToChannelUser() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: permissionAnd(isExactRole(model.ChannelUserRoleId), onOtherRole(model.TeamUserRoleId, permissionExists(PermissionManagePrivateChannelProperties))),
Add: []string{PermissionManagePrivateChannelProperties},
},
permissionTransformation{
On: permissionAnd(isExactRole(model.ChannelUserRoleId), onOtherRole(model.TeamUserRoleId, permissionExists(PermissionDeletePrivateChannel))),
Add: []string{PermissionDeletePrivateChannel},
},
permissionTransformation{
On: permissionAnd(isExactRole(model.ChannelUserRoleId), onOtherRole(model.TeamUserRoleId, permissionExists(PermissionManagePublicChannelProperties))),
Add: []string{PermissionManagePublicChannelProperties},
},
permissionTransformation{
On: permissionAnd(isExactRole(model.ChannelUserRoleId), onOtherRole(model.TeamUserRoleId, permissionExists(PermissionDeletePublicChannel))),
Add: []string{PermissionDeletePublicChannel},
},
}, nil
}
func (a *App) removeChannelManageDeleteFromTeamUser() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: permissionAnd(isExactRole(model.TeamUserRoleId), permissionExists(PermissionManagePrivateChannelProperties)),
Remove: []string{PermissionManagePrivateChannelProperties},
},
permissionTransformation{
On: permissionAnd(isExactRole(model.TeamUserRoleId), permissionExists(PermissionDeletePrivateChannel)),
Remove: []string{model.PermissionDeletePrivateChannel.Id},
},
permissionTransformation{
On: permissionAnd(isExactRole(model.TeamUserRoleId), permissionExists(PermissionManagePublicChannelProperties)),
Remove: []string{PermissionManagePublicChannelProperties},
},
permissionTransformation{
On: permissionAnd(isExactRole(model.TeamUserRoleId), permissionExists(PermissionDeletePublicChannel)),
Remove: []string{PermissionDeletePublicChannel},
},
}, nil
}
func (a *App) getViewMembersPermissionMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: isExactRole(model.SystemUserRoleId),
Add: []string{PermissionViewMembers},
},
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: []string{PermissionViewMembers},
},
}, nil
}
func (a *App) getAddManageGuestsPermissionsMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: []string{PermissionPromoteGuest, PermissionDemoteToGuest, PermissionInviteGuest},
},
}, nil
}
func (a *App) channelModerationPermissionsMigration() (permissionsMap, error) {
transformations := permissionsMap{}
var allTeamSchemes []*model.Scheme
next := a.SchemesIterator(model.SchemeScopeTeam, 100)
var schemeBatch []*model.Scheme
for schemeBatch = next(); len(schemeBatch) > 0; schemeBatch = next() {
allTeamSchemes = append(allTeamSchemes, schemeBatch...)
}
moderatedPermissionsMinusCreatePost := []string{
PermissionAddReaction,
PermissionRemoveReaction,
PermissionManagePublicChannelMembers,
PermissionManagePrivateChannelMembers,
PermissionUseChannelMentions,
}
teamAndChannelAdminConditionalTransformations := func(teamAdminID, channelAdminID, channelUserID, channelGuestID string) []permissionTransformation {
transformations := []permissionTransformation{}
for _, perm := range moderatedPermissionsMinusCreatePost {
// add each moderated permission to the channel admin if channel user or guest has the permission
trans := permissionTransformation{
On: permissionAnd(
isExactRole(channelAdminID),
permissionOr(
onOtherRole(channelUserID, permissionExists(perm)),
onOtherRole(channelGuestID, permissionExists(perm)),
),
),
Add: []string{perm},
}
transformations = append(transformations, trans)
// add each moderated permission to the team admin if channel admin, user, or guest has the permission
trans = permissionTransformation{
On: permissionAnd(
isExactRole(teamAdminID),
permissionOr(
onOtherRole(channelAdminID, permissionExists(perm)),
onOtherRole(channelUserID, permissionExists(perm)),
onOtherRole(channelGuestID, permissionExists(perm)),
),
),
Add: []string{perm},
}
transformations = append(transformations, trans)
}
return transformations
}
for _, ts := range allTeamSchemes {
// ensure all team scheme channel admins have create_post because it's not exposed via the UI
trans := permissionTransformation{
On: isExactRole(ts.DefaultChannelAdminRole),
Add: []string{PermissionCreatePost},
}
transformations = append(transformations, trans)
// ensure all team scheme team admins have create_post because it's not exposed via the UI
trans = permissionTransformation{
On: isExactRole(ts.DefaultTeamAdminRole),
Add: []string{PermissionCreatePost},
}
transformations = append(transformations, trans)
// conditionally add all other moderated permissions to team and channel admins
transformations = append(transformations, teamAndChannelAdminConditionalTransformations(
ts.DefaultTeamAdminRole,
ts.DefaultChannelAdminRole,
ts.DefaultChannelUserRole,
ts.DefaultChannelGuestRole,
)...)
}
// ensure team admins have create_post
transformations = append(transformations, permissionTransformation{
On: isExactRole(model.TeamAdminRoleId),
Add: []string{PermissionCreatePost},
})
// ensure channel admins have create_post
transformations = append(transformations, permissionTransformation{
On: isExactRole(model.ChannelAdminRoleId),
Add: []string{PermissionCreatePost},
})
// conditionally add all other moderated permissions to team and channel admins
transformations = append(transformations, teamAndChannelAdminConditionalTransformations(
model.TeamAdminRoleId,
model.ChannelAdminRoleId,
model.ChannelUserRoleId,
model.ChannelGuestRoleId,
)...)
// ensure system admin has all of the moderated permissions
transformations = append(transformations, permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: append(moderatedPermissionsMinusCreatePost, PermissionCreatePost),
})
// add the new use_channel_mentions permission to everyone who has create_post
transformations = append(transformations, permissionTransformation{
On: permissionOr(permissionExists(PermissionCreatePost), permissionExists(PermissionCreatePost_PUBLIC)),
Add: []string{PermissionUseChannelMentions},
})
return transformations, nil
}
func (a *App) getAddUseGroupMentionsPermissionMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: permissionAnd(
isNotExactRole(model.ChannelGuestRoleId),
isNotSchemeRole(sqlstore.SchemeRoleDisplayNameChannelGuest),
permissionOr(permissionExists(PermissionCreatePost), permissionExists(PermissionCreatePost_PUBLIC)),
),
Add: []string{PermissionUseGroupMentions},
},
}, nil
}
func (a *App) getAddSystemConsolePermissionsMigration() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsToAdd := []string{}
for _, permission := range append(model.SysconsoleReadPermissions, model.SysconsoleWritePermissions...) {
permissionsToAdd = append(permissionsToAdd, permission.Id)
}
// add the new permissions to system admin
transformations = append(transformations,
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: permissionsToAdd,
})
// add read_jobs to all roles with manage_jobs
transformations = append(transformations, permissionTransformation{
On: permissionExists(PermissionManageJobs),
Add: []string{PermissionReadJobs},
})
// add read_other_users_teams to all roles with edit_other_users
transformations = append(transformations, permissionTransformation{
On: permissionExists(PermissionEditOtherUsers),
Add: []string{PermissionReadOtherUsersTeams},
})
// add read_public_channel_groups to all roles with manage_public_channel_members
transformations = append(transformations, permissionTransformation{
On: permissionExists(PermissionManagePublicChannelMembers),
Add: []string{PermissionReadPublicChannelGroups},
})
// add read_private_channel_groups to all roles with manage_private_channel_members
transformations = append(transformations, permissionTransformation{
On: permissionExists(PermissionManagePrivateChannelMembers),
Add: []string{PermissionReadPrivateChannelGroups},
})
// add edit_brand to all roles with manage_system
transformations = append(transformations, permissionTransformation{
On: permissionExists(PermissionManageSystem),
Add: []string{PermissionEditBrand},
})
return transformations, nil
}
func (a *App) getAddConvertChannelPermissionsMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: permissionExists(PermissionManageTeam),
Add: []string{PermissionConvertPublicChannelToPrivate, PermissionConvertPrivateChannelToPublic},
},
}, nil
}
func (a *App) getSystemRolesPermissionsMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: []string{model.PermissionSysconsoleReadUserManagementSystemRoles.Id, model.PermissionSysconsoleWriteUserManagementSystemRoles.Id},
},
}, nil
}
func (a *App) getAddManageSharedChannelsPermissionsMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: []string{PermissionManageSharedChannels},
},
}, nil
}
func (a *App) getBillingPermissionsMigration() (permissionsMap, error) {
return permissionsMap{
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: []string{model.PermissionSysconsoleReadBilling.Id, model.PermissionSysconsoleWriteBilling.Id},
},
}, nil
}
func (a *App) getAddManageSecureConnectionsPermissionsMigration() (permissionsMap, error) {
transformations := []permissionTransformation{}
// add the new permission to system admin
transformations = append(transformations,
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: []string{PermissionManageSecureConnections},
})
// remote the deprecated permission from system admin
transformations = append(transformations,
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Remove: []string{PermissionManageRemoteClusters},
})
return transformations, nil
}
func (a *App) getAddDownloadComplianceExportResult() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsToAddComplianceRead := []string{model.PermissionDownloadComplianceExportResult.Id, model.PermissionReadDataRetentionJob.Id}
permissionsToAddComplianceWrite := []string{model.PermissionManageJobs.Id}
// add the new permissions to system admin
transformations = append(transformations,
permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: []string{model.PermissionDownloadComplianceExportResult.Id},
})
// add Download Compliance Export Result and Read Jobs to all roles with sysconsole_read_compliance
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadCompliance.Id),
Add: permissionsToAddComplianceRead,
})
// add manage_jobs to all roles with sysconsole_write_compliance
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteCompliance.Id),
Add: permissionsToAddComplianceWrite,
})
return transformations, nil
}
func (a *App) getAddExperimentalSubsectionPermissions() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsExperimentalRead := []string{model.PermissionSysconsoleReadExperimentalBleve.Id, model.PermissionSysconsoleReadExperimentalFeatures.Id, model.PermissionSysconsoleReadExperimentalFeatureFlags.Id}
permissionsExperimentalWrite := []string{model.PermissionSysconsoleWriteExperimentalBleve.Id, model.PermissionSysconsoleWriteExperimentalFeatures.Id, model.PermissionSysconsoleWriteExperimentalFeatureFlags.Id}
// Give the new subsection READ permissions to any user with READ_EXPERIMENTAL
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadExperimental.Id),
Add: permissionsExperimentalRead,
})
// Give the new subsection WRITE permissions to any user with WRITE_EXPERIMENTAL
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteExperimental.Id),
Add: permissionsExperimentalWrite,
})
// Give the ancillary permissions MANAGE_JOBS and PURGE_BLEVE_INDEXES to anyone with WRITE_EXPERIMENTAL_BLEVE
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteExperimentalBleve.Id),
Add: []string{model.PermissionCreatePostBleveIndexesJob.Id, model.PermissionPurgeBleveIndexes.Id},
})
return transformations, nil
}
func (a *App) getAddIntegrationsSubsectionPermissions() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsIntegrationsRead := []string{model.PermissionSysconsoleReadIntegrationsIntegrationManagement.Id, model.PermissionSysconsoleReadIntegrationsBotAccounts.Id, model.PermissionSysconsoleReadIntegrationsGif.Id, model.PermissionSysconsoleReadIntegrationsCors.Id}
permissionsIntegrationsWrite := []string{model.PermissionSysconsoleWriteIntegrationsIntegrationManagement.Id, model.PermissionSysconsoleWriteIntegrationsBotAccounts.Id, model.PermissionSysconsoleWriteIntegrationsGif.Id, model.PermissionSysconsoleWriteIntegrationsCors.Id}
// Give the new subsection READ permissions to any user with READ_INTEGRATIONS
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadIntegrations.Id),
Add: permissionsIntegrationsRead,
})
// Give the new subsection WRITE permissions to any user with WRITE_EXPERIMENTAL
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteIntegrations.Id),
Add: permissionsIntegrationsWrite,
})
return transformations, nil
}
func (a *App) getAddSiteSubsectionPermissions() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsSiteRead := []string{model.PermissionSysconsoleReadSiteCustomization.Id, model.PermissionSysconsoleReadSiteLocalization.Id, model.PermissionSysconsoleReadSiteUsersAndTeams.Id, model.PermissionSysconsoleReadSiteNotifications.Id, model.PermissionSysconsoleReadSiteAnnouncementBanner.Id, model.PermissionSysconsoleReadSiteEmoji.Id, model.PermissionSysconsoleReadSitePosts.Id, model.PermissionSysconsoleReadSiteFileSharingAndDownloads.Id, model.PermissionSysconsoleReadSitePublicLinks.Id, model.PermissionSysconsoleReadSiteNotices.Id}
permissionsSiteWrite := []string{model.PermissionSysconsoleWriteSiteCustomization.Id, model.PermissionSysconsoleWriteSiteLocalization.Id, model.PermissionSysconsoleWriteSiteUsersAndTeams.Id, model.PermissionSysconsoleWriteSiteNotifications.Id, model.PermissionSysconsoleWriteSiteAnnouncementBanner.Id, model.PermissionSysconsoleWriteSiteEmoji.Id, model.PermissionSysconsoleWriteSitePosts.Id, model.PermissionSysconsoleWriteSiteFileSharingAndDownloads.Id, model.PermissionSysconsoleWriteSitePublicLinks.Id, model.PermissionSysconsoleWriteSiteNotices.Id}
// Give the new subsection READ permissions to any user with READ_SITE
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadSite.Id),
Add: permissionsSiteRead,
})
// Give the new subsection WRITE permissions to any user with WRITE_SITE
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteSite.Id),
Add: permissionsSiteWrite,
})
// Give the ancillary permissions EDIT_BRAND to anyone with WRITE_SITE_CUSTOMIZATION
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteSiteCustomization.Id),
Add: []string{model.PermissionEditBrand.Id},
})
return transformations, nil
}
func (a *App) getAddComplianceSubsectionPermissions() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsComplianceRead := []string{model.PermissionSysconsoleReadComplianceDataRetentionPolicy.Id, model.PermissionSysconsoleReadComplianceComplianceExport.Id, model.PermissionSysconsoleReadComplianceComplianceMonitoring.Id, model.PermissionSysconsoleReadComplianceCustomTermsOfService.Id}
permissionsComplianceWrite := []string{model.PermissionSysconsoleWriteComplianceDataRetentionPolicy.Id, model.PermissionSysconsoleWriteComplianceComplianceExport.Id, model.PermissionSysconsoleWriteComplianceComplianceMonitoring.Id, model.PermissionSysconsoleWriteComplianceCustomTermsOfService.Id}
// Give the new subsection READ permissions to any user with READ_COMPLIANCE
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadCompliance.Id),
Add: permissionsComplianceRead,
})
// Give the new subsection WRITE permissions to any user with WRITE_COMPLIANCE
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteCompliance.Id),
Add: permissionsComplianceWrite,
})
// Ancillary permissions
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy.Id),
Add: []string{model.PermissionCreateDataRetentionJob.Id},
})
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadComplianceDataRetentionPolicy.Id),
Add: []string{model.PermissionReadDataRetentionJob.Id},
})
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteComplianceComplianceExport.Id),
Add: []string{model.PermissionCreateComplianceExportJob.Id, model.PermissionDownloadComplianceExportResult.Id},
})
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadComplianceComplianceExport.Id),
Add: []string{model.PermissionReadComplianceExportJob.Id, model.PermissionDownloadComplianceExportResult.Id},
})
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadComplianceCustomTermsOfService.Id),
Add: []string{model.PermissionReadAudits.Id},
})
return transformations, nil
}
func (a *App) getAddEnvironmentSubsectionPermissions() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsEnvironmentRead := []string{
model.PermissionSysconsoleReadEnvironmentWebServer.Id,
model.PermissionSysconsoleReadEnvironmentDatabase.Id,
model.PermissionSysconsoleReadEnvironmentElasticsearch.Id,
model.PermissionSysconsoleReadEnvironmentFileStorage.Id,
model.PermissionSysconsoleReadEnvironmentImageProxy.Id,
model.PermissionSysconsoleReadEnvironmentSMTP.Id,
model.PermissionSysconsoleReadEnvironmentPushNotificationServer.Id,
model.PermissionSysconsoleReadEnvironmentHighAvailability.Id,
model.PermissionSysconsoleReadEnvironmentRateLimiting.Id,
model.PermissionSysconsoleReadEnvironmentLogging.Id,
model.PermissionSysconsoleReadEnvironmentSessionLengths.Id,
model.PermissionSysconsoleReadEnvironmentPerformanceMonitoring.Id,
model.PermissionSysconsoleReadEnvironmentDeveloper.Id,
}
permissionsEnvironmentWrite := []string{
model.PermissionSysconsoleWriteEnvironmentWebServer.Id,
model.PermissionSysconsoleWriteEnvironmentDatabase.Id,
model.PermissionSysconsoleWriteEnvironmentElasticsearch.Id,
model.PermissionSysconsoleWriteEnvironmentFileStorage.Id,
model.PermissionSysconsoleWriteEnvironmentImageProxy.Id,
model.PermissionSysconsoleWriteEnvironmentSMTP.Id,
model.PermissionSysconsoleWriteEnvironmentPushNotificationServer.Id,
model.PermissionSysconsoleWriteEnvironmentHighAvailability.Id,
model.PermissionSysconsoleWriteEnvironmentRateLimiting.Id,
model.PermissionSysconsoleWriteEnvironmentLogging.Id,
model.PermissionSysconsoleWriteEnvironmentSessionLengths.Id,
model.PermissionSysconsoleWriteEnvironmentPerformanceMonitoring.Id,
model.PermissionSysconsoleWriteEnvironmentDeveloper.Id,
}
// Give the new subsection READ permissions to any user with READ_ENVIRONMENT
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadEnvironment.Id),
Add: permissionsEnvironmentRead,
})
// Give the new subsection WRITE permissions to any user with WRITE_ENVIRONMENT
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteEnvironment.Id),
Add: permissionsEnvironmentWrite,
})
// Give these ancillary permissions to anyone with READ_ENVIRONMENT_ELASTICSEARCH
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadEnvironmentElasticsearch.Id),
Add: []string{
model.PermissionReadElasticsearchPostIndexingJob.Id,
model.PermissionReadElasticsearchPostAggregationJob.Id,
},
})
// Give these ancillary permissions to anyone with WRITE_ENVIRONMENT_WEB_SERVER
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteEnvironmentWebServer.Id),
Add: []string{
model.PermissionTestSiteURL.Id,
model.PermissionReloadConfig.Id,
model.PermissionInvalidateCaches.Id,
},
})
// Give these ancillary permissions to anyone with WRITE_ENVIRONMENT_DATABASE
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteEnvironmentDatabase.Id),
Add: []string{model.PermissionRecycleDatabaseConnections.Id},
})
// Give these ancillary permissions to anyone with WRITE_ENVIRONMENT_ELASTICSEARCH
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteEnvironmentElasticsearch.Id),
Add: []string{
model.PermissionTestElasticsearch.Id,
model.PermissionCreateElasticsearchPostIndexingJob.Id,
model.PermissionCreateElasticsearchPostAggregationJob.Id,
model.PermissionPurgeElasticsearchIndexes.Id,
},
})
// Give these ancillary permissions to anyone with WRITE_ENVIRONMENT_FILE_STORAGE
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteEnvironmentFileStorage.Id),
Add: []string{model.PermissionTestS3.Id},
})
return transformations, nil
}
func (a *App) getAddAboutSubsectionPermissions() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsAboutRead := []string{model.PermissionSysconsoleReadAboutEditionAndLicense.Id}
permissionsAboutWrite := []string{model.PermissionSysconsoleWriteAboutEditionAndLicense.Id}
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadAbout.Id),
Add: permissionsAboutRead,
})
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteAbout.Id),
Add: permissionsAboutWrite,
})
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadAboutEditionAndLicense.Id),
Add: []string{model.PermissionReadLicenseInformation.Id},
})
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteAboutEditionAndLicense.Id),
Add: []string{model.PermissionManageLicenseInformation.Id},
})
return transformations, nil
}
func (a *App) getAddReportingSubsectionPermissions() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsReportingRead := []string{
model.PermissionSysconsoleReadReportingSiteStatistics.Id,
model.PermissionSysconsoleReadReportingTeamStatistics.Id,
model.PermissionSysconsoleReadReportingServerLogs.Id,
}
permissionsReportingWrite := []string{
model.PermissionSysconsoleWriteReportingSiteStatistics.Id,
model.PermissionSysconsoleWriteReportingTeamStatistics.Id,
model.PermissionSysconsoleWriteReportingServerLogs.Id,
}
// Give the new subsection READ permissions to any user with READ_REPORTING
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadReporting.Id),
Add: permissionsReportingRead,
})
// Give the new subsection WRITE permissions to any user with WRITE_REPORTING
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteReporting.Id),
Add: permissionsReportingWrite,
})
// Give the ancillary permissions PERMISSION_GET_ANALYTICS to anyone with PERMISSION_SYSCONSOLE_READ_USERMANAGEMENT_USERS or PERMISSION_SYSCONSOLE_READ_REPORTING_SITE_STATISTICS
transformations = append(transformations, permissionTransformation{
On: permissionOr(permissionExists(model.PermissionSysconsoleReadUserManagementUsers.Id), permissionExists(model.PermissionSysconsoleReadReportingSiteStatistics.Id)),
Add: []string{model.PermissionGetAnalytics.Id},
})
// Give the ancillary permissions PERMISSION_GET_LOGS to anyone with PERMISSION_SYSCONSOLE_READ_REPORTING_SERVER_LOGS
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadReportingServerLogs.Id),
Add: []string{model.PermissionGetLogs.Id},
})
return transformations, nil
}
func (a *App) getAddAuthenticationSubsectionPermissions() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsAuthenticationRead := []string{model.PermissionSysconsoleReadAuthenticationSignup.Id, model.PermissionSysconsoleReadAuthenticationEmail.Id, model.PermissionSysconsoleReadAuthenticationPassword.Id, model.PermissionSysconsoleReadAuthenticationMfa.Id, model.PermissionSysconsoleReadAuthenticationLdap.Id, model.PermissionSysconsoleReadAuthenticationSaml.Id, model.PermissionSysconsoleReadAuthenticationOpenid.Id, model.PermissionSysconsoleReadAuthenticationGuestAccess.Id}
permissionsAuthenticationWrite := []string{model.PermissionSysconsoleWriteAuthenticationSignup.Id, model.PermissionSysconsoleWriteAuthenticationEmail.Id, model.PermissionSysconsoleWriteAuthenticationPassword.Id, model.PermissionSysconsoleWriteAuthenticationMfa.Id, model.PermissionSysconsoleWriteAuthenticationLdap.Id, model.PermissionSysconsoleWriteAuthenticationSaml.Id, model.PermissionSysconsoleWriteAuthenticationOpenid.Id, model.PermissionSysconsoleWriteAuthenticationGuestAccess.Id}
// Give the new subsection READ permissions to any user with READ_AUTHENTICATION
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadAuthentication.Id),
Add: permissionsAuthenticationRead,
})
// Give the new subsection WRITE permissions to any user with WRITE_AUTHENTICATION
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteAuthentication.Id),
Add: permissionsAuthenticationWrite,
})
// Give the ancillary permissions for LDAP to anyone with WRITE_AUTHENTICATION_LDAP
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteAuthenticationLdap.Id),
Add: []string{model.PermissionCreateLdapSyncJob.Id, model.PermissionTestLdap.Id, model.PermissionAddLdapPublicCert.Id, model.PermissionAddLdapPrivateCert.Id, model.PermissionRemoveLdapPublicCert.Id, model.PermissionRemoveLdapPrivateCert.Id},
})
// Give the ancillary permissions PERMISSION_TEST_LDAP to anyone with READ_AUTHENTICATION_LDAP
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleReadAuthenticationLdap.Id),
Add: []string{model.PermissionReadLdapSyncJob.Id},
})
// Give the ancillary permissions PERMISSION_INVALIDATE_EMAIL_INVITE to anyone with WRITE_AUTHENTICATION_EMAIL
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteAuthenticationEmail.Id),
Add: []string{model.PermissionInvalidateEmailInvite.Id},
})
// Give the ancillary permissions for SAML to anyone with WRITE_AUTHENTICATION_SAML
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteAuthenticationSaml.Id),
Add: []string{model.PermissionGetSamlMetadataFromIdp.Id, model.PermissionAddSamlPublicCert.Id, model.PermissionAddSamlPrivateCert.Id, model.PermissionAddSamlIdpCert.Id, model.PermissionRemoveSamlPublicCert.Id, model.PermissionRemoveSamlPrivateCert.Id, model.PermissionRemoveSamlIdpCert.Id, model.PermissionGetSamlCertStatus.Id},
})
return transformations, nil
}
// This migration fixes https://github.com/mattermost/mattermost-server/issues/17642 where this particular ancillary permission was forgotten during the initial migrations
func (a *App) getAddTestEmailAncillaryPermission() (permissionsMap, error) {
transformations := []permissionTransformation{}
// Give these ancillary permissions to anyone with WRITE_ENVIRONMENT_SMTP
transformations = append(transformations, permissionTransformation{
On: permissionExists(model.PermissionSysconsoleWriteEnvironmentSMTP.Id),
Add: []string{model.PermissionTestEmail.Id},
})
return transformations, nil
}
func (a *App) getAddCustomUserGroupsPermissions() (permissionsMap, error) {
t := []permissionTransformation{}
customGroupPermissions := []string{
model.PermissionCreateCustomGroup.Id,
model.PermissionManageCustomGroupMembers.Id,
model.PermissionEditCustomGroup.Id,
model.PermissionDeleteCustomGroup.Id,
}
t = append(t, permissionTransformation{
On: isExactRole(model.SystemUserRoleId),
Add: customGroupPermissions,
})
t = append(t, permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: customGroupPermissions,
})
return t, nil
}
func (a *App) getAddCustomUserGroupsPermissionRestore() (permissionsMap, error) {
t := []permissionTransformation{}
customGroupPermissions := []string{
model.PermissionRestoreCustomGroup.Id,
}
t = append(t, permissionTransformation{
On: isExactRole(model.SystemUserRoleId),
Add: customGroupPermissions,
})
t = append(t, permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: customGroupPermissions,
})
t = append(t, permissionTransformation{
On: isExactRole(model.SystemCustomGroupAdminRoleId),
Add: customGroupPermissions,
})
return t, nil
}
func (a *App) getAddPlaybooksPermissions() (permissionsMap, error) {
transformations := []permissionTransformation{}
transformations = append(transformations, permissionTransformation{
On: permissionOr(
permissionExists(model.PermissionCreatePublicChannel.Id),
permissionExists(model.PermissionCreatePrivateChannel.Id),
),
Add: []string{
model.PermissionPublicPlaybookCreate.Id,
model.PermissionPrivatePlaybookCreate.Id,
},
})
transformations = append(transformations, permissionTransformation{
On: isExactRole(model.SystemAdminRoleId),
Add: []string{
model.PermissionPublicPlaybookManageProperties.Id,
model.PermissionPublicPlaybookManageMembers.Id,
model.PermissionPublicPlaybookView.Id,
model.PermissionPublicPlaybookMakePrivate.Id,
model.PermissionPrivatePlaybookManageProperties.Id,
model.PermissionPrivatePlaybookManageMembers.Id,
model.PermissionPrivatePlaybookView.Id,
model.PermissionPrivatePlaybookMakePublic.Id,
model.PermissionRunCreate.Id,
model.PermissionRunManageProperties.Id,
model.PermissionRunManageMembers.Id,
model.PermissionRunView.Id,
},
})
return transformations, nil
}
func (a *App) getPlaybooksPermissionsAddManageRoles() (permissionsMap, error) {
transformations := []permissionTransformation{}
transformations = append(transformations, permissionTransformation{
On: permissionOr(
isExactRole(model.PlaybookAdminRoleId),
isExactRole(model.TeamAdminRoleId),
isExactRole(model.SystemAdminRoleId),
),
Add: []string{
model.PermissionPublicPlaybookManageRoles.Id,
model.PermissionPrivatePlaybookManageRoles.Id,
},
})
return transformations, nil
}
func (a *App) getProductsBoardsPermissions() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsProductsRead := []string{model.PermissionSysconsoleReadProductsBoards.Id}
permissionsProductsWrite := []string{model.PermissionSysconsoleWriteProductsBoards.Id}
// Give the new subsection READ permissions to any user with SYSTEM_MANAGER
transformations = append(transformations, permissionTransformation{
On: permissionOr(isExactRole(model.SystemManagerRoleId)),
Add: permissionsProductsRead,
})
// Give the new subsection WRITE permissions to any user with SYSTEM_ADMIN
transformations = append(transformations, permissionTransformation{
On: permissionOr(isExactRole(model.SystemAdminRoleId)),
Add: permissionsProductsWrite,
})
return transformations, nil
}
// DoPermissionsMigrations execute all the permissions migrations need by the current version.
func (a *App) DoPermissionsMigrations() error {
return a.Srv().doPermissionsMigrations()
}
func (s *Server) doPermissionsMigrations() error {
a := New(ServerConnector(s.Channels()))
PermissionsMigrations := []struct {
Key string
Migration func() (permissionsMap, error)
}{
{Key: model.MigrationKeyEmojiPermissionsSplit, Migration: a.getEmojisPermissionsSplitMigration},
{Key: model.MigrationKeyWebhookPermissionsSplit, Migration: a.getWebhooksPermissionsSplitMigration},
{Key: model.MigrationKeyListJoinPublicPrivateTeams, Migration: a.getListJoinPublicPrivateTeamsPermissionsMigration},
{Key: model.MigrationKeyRemovePermanentDeleteUser, Migration: a.removePermanentDeleteUserMigration},
{Key: model.MigrationKeyAddBotPermissions, Migration: a.getAddBotPermissionsMigration},
{Key: model.MigrationKeyApplyChannelManageDeleteToChannelUser, Migration: a.applyChannelManageDeleteToChannelUser},
{Key: model.MigrationKeyRemoveChannelManageDeleteFromTeamUser, Migration: a.removeChannelManageDeleteFromTeamUser},
{Key: model.MigrationKeyViewMembersNewPermission, Migration: a.getViewMembersPermissionMigration},
{Key: model.MigrationKeyAddManageGuestsPermissions, Migration: a.getAddManageGuestsPermissionsMigration},
{Key: model.MigrationKeyChannelModerationsPermissions, Migration: a.channelModerationPermissionsMigration},
{Key: model.MigrationKeyAddUseGroupMentionsPermission, Migration: a.getAddUseGroupMentionsPermissionMigration},
{Key: model.MigrationKeyAddSystemConsolePermissions, Migration: a.getAddSystemConsolePermissionsMigration},
{Key: model.MigrationKeyAddConvertChannelPermissions, Migration: a.getAddConvertChannelPermissionsMigration},
{Key: model.MigrationKeyAddManageSharedChannelPermissions, Migration: a.getAddManageSharedChannelsPermissionsMigration},
{Key: model.MigrationKeyAddManageSecureConnectionsPermissions, Migration: a.getAddManageSecureConnectionsPermissionsMigration},
{Key: model.MigrationKeyAddSystemRolesPermissions, Migration: a.getSystemRolesPermissionsMigration},
{Key: model.MigrationKeyAddBillingPermissions, Migration: a.getBillingPermissionsMigration},
{Key: model.MigrationKeyAddDownloadComplianceExportResults, Migration: a.getAddDownloadComplianceExportResult},
{Key: model.MigrationKeyAddExperimentalSubsectionPermissions, Migration: a.getAddExperimentalSubsectionPermissions},
{Key: model.MigrationKeyAddAuthenticationSubsectionPermissions, Migration: a.getAddAuthenticationSubsectionPermissions},
{Key: model.MigrationKeyAddIntegrationsSubsectionPermissions, Migration: a.getAddIntegrationsSubsectionPermissions},
{Key: model.MigrationKeyAddSiteSubsectionPermissions, Migration: a.getAddSiteSubsectionPermissions},
{Key: model.MigrationKeyAddComplianceSubsectionPermissions, Migration: a.getAddComplianceSubsectionPermissions},
{Key: model.MigrationKeyAddEnvironmentSubsectionPermissions, Migration: a.getAddEnvironmentSubsectionPermissions},
{Key: model.MigrationKeyAddAboutSubsectionPermissions, Migration: a.getAddAboutSubsectionPermissions},
{Key: model.MigrationKeyAddReportingSubsectionPermissions, Migration: a.getAddReportingSubsectionPermissions},
{Key: model.MigrationKeyAddTestEmailAncillaryPermission, Migration: a.getAddTestEmailAncillaryPermission},
{Key: model.MigrationKeyAddPlaybooksPermissions, Migration: a.getAddPlaybooksPermissions},
{Key: model.MigrationKeyAddCustomUserGroupsPermissions, Migration: a.getAddCustomUserGroupsPermissions},
{Key: model.MigrationKeyAddPlayboosksManageRolesPermissions, Migration: a.getPlaybooksPermissionsAddManageRoles},
{Key: model.MigrationKeyAddProductsBoardsPermissions, Migration: a.getProductsBoardsPermissions},
{Key: model.MigrationKeyAddCustomUserGroupsPermissionRestore, Migration: a.getAddCustomUserGroupsPermissionRestore},
}
roles, err := s.Store().Role().GetAll()
if err != nil {
return err
}
for _, migration := range PermissionsMigrations {
migMap, err := migration.Migration()
if err != nil {
return err
}
if err := s.doPermissionsMigration(migration.Key, migMap, roles); err != nil {
return err
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"encoding/json"
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
)
const (
TimestampFormat = "Mon Jan 2 15:04:05 -0700 MST 2006"
)
// Busy represents the busy state of the server. A server marked busy
// will have non-critical services disabled. If a Cluster is provided
// any changes will be propagated to each node.
type Busy struct {
busy int32 // protected via atomic for fast IsBusy calls
mux sync.RWMutex
timer *time.Timer
expires time.Time
cluster einterfaces.ClusterInterface
}
// NewBusy creates a new Busy instance with optional cluster which will
// be notified of busy state changes.
func NewBusy(cluster einterfaces.ClusterInterface) *Busy {
return &Busy{cluster: cluster}
}
// IsBusy returns true if the server has been marked as busy.
func (b *Busy) IsBusy() bool {
if b == nil {
return false
}
return atomic.LoadInt32(&b.busy) != 0
}
// Set marks the server as busy for dur duration and notifies cluster nodes.
func (b *Busy) Set(dur time.Duration) {
b.mux.Lock()
defer b.mux.Unlock()
// minimum 1 second
if dur < (time.Second * 1) {
dur = time.Second * 1
}
b.setWithoutNotify(dur)
if b.cluster != nil {
sbs := &model.ServerBusyState{Busy: true, Expires: b.expires.Unix(), ExpiresTS: b.expires.UTC().Format(TimestampFormat)}
b.notifyServerBusyChange(sbs)
}
}
// must hold mutex
func (b *Busy) setWithoutNotify(dur time.Duration) {
b.clearWithoutNotify()
atomic.StoreInt32(&b.busy, 1)
b.expires = time.Now().Add(dur)
b.timer = time.AfterFunc(dur, func() {
b.mux.Lock()
b.clearWithoutNotify()
b.mux.Unlock()
})
}
// ClearBusy marks the server as not busy and notifies cluster nodes.
func (b *Busy) Clear() {
b.mux.Lock()
defer b.mux.Unlock()
b.clearWithoutNotify()
if b.cluster != nil {
sbs := &model.ServerBusyState{Busy: false, Expires: time.Time{}.Unix(), ExpiresTS: ""}
b.notifyServerBusyChange(sbs)
}
}
// must hold mutex
func (b *Busy) clearWithoutNotify() {
if b.timer != nil {
b.timer.Stop() // don't drain timer.C channel for AfterFunc timers.
}
b.timer = nil
b.expires = time.Time{}
atomic.StoreInt32(&b.busy, 0)
}
// Expires returns the expected time that the server
// will be marked not busy. This expiry can be extended
// via additional calls to SetBusy.
func (b *Busy) Expires() time.Time {
b.mux.RLock()
defer b.mux.RUnlock()
return b.expires
}
// notifyServerBusyChange informs all cluster members of a server busy state change.
func (b *Busy) notifyServerBusyChange(sbs *model.ServerBusyState) {
if b.cluster == nil {
return
}
buf, _ := json.Marshal(sbs)
msg := &model.ClusterMessage{
Event: model.ClusterEventBusyStateChanged,
SendType: model.ClusterSendReliable,
WaitForAllToSend: true,
Data: buf,
}
b.cluster.SendClusterMessage(msg)
}
// ClusterEventChanged is called when a CLUSTER_EVENT_BUSY_STATE_CHANGED is received.
func (b *Busy) ClusterEventChanged(sbs *model.ServerBusyState) {
b.mux.Lock()
defer b.mux.Unlock()
if sbs.Busy {
expires := time.Unix(sbs.Expires, 0)
dur := time.Until(expires)
if dur > 0 {
b.setWithoutNotify(dur)
}
} else {
b.clearWithoutNotify()
}
}
func (b *Busy) ToJSON() ([]byte, error) {
b.mux.RLock()
defer b.mux.RUnlock()
sbs := &model.ServerBusyState{
Busy: atomic.LoadInt32(&b.busy) != 0,
Expires: b.expires.Unix(),
ExpiresTS: b.expires.UTC().Format(TimestampFormat),
}
sbsJSON, jsonErr := json.Marshal(sbs)
if jsonErr != nil {
return []byte{}, fmt.Errorf("failed to encode server busy state to JSON: %w", jsonErr)
}
return sbsJSON, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"errors"
"fmt"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// ensure cluster service wrapper implements `product.ClusterService`
var _ product.ClusterService = (*PlatformService)(nil)
// Ensure KV store wrapper implements `product.KVStoreService`
var _ product.KVStoreService = (*PlatformService)(nil)
func (ps *PlatformService) Cluster() einterfaces.ClusterInterface {
return ps.clusterIFace
}
func (ps *PlatformService) NewClusterDiscoveryService() *ClusterDiscoveryService {
ds := &ClusterDiscoveryService{
ClusterDiscovery: model.ClusterDiscovery{},
platform: ps,
stop: make(chan bool),
}
return ds
}
func (ps *PlatformService) IsLeader() bool {
if ps.License() != nil && *ps.Config().ClusterSettings.Enable && ps.clusterIFace != nil {
return ps.clusterIFace.IsLeader()
}
return true
}
func (ps *PlatformService) SetCluster(impl einterfaces.ClusterInterface) { //nolint:unused
ps.clusterIFace = impl
}
func (ps *PlatformService) PublishPluginClusterEvent(productID string, ev model.PluginClusterEvent, opts model.PluginClusterEventSendOptions) error {
if ps.clusterIFace == nil {
return nil
}
msg := &model.ClusterMessage{
Event: model.ClusterEventPluginEvent,
SendType: opts.SendType,
WaitForAllToSend: false,
Props: map[string]string{
"ProductID": productID,
"EventID": ev.Id,
},
Data: ev.Data,
}
// If TargetId is empty we broadcast to all other cluster nodes.
if opts.TargetId == "" {
ps.clusterIFace.SendClusterMessage(msg)
} else {
if err := ps.clusterIFace.SendClusterMessageToNode(opts.TargetId, msg); err != nil {
return fmt.Errorf("failed to send message to cluster node %q: %w", opts.TargetId, err)
}
}
return nil
}
func (ps *PlatformService) PublishWebSocketEvent(productID string, event string, payload map[string]any, broadcast *model.WebsocketBroadcast) {
ev := model.NewWebSocketEvent(fmt.Sprintf("custom_%v_%v", productID, event), "", "", "", nil, "")
ev = ev.SetBroadcast(broadcast).SetData(payload)
ps.Publish(ev)
}
func (ps *PlatformService) SetPluginKeyWithOptions(productID string, key string, value []byte, options model.PluginKVSetOptions) (bool, *model.AppError) {
if err := options.IsValid(); err != nil {
mlog.Debug("Failed to set plugin key value with options", mlog.String("plugin_id", productID), mlog.String("key", key), mlog.Err(err))
return false, err
}
updated, err := ps.Store.Plugin().SetWithOptions(productID, key, value, options)
if err != nil {
mlog.Error("Failed to set plugin key value with options", mlog.String("plugin_id", productID), mlog.String("key", key), mlog.Err(err))
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return false, appErr
default:
return false, model.NewAppError("SetPluginKeyWithOptions", "app.plugin_store.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// Clean up a previous entry using the hashed key, if it exists.
if err := ps.Store.Plugin().Delete(productID, getKeyHash(key)); err != nil {
mlog.Warn("Failed to clean up previously hashed plugin key value", mlog.String("plugin_id", productID), mlog.String("key", key), mlog.Err(err))
}
return updated, nil
}
func (ps *PlatformService) KVGet(productID, key string) ([]byte, *model.AppError) {
if kv, err := ps.Store.Plugin().Get(productID, key); err == nil {
return kv.Value, nil
} else if nfErr := new(store.ErrNotFound); !errors.As(err, &nfErr) {
mlog.Error("Failed to query plugin key value", mlog.String("plugin_id", productID), mlog.String("key", key), mlog.Err(err))
return nil, model.NewAppError("GetPluginKey", "app.plugin_store.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Lookup using the hashed version of the key for keys written prior to v5.6.
if kv, err := ps.Store.Plugin().Get(productID, getKeyHash(key)); err == nil {
return kv.Value, nil
} else if nfErr := new(store.ErrNotFound); !errors.As(err, &nfErr) {
mlog.Error("Failed to query plugin key value using hashed key", mlog.String("plugin_id", productID), mlog.String("key", key), mlog.Err(err))
return nil, model.NewAppError("GetPluginKey", "app.plugin_store.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil, nil
}
func (ps *PlatformService) KVDelete(productID, key string) *model.AppError {
if err := ps.Store.Plugin().Delete(productID, getKeyHash(key)); err != nil {
ps.logger.Error("Failed to delete plugin key value", mlog.String("plugin_id", productID), mlog.String("key", key), mlog.Err(err))
return model.NewAppError("DeletePluginKey", "app.plugin_store.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Also delete the key without hashing
if err := ps.Store.Plugin().Delete(productID, key); err != nil {
ps.logger.Error("Failed to delete plugin key value using hashed key", mlog.String("plugin_id", productID), mlog.String("key", key), mlog.Err(err))
return model.NewAppError("DeletePluginKey", "app.plugin_store.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (ps *PlatformService) KVList(productID string, page, perPage int) ([]string, *model.AppError) {
data, err := ps.Store.Plugin().List(productID, page*perPage, perPage)
if err != nil {
ps.logger.Error("Failed to list plugin key values", mlog.Int("page", page), mlog.Int("perPage", perPage), mlog.Err(err))
return nil, model.NewAppError("ListPluginKeys", "app.plugin_store.list.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return data, nil
}
// Registers a given function to be called when the cluster leader may have changed. Returns a unique ID for the
// listener which can later be used to remove it. If clustering is not enabled in this build, the callback will never
// be called.
func (ps *PlatformService) AddClusterLeaderChangedListener(listener func()) string {
id := model.NewId()
ps.clusterLeaderListeners.Store(id, listener)
return id
}
// Removes a listener function by the unique ID returned when AddConfigListener was called
func (ps *PlatformService) RemoveClusterLeaderChangedListener(id string) {
ps.clusterLeaderListeners.Delete(id)
}
func (ps *PlatformService) InvokeClusterLeaderChangedListeners() {
ps.logger.Info("Cluster leader changed. Invoking ClusterLeaderChanged listeners.")
// This needs to be run in a separate goroutine otherwise a recursive lock happens
// because the listener function eventually ends up calling .IsLeader().
// Fixing this would require the changed event to pass the leader directly, but that
// requires a lot of work.
ps.Go(func() {
ps.clusterLeaderListeners.Range(func(_, listener any) bool {
listener.(func())()
return true
})
})
}
func (ps *PlatformService) Publish(message *model.WebSocketEvent) {
if ps.metricsIFace != nil {
ps.metricsIFace.IncrementWebsocketEvent(message.EventType())
}
ps.PublishSkipClusterSend(message)
if ps.clusterIFace != nil {
data, err := message.ToJSON()
if err != nil {
mlog.Warn("Failed to encode message to JSON", mlog.Err(err))
}
cm := &model.ClusterMessage{
Event: model.ClusterEventPublish,
SendType: model.ClusterSendBestEffort,
Data: data,
}
if message.EventType() == model.WebsocketEventPosted ||
message.EventType() == model.WebsocketEventPostEdited ||
message.EventType() == model.WebsocketEventDirectAdded ||
message.EventType() == model.WebsocketEventGroupAdded ||
message.EventType() == model.WebsocketEventAddedToTeam ||
message.GetBroadcast().ReliableClusterSend {
cm.SendType = model.ClusterSendReliable
}
ps.clusterIFace.SendClusterMessage(cm)
}
}
func (ps *PlatformService) PublishSkipClusterSend(event *model.WebSocketEvent) {
if event.GetBroadcast().UserId != "" {
hub := ps.GetHubForUserId(event.GetBroadcast().UserId)
if hub != nil {
hub.Broadcast(event)
}
} else {
for _, hub := range ps.hubs {
hub.Broadcast(event)
}
}
// Notify shared channel sync service
ps.SharedChannelSyncHandler(event)
}
func (ps *PlatformService) ListPluginKeys(pluginID string, page, perPage int) ([]string, *model.AppError) {
data, err := ps.Store.Plugin().List(pluginID, page*perPage, perPage)
if err != nil {
mlog.Error("Failed to list plugin key values", mlog.Int("page", page), mlog.Int("perPage", perPage), mlog.Err(err))
return nil, model.NewAppError("ListPluginKeys", "app.plugin_store.list.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return data, nil
}
func (ps *PlatformService) DeletePluginKey(pluginID string, key string) *model.AppError {
if err := ps.Store.Plugin().Delete(pluginID, getKeyHash(key)); err != nil {
mlog.Error("Failed to delete plugin key value", mlog.String("plugin_id", pluginID), mlog.String("key", key), mlog.Err(err))
return model.NewAppError("DeletePluginKey", "app.plugin_store.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Also delete the key without hashing
if err := ps.Store.Plugin().Delete(pluginID, key); err != nil {
mlog.Error("Failed to delete plugin key value using hashed key", mlog.String("plugin_id", pluginID), mlog.String("key", key), mlog.Err(err))
return model.NewAppError("DeletePluginKey", "app.plugin_store.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
DiscoveryServiceWritePing = 60 * time.Second
)
type ClusterDiscoveryService struct {
model.ClusterDiscovery
platform *PlatformService
stop chan bool
}
func (cds *ClusterDiscoveryService) Start() {
err := cds.platform.Store.ClusterDiscovery().Cleanup()
if err != nil {
mlog.Warn("ClusterDiscoveryService failed to cleanup the outdated cluster discovery information", mlog.Err(err))
}
exists, err := cds.platform.Store.ClusterDiscovery().Exists(&cds.ClusterDiscovery)
if err != nil {
mlog.Warn("ClusterDiscoveryService failed to check if row exists", mlog.String("ClusterDiscoveryID", cds.ClusterDiscovery.Id), mlog.Err(err))
} else if exists {
if _, err := cds.platform.Store.ClusterDiscovery().Delete(&cds.ClusterDiscovery); err != nil {
mlog.Warn("ClusterDiscoveryService failed to start clean", mlog.String("ClusterDiscoveryID", cds.ClusterDiscovery.Id), mlog.Err(err))
}
}
if err := cds.platform.Store.ClusterDiscovery().Save(&cds.ClusterDiscovery); err != nil {
mlog.Error("ClusterDiscoveryService failed to save", mlog.String("ClusterDiscoveryID", cds.ClusterDiscovery.Id), mlog.Err(err))
return
}
go func() {
mlog.Debug("ClusterDiscoveryService ping writer started", mlog.String("ClusterDiscoveryID", cds.ClusterDiscovery.Id))
ticker := time.NewTicker(DiscoveryServiceWritePing)
defer func() {
ticker.Stop()
if _, err := cds.platform.Store.ClusterDiscovery().Delete(&cds.ClusterDiscovery); err != nil {
mlog.Warn("ClusterDiscoveryService failed to cleanup", mlog.String("ClusterDiscoveryID", cds.ClusterDiscovery.Id), mlog.Err(err))
}
mlog.Debug("ClusterDiscoveryService ping writer stopped", mlog.String("ClusterDiscoveryID", cds.ClusterDiscovery.Id))
}()
for {
select {
case <-ticker.C:
if err := cds.platform.Store.ClusterDiscovery().SetLastPingAt(&cds.ClusterDiscovery); err != nil {
mlog.Error("ClusterDiscoveryService failed to write ping", mlog.String("ClusterDiscoveryID", cds.ClusterDiscovery.Id), mlog.Err(err))
}
case <-cds.stop:
return
}
}
}()
}
func (cds *ClusterDiscoveryService) Stop() {
cds.stop <- true
}
func (ps *PlatformService) GetClusterId() string {
if ps.Cluster() == nil {
return ""
}
return ps.Cluster().GetClusterId()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"bytes"
"encoding/json"
"fmt"
"runtime/debug"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (ps *PlatformService) RegisterClusterHandlers() {
ps.clusterIFace.RegisterClusterMessageHandler(model.ClusterEventPublish, ps.ClusterPublishHandler)
ps.clusterIFace.RegisterClusterMessageHandler(model.ClusterEventUpdateStatus, ps.ClusterUpdateStatusHandler)
ps.clusterIFace.RegisterClusterMessageHandler(model.ClusterEventInvalidateAllCaches, ps.ClusterInvalidateAllCachesHandler)
ps.clusterIFace.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannelMembersNotifyProps, ps.clusterInvalidateCacheForChannelMembersNotifyPropHandler)
ps.clusterIFace.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannelByName, ps.clusterInvalidateCacheForChannelByNameHandler)
ps.clusterIFace.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForUser, ps.clusterInvalidateCacheForUserHandler)
ps.clusterIFace.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForUserTeams, ps.clusterInvalidateCacheForUserTeamsHandler)
ps.clusterIFace.RegisterClusterMessageHandler(model.ClusterEventBusyStateChanged, ps.clusterBusyStateChgHandler)
ps.clusterIFace.RegisterClusterMessageHandler(model.ClusterEventClearSessionCacheForUser, ps.clusterClearSessionCacheForUserHandler)
ps.clusterIFace.RegisterClusterMessageHandler(model.ClusterEventClearSessionCacheForAllUsers, ps.clusterClearSessionCacheForAllUsersHandler)
for e, h := range ps.additionalClusterHandlers {
ps.clusterIFace.RegisterClusterMessageHandler(e, h)
}
}
func (ps *PlatformService) RegisterClusterMessageHandler(ev model.ClusterEvent, h einterfaces.ClusterMessageHandler) {
ps.additionalClusterHandlers[ev] = h
}
// ClusterHandlersPreCheck checks whether the platform service is ready to handle cluster messages.
func (ps *PlatformService) ClusterHandlersPreCheck() error {
if ps.Store == nil {
return fmt.Errorf("could not find store")
}
if ps.statusCache == nil {
return fmt.Errorf("could not find status cache")
}
return nil
}
func (ps *PlatformService) ClusterPublishHandler(msg *model.ClusterMessage) {
event, err := model.WebSocketEventFromJSON(bytes.NewReader(msg.Data))
if err != nil {
ps.logger.Warn("Failed to decode event from JSON", mlog.Err(err))
return
}
ps.PublishSkipClusterSend(event)
}
func (ps *PlatformService) ClusterUpdateStatusHandler(msg *model.ClusterMessage) {
var status model.Status
if jsonErr := json.Unmarshal(msg.Data, &status); jsonErr != nil {
ps.logger.Warn("Failed to decode status from JSON")
}
ps.statusCache.Set(status.UserId, status)
}
func (ps *PlatformService) ClusterInvalidateAllCachesHandler(msg *model.ClusterMessage) {
ps.InvalidateAllCachesSkipSend()
}
func (ps *PlatformService) clusterInvalidateCacheForChannelMembersNotifyPropHandler(msg *model.ClusterMessage) {
ps.invalidateCacheForChannelMembersNotifyPropsSkipClusterSend(string(msg.Data))
}
func (ps *PlatformService) clusterInvalidateCacheForChannelByNameHandler(msg *model.ClusterMessage) {
ps.invalidateCacheForChannelByNameSkipClusterSend(msg.Props["id"], msg.Props["name"])
}
func (ps *PlatformService) clusterInvalidateCacheForUserHandler(msg *model.ClusterMessage) {
ps.InvalidateCacheForUserSkipClusterSend(string(msg.Data))
}
func (ps *PlatformService) clusterInvalidateCacheForUserTeamsHandler(msg *model.ClusterMessage) {
ps.invalidateWebConnSessionCacheForUser(string(msg.Data))
}
func (ps *PlatformService) ClearSessionCacheForUserSkipClusterSend(userID string) {
ps.ClearUserSessionCacheLocal(userID)
ps.invalidateWebConnSessionCacheForUser(userID)
}
func (ps *PlatformService) ClearSessionCacheForAllUsersSkipClusterSend() {
ps.logger.Info("Purging sessions cache")
ps.ClearAllUsersSessionCacheLocal()
}
func (ps *PlatformService) clusterClearSessionCacheForUserHandler(msg *model.ClusterMessage) {
ps.ClearSessionCacheForUserSkipClusterSend(string(msg.Data))
}
func (ps *PlatformService) clusterClearSessionCacheForAllUsersHandler(msg *model.ClusterMessage) {
ps.ClearSessionCacheForAllUsersSkipClusterSend()
}
func (ps *PlatformService) clusterBusyStateChgHandler(msg *model.ClusterMessage) {
var sbs model.ServerBusyState
if jsonErr := json.Unmarshal(msg.Data, &sbs); jsonErr != nil {
mlog.Warn("Failed to decode server busy state from JSON", mlog.Err(jsonErr))
}
ps.Busy.ClusterEventChanged(&sbs)
if sbs.Busy {
ps.logger.Warn("server busy state activated via cluster event - non-critical services disabled", mlog.Int64("expires_sec", sbs.Expires))
} else {
ps.logger.Info("server busy state cleared via cluster event - non-critical services enabled")
}
}
func (ps *PlatformService) invalidateCacheForChannelMembersNotifyPropsSkipClusterSend(channelID string) {
ps.Store.Channel().InvalidateCacheForChannelMembersNotifyProps(channelID)
}
func (ps *PlatformService) invalidateCacheForChannelByNameSkipClusterSend(teamID, name string) {
if teamID == "" {
teamID = "dm"
}
ps.Store.Channel().InvalidateChannelByName(teamID, name)
}
func (ps *PlatformService) InvalidateCacheForUserSkipClusterSend(userID string) {
ps.Store.Channel().InvalidateAllChannelMembersForUser(userID)
ps.invalidateWebConnSessionCacheForUser(userID)
}
func (ps *PlatformService) invalidateWebConnSessionCacheForUser(userID string) {
hub := ps.GetHubForUserId(userID)
if hub != nil {
hub.InvalidateUser(userID)
}
}
func (ps *PlatformService) InvalidateAllCachesSkipSend() {
ps.logger.Info("Purging all caches")
ps.ClearAllUsersSessionCacheLocal()
ps.statusCache.Purge()
ps.Store.Team().ClearCaches()
ps.Store.Channel().ClearCaches()
ps.Store.User().ClearCaches()
ps.Store.Post().ClearCaches()
ps.Store.FileInfo().ClearCaches()
ps.Store.Webhook().ClearCaches()
linkCache.Purge()
ps.LoadLicense()
}
func (ps *PlatformService) InvalidateAllCaches() *model.AppError {
debug.FreeOSMemory()
ps.InvalidateAllCachesSkipSend()
if ps.clusterIFace != nil {
msg := &model.ClusterMessage{
Event: model.ClusterEventInvalidateAllCaches,
SendType: model.ClusterSendReliable,
WaitForAllToSend: true,
}
ps.clusterIFace.SendClusterMessage(msg)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/md5"
"crypto/rand"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"reflect"
"strconv"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// ServiceConfig is used to initialize the PlatformService.
// The mandatory fields will be checked during the initialization of the service.
type ServiceConfig struct {
// Mandatory fields
ConfigStore *config.Store
Store store.Store
// Optional fields
Cluster einterfaces.ClusterInterface
}
// ensure the config wrapper implements `product.ConfigService`
var _ product.ConfigService = (*PlatformService)(nil)
func (ps *PlatformService) Config() *model.Config {
return ps.configStore.Get()
}
// Registers a function with a given listener to be called when the config is reloaded and may have changed. The function
// will be called with two arguments: the old config and the new config. AddConfigListener returns a unique ID
// for the listener that can later be used to remove it.
func (ps *PlatformService) AddConfigListener(listener func(*model.Config, *model.Config)) string {
return ps.configStore.AddListener(listener)
}
func (ps *PlatformService) RemoveConfigListener(id string) {
ps.configStore.RemoveListener(id)
}
func (ps *PlatformService) UpdateConfig(f func(*model.Config)) {
if ps.configStore.IsReadOnly() {
return
}
old := ps.Config()
updated := old.Clone()
f(updated)
if _, _, err := ps.configStore.Set(updated); err != nil {
ps.logger.Error("Failed to update config", mlog.Err(err))
}
}
// SaveConfig replaces the active configuration, optionally notifying cluster peers.
// It returns both the previous and current configs.
func (ps *PlatformService) SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bool) (*model.Config, *model.Config, *model.AppError) {
oldCfg, newCfg, err := ps.configStore.Set(newCfg)
if errors.Is(err, config.ErrReadOnlyConfiguration) {
return nil, nil, model.NewAppError("saveConfig", "ent.cluster.save_config.error", nil, "", http.StatusForbidden).Wrap(err)
} else if err != nil {
return nil, nil, model.NewAppError("saveConfig", "app.save_config.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if ps.clusterIFace != nil {
err := ps.clusterIFace.ConfigChanged(ps.configStore.RemoveEnvironmentOverrides(oldCfg),
ps.configStore.RemoveEnvironmentOverrides(newCfg), sendConfigChangeClusterMessage)
if err != nil {
return nil, nil, err
}
}
return oldCfg, newCfg, nil
}
func (ps *PlatformService) ReloadConfig() error {
if err := ps.configStore.Load(); err != nil {
return err
}
return nil
}
func (ps *PlatformService) GetEnvironmentOverridesWithFilter(filter func(reflect.StructField) bool) map[string]interface{} {
return ps.configStore.GetEnvironmentOverridesWithFilter(filter)
}
func (ps *PlatformService) GetEnvironmentOverrides() map[string]interface{} {
return ps.configStore.GetEnvironmentOverrides()
}
func (ps *PlatformService) DescribeConfig() string {
return ps.configStore.String()
}
func (ps *PlatformService) CleanUpConfig() error {
return ps.configStore.CleanUp()
}
// ConfigureLogger applies the specified configuration to a logger.
func (ps *PlatformService) ConfigureLogger(name string, logger *mlog.Logger, logSettings *model.LogSettings, getPath func(string) string) error {
// Advanced logging is E20 only, however logging must be initialized before the license
// file is loaded. If no valid E20 license exists then advanced logging will be
// shutdown once license is loaded/checked.
var err error
dsn := *logSettings.AdvancedLoggingConfig
var logConfigSrc config.LogConfigSrc
if dsn != "" {
logConfigSrc, err = config.NewLogConfigSrc(dsn, ps.configStore)
if err != nil {
return fmt.Errorf("invalid config source for %s, %w", name, err)
}
ps.logger.Info("Loaded configuration for "+name, mlog.String("source", dsn))
}
cfg, err := config.MloggerConfigFromLoggerConfig(logSettings, logConfigSrc, getPath)
if err != nil {
return fmt.Errorf("invalid config source for %s, %w", name, err)
}
if err := logger.ConfigureTargets(cfg, nil); err != nil {
return fmt.Errorf("invalid config for %s, %w", name, err)
}
return nil
}
func (ps *PlatformService) GetConfigStore() *config.Store {
return ps.configStore
}
func (ps *PlatformService) GetConfigFile(name string) ([]byte, error) {
return ps.configStore.GetFile(name)
}
func (ps *PlatformService) SetConfigFile(name string, data []byte) error {
return ps.configStore.SetFile(name, data)
}
func (ps *PlatformService) RemoveConfigFile(name string) error {
return ps.configStore.RemoveFile(name)
}
func (ps *PlatformService) HasConfigFile(name string) (bool, error) {
return ps.configStore.HasFile(name)
}
func (ps *PlatformService) SetConfigReadOnlyFF(readOnly bool) {
ps.configStore.SetReadOnlyFF(readOnly)
}
func (ps *PlatformService) ClientConfigHash() string {
return ps.clientConfigHash.Load().(string)
}
func (ps *PlatformService) regenerateClientConfig() {
clientConfig := config.GenerateClientConfig(ps.Config(), ps.telemetryId, ps.License())
limitedClientConfig := config.GenerateLimitedClientConfig(ps.Config(), ps.telemetryId, ps.License())
if clientConfig["EnableCustomTermsOfService"] == "true" {
termsOfService, err := ps.Store.TermsOfService().GetLatest(true)
if err != nil {
mlog.Err(err)
} else {
clientConfig["CustomTermsOfServiceId"] = termsOfService.Id
limitedClientConfig["CustomTermsOfServiceId"] = termsOfService.Id
}
}
if key := ps.AsymmetricSigningKey(); key != nil {
der, _ := x509.MarshalPKIXPublicKey(&key.PublicKey)
clientConfig["AsymmetricSigningPublicKey"] = base64.StdEncoding.EncodeToString(der)
limitedClientConfig["AsymmetricSigningPublicKey"] = base64.StdEncoding.EncodeToString(der)
}
clientConfigJSON, _ := json.Marshal(clientConfig)
ps.clientConfig.Store(clientConfig)
ps.limitedClientConfig.Store(limitedClientConfig)
ps.clientConfigHash.Store(fmt.Sprintf("%x", md5.Sum(clientConfigJSON)))
}
// AsymmetricSigningKey will return a private key that can be used for asymmetric signing.
func (ps *PlatformService) AsymmetricSigningKey() *ecdsa.PrivateKey {
if key := ps.asymmetricSigningKey.Load(); key != nil {
return key.(*ecdsa.PrivateKey)
}
return nil
}
// EnsureAsymmetricSigningKey ensures that an asymmetric signing key exists and future calls to
// AsymmetricSigningKey will always return a valid signing key.
func (ps *PlatformService) EnsureAsymmetricSigningKey() error {
if ps.AsymmetricSigningKey() != nil {
return nil
}
var key *model.SystemAsymmetricSigningKey
value, err := ps.Store.System().GetByName(model.SystemAsymmetricSigningKeyKey)
if err == nil {
if err := json.Unmarshal([]byte(value.Value), &key); err != nil {
return err
}
}
// If we don't already have a key, try to generate one.
if key == nil {
newECDSAKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return err
}
newKey := &model.SystemAsymmetricSigningKey{
ECDSAKey: &model.SystemECDSAKey{
Curve: "P-256",
X: newECDSAKey.X,
Y: newECDSAKey.Y,
D: newECDSAKey.D,
},
}
system := &model.System{
Name: model.SystemAsymmetricSigningKeyKey,
}
v, err := json.Marshal(newKey)
if err != nil {
return err
}
system.Value = string(v)
// If we were able to save the key, use it, otherwise log the error.
if err = ps.Store.System().Save(system); err != nil {
mlog.Warn("Failed to save AsymmetricSigningKey", mlog.Err(err))
} else {
key = newKey
}
}
// If we weren't able to save a new key above, another server must have beat us to it. Get the
// key from the database, and if that fails, error out.
if key == nil {
value, err := ps.Store.System().GetByName(model.SystemAsymmetricSigningKeyKey)
if err != nil {
return err
}
if err := json.Unmarshal([]byte(value.Value), &key); err != nil {
return err
}
}
var curve elliptic.Curve
switch key.ECDSAKey.Curve {
case "P-256":
curve = elliptic.P256()
default:
return fmt.Errorf("unknown curve: " + key.ECDSAKey.Curve)
}
ps.asymmetricSigningKey.Store(&ecdsa.PrivateKey{
PublicKey: ecdsa.PublicKey{
Curve: curve,
X: key.ECDSAKey.X,
Y: key.ECDSAKey.Y,
},
D: key.ECDSAKey.D,
})
ps.regenerateClientConfig()
return nil
}
// LimitedClientConfigWithComputed gets the configuration in a format suitable for sending to the client.
func (ps *PlatformService) LimitedClientConfigWithComputed() map[string]string {
respCfg := map[string]string{}
for k, v := range ps.LimitedClientConfig() {
respCfg[k] = v
}
// These properties are not configurable, but nevertheless represent configuration expected
// by the client.
respCfg["NoAccounts"] = strconv.FormatBool(ps.IsFirstUserAccount())
return respCfg
}
// ClientConfigWithComputed gets the configuration in a format suitable for sending to the client.
func (ps *PlatformService) ClientConfigWithComputed() map[string]string {
respCfg := map[string]string{}
for k, v := range ps.clientConfig.Load().(map[string]string) {
respCfg[k] = v
}
// These properties are not configurable, but nevertheless represent configuration expected
// by the client.
respCfg["NoAccounts"] = strconv.FormatBool(ps.IsFirstUserAccount())
respCfg["MaxPostSize"] = strconv.Itoa(ps.MaxPostSize())
respCfg["UpgradedFromTE"] = strconv.FormatBool(ps.isUpgradedFromTE())
respCfg["InstallationDate"] = ""
if installationDate, err := ps.GetSystemInstallDate(); err == nil {
respCfg["InstallationDate"] = strconv.FormatInt(installationDate, 10)
}
if ver, err := ps.Store.GetDBSchemaVersion(); err != nil {
mlog.Error("Could not get the schema version", mlog.Err(err))
} else {
respCfg["SchemaVersion"] = strconv.Itoa(ver)
}
return respCfg
}
func (ps *PlatformService) LimitedClientConfig() map[string]string {
return ps.limitedClientConfig.Load().(map[string]string)
}
func (ps *PlatformService) IsFirstUserAccount() bool {
count, err := ps.Store.User().Count(model.UserCountOptions{IncludeDeleted: true})
if err != nil {
return false
}
return count <= 0
}
func (ps *PlatformService) MaxPostSize() int {
maxPostSize := ps.Store.Post().GetMaxPostSize()
if maxPostSize == 0 {
return model.PostMessageMaxRunesV1
}
return maxPostSize
}
func (ps *PlatformService) isUpgradedFromTE() bool {
val, err := ps.Store.System().GetByName(model.SystemUpgradedFromTeId)
if err != nil {
return false
}
return val.Value == "true"
}
func (ps *PlatformService) GetSystemInstallDate() (int64, *model.AppError) {
systemData, err := ps.Store.System().GetByName(model.SystemInstallationDateKey)
if err != nil {
return 0, model.NewAppError("getSystemInstallDate", "app.system.get_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
value, err := strconv.ParseInt(systemData.Value, 10, 64)
if err != nil {
return 0, model.NewAppError("getSystemInstallDate", "app.system_install_date.parse_int.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return value, nil
}
func (ps *PlatformService) ClientConfig() map[string]string {
return ps.clientConfig.Load().(map[string]string)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
)
var clusterInterface func(*PlatformService) einterfaces.ClusterInterface
func RegisterClusterInterface(f func(*PlatformService) einterfaces.ClusterInterface) {
clusterInterface = f
}
var elasticsearchInterface func(*PlatformService) searchengine.SearchEngineInterface
func RegisterElasticsearchInterface(f func(*PlatformService) searchengine.SearchEngineInterface) {
elasticsearchInterface = f
}
var licenseInterface func(*PlatformService) einterfaces.LicenseInterface
func RegisterLicenseInterface(f func(*PlatformService) einterfaces.LicenseInterface) {
licenseInterface = f
}
var metricsInterfaceFn func(*PlatformService, string, string) einterfaces.MetricsInterface
func RegisterMetricsInterface(f func(*PlatformService, string, string) einterfaces.MetricsInterface) {
metricsInterfaceFn = f
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"encoding/json"
"os"
"time"
"github.com/mattermost/mattermost-server/v6/server/channels/app/featureflag"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// SetupFeatureFlags called on startup and when the cluster leader changes.
// Starts or stops the synchronization of feature flags from upstream management.
func (ps *PlatformService) SetupFeatureFlags() {
ps.featureFlagSynchronizerMutex.Lock()
defer ps.featureFlagSynchronizerMutex.Unlock()
splitKey := *ps.Config().ServiceSettings.SplitKey
splitConfigured := splitKey != ""
syncFeatureFlags := splitConfigured && ps.IsLeader()
ps.configStore.SetReadOnlyFF(!splitConfigured)
if syncFeatureFlags {
if err := ps.startFeatureFlagUpdateJob(); err != nil {
ps.logger.Warn("Unable to setup synchronization with feature flag management. Will fallback to cache.", mlog.Err(err))
}
} else {
ps.StopFeatureFlagUpdateJob()
}
if err := ps.configStore.Load(); err != nil {
ps.logger.Warn("Unable to load config store after feature flag setup.", mlog.Err(err))
}
}
func (ps *PlatformService) updateFeatureFlagValuesFromManagement() {
newCfg := ps.configStore.GetNoEnv().Clone()
oldFlags := *newCfg.FeatureFlags
newFlags := ps.featureFlagSynchronizer.UpdateFeatureFlagValues(oldFlags)
oldFlagsBytes, _ := json.Marshal(oldFlags)
newFlagsBytes, _ := json.Marshal(newFlags)
ps.logger.Debug("Checking feature flags from management service", mlog.String("old_flags", string(oldFlagsBytes)), mlog.String("new_flags", string(newFlagsBytes)))
if oldFlags != newFlags {
ps.logger.Debug("Feature flag change detected, updating config")
*newCfg.FeatureFlags = newFlags
ps.SaveConfig(newCfg, true)
}
}
func (ps *PlatformService) startFeatureFlagUpdateJob() error {
// Can be run multiple times
if ps.featureFlagSynchronizer != nil {
return nil
}
var log *mlog.Logger
if *ps.Config().ServiceSettings.DebugSplit {
log = ps.logger
}
attributes := map[string]any{}
// if we are part of a cloud installation, add its installation and group id
if installationId := os.Getenv("MM_CLOUD_INSTALLATION_ID"); installationId != "" {
attributes["installation_id"] = installationId
}
if groupId := os.Getenv("MM_CLOUD_GROUP_ID"); groupId != "" {
attributes["group_id"] = groupId
}
synchronizer, err := featureflag.NewSynchronizer(featureflag.SyncParams{
ServerID: ps.telemetryId,
SplitKey: *ps.Config().ServiceSettings.SplitKey,
Log: log,
Attributes: attributes,
})
if err != nil {
return err
}
ps.featureFlagStop = make(chan struct{})
ps.featureFlagStopped = make(chan struct{})
ps.featureFlagSynchronizer = synchronizer
syncInterval := *ps.Config().ServiceSettings.FeatureFlagSyncIntervalSeconds
go func() {
ticker := time.NewTicker(time.Duration(syncInterval) * time.Second)
defer ticker.Stop()
defer close(ps.featureFlagStopped)
if err := synchronizer.EnsureReady(); err != nil {
ps.logger.Warn("Problem connecting to feature flag management. Will fallback to cloud cache.", mlog.Err(err))
return
}
ps.updateFeatureFlagValuesFromManagement()
for {
select {
case <-ps.featureFlagStop:
return
case <-ticker.C:
ps.updateFeatureFlagValuesFromManagement()
}
}
}()
return nil
}
func (ps *PlatformService) StopFeatureFlagUpdateJob() {
if ps.featureFlagSynchronizer != nil {
close(ps.featureFlagStop)
<-ps.featureFlagStopped
ps.featureFlagSynchronizer.Close()
ps.featureFlagSynchronizer = nil
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import "sync/atomic"
// Go creates a goroutine, but maintains a record of it to ensure that execution completes before
// the server is shutdown.
func (ps *PlatformService) Go(f func()) {
atomic.AddInt32(&ps.goroutineCount, 1)
go func() {
f()
atomic.AddInt32(&ps.goroutineCount, -1)
select {
case ps.goroutineExitSignal <- struct{}{}:
default:
}
}()
}
// waitForGoroutines blocks until all goroutines created by PlatformService.Go() exit.
func (ps *PlatformService) waitForGoroutines() {
for atomic.LoadInt32(&ps.goroutineCount) != 0 {
<-ps.goroutineExitSignal
}
}
func (ps *PlatformService) GoBuffered(f func()) {
ps.goroutineBuffered <- struct{}{}
atomic.AddInt32(&ps.goroutineCount, 1)
go func() {
f()
atomic.AddInt32(&ps.goroutineCount, -1)
select {
case ps.goroutineExitSignal <- struct{}{}:
default:
}
<-ps.goroutineBuffered
}()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
LicenseEnv = "MM_LICENSE"
JWTDefaultTokenExpiration = 7 * 24 * time.Hour // 7 days of expiration
)
// JWTClaims custom JWT claims with the needed information for the
// renewal process
type JWTClaims struct {
LicenseID string `json:"license_id"`
ActiveUsers int64 `json:"active_users"`
jwt.StandardClaims
}
func (ps *PlatformService) LicenseManager() einterfaces.LicenseInterface {
return ps.licenseManager
}
func (ps *PlatformService) SetLicenseManager(impl einterfaces.LicenseInterface) {
ps.licenseManager = impl
}
func (ps *PlatformService) License() *model.License {
license, _ := ps.licenseValue.Load().(*model.License)
return license
}
func (ps *PlatformService) LoadLicense() {
// ENV var overrides all other sources of license.
licenseStr := os.Getenv(LicenseEnv)
if licenseStr != "" {
license, err := utils.LicenseValidator.LicenseFromBytes([]byte(licenseStr))
if err != nil {
ps.logger.Error("Failed to read license set in environment.", mlog.Err(err))
return
}
// skip the restrictions if license is a sanctioned trial
if !license.IsSanctionedTrial() && license.IsTrialLicense() {
canStartTrialLicense, err := ps.licenseManager.CanStartTrial()
if err != nil {
ps.logger.Error("Failed to validate trial eligibility.", mlog.Err(err))
return
}
if !canStartTrialLicense {
ps.logger.Info("Cannot start trial multiple times.")
return
}
}
if ps.ValidateAndSetLicenseBytes([]byte(licenseStr)) {
ps.logger.Info("License key from ENV is valid, unlocking enterprise features.")
}
return
}
licenseId := ""
props, nErr := ps.Store.System().Get()
if nErr == nil {
licenseId = props[model.SystemActiveLicenseId]
}
if !model.IsValidId(licenseId) {
// Lets attempt to load the file from disk since it was missing from the DB
license, licenseBytes := utils.GetAndValidateLicenseFileFromDisk(*ps.Config().ServiceSettings.LicenseFileLocation)
if license != nil {
if _, err := ps.SaveLicense(licenseBytes); err != nil {
ps.logger.Error("Failed to save license key loaded from disk.", mlog.Err(err))
} else {
licenseId = license.Id
}
}
}
record, nErr := ps.Store.License().Get(licenseId)
if nErr != nil {
ps.logger.Error("License key from https://mattermost.com required to unlock enterprise features.", mlog.Err(nErr))
ps.SetLicense(nil)
return
}
ps.ValidateAndSetLicenseBytes([]byte(record.Bytes))
ps.logger.Info("License key valid unlocking enterprise features.")
}
func (ps *PlatformService) SaveLicense(licenseBytes []byte) (*model.License, *model.AppError) {
success, licenseStr := utils.LicenseValidator.ValidateLicense(licenseBytes)
if !success {
return nil, model.NewAppError("addLicense", model.InvalidLicenseError, nil, "", http.StatusBadRequest)
}
var license model.License
if jsonErr := json.Unmarshal([]byte(licenseStr), &license); jsonErr != nil {
return nil, model.NewAppError("addLicense", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
uniqueUserCount, err := ps.Store.User().Count(model.UserCountOptions{})
if err != nil {
return nil, model.NewAppError("addLicense", "api.license.add_license.invalid_count.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if uniqueUserCount > int64(*license.Features.Users) {
return nil, model.NewAppError("addLicense", "api.license.add_license.unique_users.app_error", map[string]any{"Users": *license.Features.Users, "Count": uniqueUserCount}, "", http.StatusBadRequest)
}
if license.IsExpired() {
return nil, model.NewAppError("addLicense", model.ExpiredLicenseError, nil, "", http.StatusBadRequest)
}
if *ps.Config().JobSettings.RunJobs && ps.Jobs != nil {
if err := ps.Jobs.StopWorkers(); err != nil && !errors.Is(err, jobs.ErrWorkersNotRunning) {
ps.logger.Warn("Stopping job server workers failed", mlog.Err(err))
}
}
if *ps.Config().JobSettings.RunScheduler && ps.Jobs != nil {
if err := ps.Jobs.StopSchedulers(); err != nil && !errors.Is(err, jobs.ErrSchedulersNotRunning) {
ps.logger.Error("Stopping job server schedulers failed", mlog.Err(err))
}
}
defer func() {
// restart job server workers - this handles the edge case where a license file is uploaded, but the job server
// doesn't start until the server is restarted, which prevents the 'run job now' buttons in system console from
// functioning as expected
if *ps.Config().JobSettings.RunJobs && ps.Jobs != nil {
if err := ps.Jobs.StartWorkers(); err != nil {
ps.logger.Error("Starting job server workers failed", mlog.Err(err))
}
}
if *ps.Config().JobSettings.RunScheduler && ps.Jobs != nil {
if err := ps.Jobs.StartSchedulers(); err != nil && !errors.Is(err, jobs.ErrSchedulersRunning) {
ps.logger.Error("Starting job server schedulers failed", mlog.Err(err))
}
}
}()
if ok := ps.SetLicense(&license); !ok {
return nil, model.NewAppError("addLicense", model.ExpiredLicenseError, nil, "", http.StatusBadRequest)
}
record := &model.LicenseRecord{}
record.Id = license.Id
record.Bytes = string(licenseBytes)
_, nErr := ps.Store.License().Save(record)
if nErr != nil {
ps.RemoveLicense()
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("addLicense", "api.license.add_license.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
sysVar := &model.System{}
sysVar.Name = model.SystemActiveLicenseId
sysVar.Value = license.Id
if err := ps.Store.System().SaveOrUpdate(sysVar); err != nil {
ps.RemoveLicense()
return nil, model.NewAppError("addLicense", "api.license.add_license.save_active.app_error", nil, "", http.StatusInternalServerError)
}
// only on prem licenses set this in the first place
if !license.IsCloud() {
_, err := ps.Store.System().PermanentDeleteByName(model.SystemHostedPurchaseNeedsScreening)
if err != nil {
ps.logger.Warn(fmt.Sprintf("Failed to remove %s system store key", model.SystemHostedPurchaseNeedsScreening))
}
}
ps.ReloadConfig()
ps.InvalidateAllCaches()
return &license, nil
}
func (ps *PlatformService) SetLicense(license *model.License) bool {
oldLicense := ps.licenseValue.Load()
defer func() {
for _, listener := range ps.licenseListeners {
if oldLicense == nil {
listener(nil, license)
} else {
listener(oldLicense.(*model.License), license)
}
}
}()
if license != nil {
license.Features.SetDefaults()
ps.licenseValue.Store(license)
ps.clientLicenseValue.Store(utils.GetClientLicense(license))
return true
}
ps.licenseValue.Store((*model.License)(nil))
ps.clientLicenseValue.Store(map[string]string(nil))
return false
}
func (ps *PlatformService) ValidateAndSetLicenseBytes(b []byte) bool {
if success, licenseStr := utils.LicenseValidator.ValidateLicense(b); success {
var license model.License
if jsonErr := json.Unmarshal([]byte(licenseStr), &license); jsonErr != nil {
ps.logger.Warn("Failed to decode license from JSON", mlog.Err(jsonErr))
return false
}
ps.SetLicense(&license)
return true
}
ps.logger.Warn("No valid enterprise license found")
return false
}
func (ps *PlatformService) SetClientLicense(m map[string]string) {
ps.clientLicenseValue.Store(m)
}
func (ps *PlatformService) ClientLicense() map[string]string {
if clientLicense, _ := ps.clientLicenseValue.Load().(map[string]string); clientLicense != nil {
return clientLicense
}
return map[string]string{"IsLicensed": "false"}
}
func (ps *PlatformService) RemoveLicense() *model.AppError {
if license, _ := ps.licenseValue.Load().(*model.License); license == nil {
return nil
}
ps.logger.Info("Remove license.", mlog.String("id", model.SystemActiveLicenseId))
sysVar := &model.System{}
sysVar.Name = model.SystemActiveLicenseId
sysVar.Value = ""
if err := ps.Store.System().SaveOrUpdate(sysVar); err != nil {
return model.NewAppError("RemoveLicense", "app.system.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
ps.SetLicense(nil)
ps.ReloadConfig()
ps.InvalidateAllCaches()
return nil
}
func (ps *PlatformService) AddLicenseListener(listener func(oldLicense, newLicense *model.License)) string {
id := model.NewId()
ps.licenseListeners[id] = listener
return id
}
func (ps *PlatformService) RemoveLicenseListener(id string) {
delete(ps.licenseListeners, id)
}
func (ps *PlatformService) GetSanitizedClientLicense() map[string]string {
return utils.GetSanitizedClientLicense(ps.ClientLicense())
}
// RequestTrialLicense request a trial license from the mattermost official license server
func (ps *PlatformService) RequestTrialLicense(trialRequest *model.TrialLicenseRequest) *model.AppError {
trialRequestJSON, err := json.Marshal(trialRequest)
if err != nil {
return model.NewAppError("RequestTrialLicense", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
resp, err := http.Post(ps.getRequestTrialURL(), "application/json", bytes.NewBuffer(trialRequestJSON))
if err != nil {
return model.NewAppError("RequestTrialLicense", "api.license.request_trial_license.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
defer resp.Body.Close()
// CloudFlare sitting in front of the Customer Portal will block this request with a 451 response code in the event that the request originates from a country sanctioned by the U.S. Government.
if resp.StatusCode == http.StatusUnavailableForLegalReasons {
return model.NewAppError("RequestTrialLicense", "api.license.request_trial_license.embargoed", nil, "Request for trial license came from an embargoed country", http.StatusUnavailableForLegalReasons)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return model.NewAppError("RequestTrialLicense", "api.license.request_trial_license.app_error", nil,
fmt.Sprintf("Unexpected HTTP status code %q returned by server", resp.Status), http.StatusInternalServerError)
}
var licenseResponse map[string]string
err = json.NewDecoder(resp.Body).Decode(&licenseResponse)
if err != nil {
ps.logger.Warn("Error decoding license response", mlog.Err(err))
}
if _, ok := licenseResponse["license"]; !ok {
return model.NewAppError("RequestTrialLicense", "api.license.request_trial_license.app_error", nil, licenseResponse["message"], http.StatusBadRequest)
}
if _, err := ps.SaveLicense([]byte(licenseResponse["license"])); err != nil {
return err
}
ps.ReloadConfig()
ps.InvalidateAllCaches()
return nil
}
// GenerateRenewalToken returns a renewal token that expires after duration expiration
func (ps *PlatformService) GenerateRenewalToken(expiration time.Duration) (string, *model.AppError) {
license := ps.License()
if license == nil {
return "", model.NewAppError("GenerateRenewalToken", "app.license.generate_renewal_token.no_license", nil, "", http.StatusBadRequest)
}
if license.IsCloud() {
return "", model.NewAppError("GenerateRenewalToken", "app.license.generate_renewal_token.bad_license", nil, "", http.StatusBadRequest)
}
activeUsers, err := ps.Store.User().Count(model.UserCountOptions{})
if err != nil {
return "", model.NewAppError("GenerateRenewalToken", "app.license.generate_renewal_token.app_error",
nil, "", http.StatusInternalServerError).Wrap(err)
}
expirationTime := time.Now().UTC().Add(expiration)
claims := &JWTClaims{
LicenseID: license.Id,
ActiveUsers: activeUsers,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(license.Customer.Email))
if err != nil {
return "", model.NewAppError("GenerateRenewalToken", "app.license.generate_renewal_token.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return tokenString, nil
}
// GenerateLicenseRenewalLink returns a link that points to the CWS where clients can renew license
func (ps *PlatformService) GenerateLicenseRenewalLink() (string, string, *model.AppError) {
renewalToken, err := ps.GenerateRenewalToken(JWTDefaultTokenExpiration)
if err != nil {
return "", "", err
}
return fmt.Sprintf("%s?token=%s", ps.getLicenseRenewalURL(), renewalToken), renewalToken, nil
}
func (ps *PlatformService) getLicenseRenewalURL() string {
return fmt.Sprintf("%s/subscribe/renew", *ps.Config().CloudSettings.CWSURL)
}
func (ps *PlatformService) getRequestTrialURL() string {
return fmt.Sprintf("%s/api/v1/trials", *ps.Config().CloudSettings.CWSURL)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"time"
"github.com/mattermost/mattermost-server/v6/server/platform/services/cache"
)
const LinkCacheSize = 10000
const LinkCacheDuration = 1 * time.Hour
var linkCache = cache.NewLRU(cache.LRUOptions{
Size: LinkCacheSize,
})
func PurgeLinkCache() {
linkCache.Purge()
}
func LinkCache() cache.Cache {
return linkCache
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (ps *PlatformService) Log() mlog.LoggerIFace {
return ps.logger
}
func (ps *PlatformService) ReconfigureLogger() error {
return ps.initLogging()
}
// initLogging initializes and configures the logger(s). This may be called more than once.
func (ps *PlatformService) initLogging() error {
// create the app logger if needed
if ps.logger == nil {
var err error
ps.logger, err = mlog.NewLogger()
if err != nil {
return err
}
logCfg, err := config.MloggerConfigFromLoggerConfig(&ps.Config().LogSettings, nil, config.GetLogFileLocation)
if err != nil {
return err
}
if errCfg := ps.logger.ConfigureTargets(logCfg, nil); errCfg != nil {
return fmt.Errorf("failed to configure test logger: %w", errCfg)
}
}
// create notification logger if needed
if ps.notificationsLogger == nil {
l, err := mlog.NewLogger()
if err != nil {
return err
}
ps.notificationsLogger = l.With(mlog.String("logSource", "notifications"))
}
if err := ps.ConfigureLogger("logging", ps.logger, &ps.Config().LogSettings, config.GetLogFileLocation); err != nil {
// if the config is locked then a unit test has already configured and locked the logger; not an error.
if !errors.Is(err, mlog.ErrConfigurationLock) {
// revert to default logger if the config is invalid
mlog.InitGlobalLogger(nil)
return err
}
}
// Redirect default Go logger to app logger.
ps.logger.RedirectStdLog(mlog.LvlStdLog)
// Use the app logger as the global logger (eventually remove all instances of global logging).
mlog.InitGlobalLogger(ps.logger)
notificationLogSettings := config.GetLogSettingsFromNotificationsLogSettings(&ps.Config().NotificationLogSettings)
if err := ps.ConfigureLogger("notification logging", ps.notificationsLogger, notificationLogSettings, config.GetNotificationsLogFileLocation); err != nil {
if !errors.Is(err, mlog.ErrConfigurationLock) {
mlog.Error("Error configuring notification logger", mlog.Err(err))
return err
}
}
return nil
}
func (ps *PlatformService) Logger() *mlog.Logger {
return ps.logger
}
func (ps *PlatformService) NotificationsLogger() *mlog.Logger {
return ps.notificationsLogger
}
func (ps *PlatformService) EnableLoggingMetrics() {
if ps.metrics == nil || ps.metricsIFace == nil {
return
}
ps.logger.SetMetricsCollector(ps.metricsIFace.GetLoggerMetricsCollector(), mlog.DefaultMetricsUpdateFreqMillis)
// logging config needs to be reloaded when metrics collector is added or changed.
if err := ps.initLogging(); err != nil {
mlog.Error("Error re-configuring logging for metrics")
return
}
mlog.Debug("Logging metrics enabled")
}
// RemoveUnlicensedLogTargets removes any unlicensed log target types.
func (ps *PlatformService) RemoveUnlicensedLogTargets(license *model.License) {
if license != nil && *license.Features.AdvancedLogging {
// advanced logging enabled via license; no need to remove any targets
return
}
timeoutCtx, cancelCtx := context.WithTimeout(context.Background(), time.Second*10)
defer cancelCtx()
ps.logger.RemoveTargets(timeoutCtx, func(ti mlog.TargetInfo) bool {
return ti.Type != "*targets.Writer" && ti.Type != "*targets.File"
})
ps.notificationsLogger.RemoveTargets(timeoutCtx, func(ti mlog.TargetInfo) bool {
return ti.Type != "*targets.Writer" && ti.Type != "*targets.File"
})
}
func (ps *PlatformService) GetLogsSkipSend(page, perPage int, logFilter *model.LogFilter) ([]string, *model.AppError) {
var lines []string
if *ps.Config().LogSettings.EnableFile {
ps.Log().Flush()
logFile := config.GetLogFileLocation(*ps.Config().LogSettings.FileLocation)
file, err := os.Open(logFile)
if err != nil {
return nil, model.NewAppError("getLogs", "api.admin.file_read_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
defer file.Close()
var newLine = []byte{'\n'}
var lineCount int
const searchPos = -1
b := make([]byte, 1)
var endOffset int64 = 0
// if the file exists and it's last byte is '\n' - skip it
var stat os.FileInfo
if stat, err = os.Stat(logFile); err == nil {
if _, err = file.ReadAt(b, stat.Size()-1); err == nil && b[0] == newLine[0] {
endOffset = -1
}
}
lineEndPos, err := file.Seek(endOffset, io.SeekEnd)
if err != nil {
return nil, model.NewAppError("getLogs", "api.admin.file_read_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for {
pos, err := file.Seek(searchPos, io.SeekCurrent)
if err != nil {
return nil, model.NewAppError("getLogs", "api.admin.file_read_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
_, err = file.ReadAt(b, pos)
if err != nil {
return nil, model.NewAppError("getLogs", "api.admin.file_read_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if b[0] == newLine[0] || pos == 0 {
lineCount++
if lineCount > page*perPage {
line := make([]byte, lineEndPos-pos)
_, err := file.ReadAt(line, pos)
if err != nil {
return nil, model.NewAppError("getLogs", "api.admin.file_read_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
filtered := false
var entry *model.LogEntry
err = json.Unmarshal(line, &entry)
if err != nil {
mlog.Debug("Failed to parse line, skipping")
} else {
filtered = isLogFilteredByLevel(logFilter, entry) || filtered
filtered = isLogFilteredByDate(logFilter, entry) || filtered
}
if filtered {
lineCount--
} else {
lines = append(lines, string(line))
}
}
if pos == 0 {
break
}
lineEndPos = pos
}
if len(lines) == perPage {
break
}
}
for i, j := 0, len(lines)-1; i < j; i, j = i+1, j-1 {
lines[i], lines[j] = lines[j], lines[i]
}
} else {
lines = append(lines, "")
}
return lines, nil
}
func isLogFilteredByLevel(logFilter *model.LogFilter, entry *model.LogEntry) bool {
logLevels := logFilter.LogLevels
if len(logLevels) == 0 {
return false
}
for _, level := range logLevels {
if entry.Level == level {
return false
}
}
return true
}
func isLogFilteredByDate(logFilter *model.LogFilter, entry *model.LogEntry) bool {
if logFilter.DateFrom == "" && logFilter.DateTo == "" {
return false
}
dateFrom, err := time.Parse("2006-01-02 15:04:05.999 -07:00", logFilter.DateFrom)
if err != nil {
dateFrom = time.Time{}
}
dateTo, err := time.Parse("2006-01-02 15:04:05.999 -07:00", logFilter.DateTo)
if err != nil {
dateTo = time.Now()
}
timestamp, err := time.Parse("2006-01-02 15:04:05.999 -07:00", entry.Timestamp)
if err != nil {
mlog.Debug("Cannot parse timestamp, skipping")
return false
}
if timestamp.Equal(dateFrom) || timestamp.Equal(dateTo) {
return false
}
if timestamp.After(dateFrom) && timestamp.Before(dateTo) {
return false
}
return true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"context"
"fmt"
"net"
"net/http"
"net/http/pprof"
"runtime"
"sync"
"text/template"
"time"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const TimeToWaitForConnectionsToCloseOnServerShutdown = time.Second
type platformMetrics struct {
server *http.Server
router *mux.Router
lock sync.Mutex
logger *mlog.Logger
metricsImpl einterfaces.MetricsInterface
cfgFn func() *model.Config
listenAddr string
}
// resetMetrics resets the metrics server. Clears the metrics if the metrics are disabled by the config.
func (ps *PlatformService) resetMetrics() error {
if !*ps.Config().MetricsSettings.Enable {
if ps.metrics != nil {
return ps.metrics.stopMetricsServer()
}
return nil
}
if ps.metrics != nil {
if err := ps.metrics.stopMetricsServer(); err != nil {
return err
}
}
ps.metrics = &platformMetrics{
cfgFn: ps.Config,
metricsImpl: ps.metricsIFace,
logger: ps.logger,
}
if err := ps.metrics.initMetricsRouter(); err != nil {
return err
}
if ps.metricsIFace != nil {
ps.metricsIFace.Register()
}
return ps.metrics.startMetricsServer()
}
func (pm *platformMetrics) stopMetricsServer() error {
pm.lock.Lock()
defer pm.lock.Unlock()
if pm.server != nil {
ctx, cancel := context.WithTimeout(context.Background(), TimeToWaitForConnectionsToCloseOnServerShutdown)
defer cancel()
if err := pm.server.Shutdown(ctx); err != nil {
return fmt.Errorf("could not shutdown metrics server: %v", err)
}
pm.logger.Info("Metrics and profiling server is stopped")
}
return nil
}
func (pm *platformMetrics) startMetricsServer() error {
var notify chan struct{}
pm.lock.Lock()
defer func() {
if notify != nil {
<-notify
}
pm.lock.Unlock()
}()
l, err := net.Listen("tcp", *pm.cfgFn().MetricsSettings.ListenAddress)
if err != nil {
return err
}
notify = make(chan struct{})
pm.server = &http.Server{
Handler: handlers.RecoveryHandler(handlers.PrintRecoveryStack(true))(pm.router),
ReadTimeout: time.Duration(*pm.cfgFn().ServiceSettings.ReadTimeout) * time.Second,
WriteTimeout: time.Duration(*pm.cfgFn().ServiceSettings.WriteTimeout) * time.Second,
}
go func() {
close(notify)
if err := pm.server.Serve(l); err != nil && err != http.ErrServerClosed {
pm.logger.Fatal(err.Error())
}
}()
pm.listenAddr = l.Addr().String()
pm.logger.Info("Metrics and profiling server is started", mlog.String("address", pm.listenAddr))
return nil
}
func (pm *platformMetrics) initMetricsRouter() error {
pm.router = mux.NewRouter()
runtime.SetBlockProfileRate(*pm.cfgFn().MetricsSettings.BlockProfileRate)
metricsPage := `
<html>
<body>{{if .}}
<div><a href="/metrics">Metrics</a></div>{{end}}
<div><a href="/debug/pprof/">Profiling Root</a></div>
<div><a href="/debug/pprof/cmdline">Profiling Command Line</a></div>
<div><a href="/debug/pprof/symbol">Profiling Symbols</a></div>
<div><a href="/debug/pprof/goroutine">Profiling Goroutines</a></div>
<div><a href="/debug/pprof/heap">Profiling Heap</a></div>
<div><a href="/debug/pprof/threadcreate">Profiling Threads</a></div>
<div><a href="/debug/pprof/block">Profiling Blocking</a></div>
<div><a href="/debug/pprof/trace">Profiling Execution Trace</a></div>
<div><a href="/debug/pprof/profile">Profiling CPU</a></div>
</body>
</html>
`
metricsPageTmpl, err := template.New("page").Parse(metricsPage)
if err != nil {
return errors.Wrap(err, "failed to create template")
}
rootHandler := func(w http.ResponseWriter, r *http.Request) {
metricsPageTmpl.Execute(w, pm.metricsImpl != nil)
}
pm.router.HandleFunc("/", rootHandler)
pm.router.StrictSlash(true)
pm.router.Handle("/debug", http.RedirectHandler("/", http.StatusMovedPermanently))
pm.router.HandleFunc("/debug/pprof/", pprof.Index)
pm.router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
pm.router.HandleFunc("/debug/pprof/profile", pprof.Profile)
pm.router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
pm.router.HandleFunc("/debug/pprof/trace", pprof.Trace)
// Manually add support for paths linked to by index page at /debug/pprof/
pm.router.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine"))
pm.router.Handle("/debug/pprof/heap", pprof.Handler("heap"))
pm.router.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
pm.router.Handle("/debug/pprof/block", pprof.Handler("block"))
return nil
}
func (ps *PlatformService) HandleMetrics(route string, h http.Handler) {
if ps.metrics != nil {
ps.metrics.router.Handle(route, h)
}
}
func (ps *PlatformService) RestartMetrics() error {
return ps.resetMetrics()
}
func (ps *PlatformService) Metrics() einterfaces.MetricsInterface {
if ps.metrics == nil {
return nil
}
return ps.metricsIFace
}
// Code generated by mockery v2.14.0. DO NOT EDIT.
// Regenerate this file using `make platform-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// SuiteIFace is an autogenerated mock type for the SuiteIFace type
type SuiteIFace struct {
mock.Mock
}
// GetSession provides a mock function with given fields: token
func (_m *SuiteIFace) GetSession(token string) (*model.Session, *model.AppError) {
ret := _m.Called(token)
var r0 *model.Session
if rf, ok := ret.Get(0).(func(string) *model.Session); ok {
r0 = rf(token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Session)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(string) *model.AppError); ok {
r1 = rf(token)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// RolesGrantPermission provides a mock function with given fields: roleNames, permissionId
func (_m *SuiteIFace) RolesGrantPermission(roleNames []string, permissionId string) bool {
ret := _m.Called(roleNames, permissionId)
var r0 bool
if rf, ok := ret.Get(0).(func([]string, string) bool); ok {
r0 = rf(roleNames, permissionId)
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// UserCanSeeOtherUser provides a mock function with given fields: userID, otherUserId
func (_m *SuiteIFace) UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError) {
ret := _m.Called(userID, otherUserId)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string) bool); ok {
r0 = rf(userID, otherUserId)
} else {
r0 = ret.Get(0).(bool)
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(string, string) *model.AppError); ok {
r1 = rf(userID, otherUserId)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
type mockConstructorTestingTNewSuiteIFace interface {
mock.TestingT
Cleanup(func())
}
// NewSuiteIFace creates a new instance of SuiteIFace. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewSuiteIFace(t mockConstructorTestingTNewSuiteIFace) *SuiteIFace {
mock := &SuiteIFace{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"fmt"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/localcachelayer"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type Option func(ps *PlatformService) error
// By default, the app will use the store specified by the configuration. This allows you to
// construct an app with a different store.
//
// The override parameter must be either a store.Store or func(App) store.Store().
func StoreOverride(override any) Option {
return func(ps *PlatformService) error {
switch o := override.(type) {
case store.Store:
ps.newStore = func() (store.Store, error) {
return o, nil
}
return nil
case func(*PlatformService) store.Store:
ps.newStore = func() (store.Store, error) {
return o(ps), nil
}
return nil
default:
return errors.New("invalid StoreOverride")
}
}
}
func StoreOverrideWithCache(override store.Store) Option {
return func(ps *PlatformService) error {
ps.newStore = func() (store.Store, error) {
lcl, err := localcachelayer.NewLocalCacheLayer(override, ps.metricsIFace, ps.clusterIFace, ps.cacheProvider)
if err != nil {
return nil, err
}
return lcl, nil
}
return nil
}
}
// Config applies the given config dsn, whether a path to config.json
// or a database connection string. It receives as well a set of
// custom defaults that will be applied for any unset property of the
// config loaded from the dsn on top of the normal defaults
func Config(dsn string, readOnly bool, configDefaults *model.Config) Option {
return func(ps *PlatformService) error {
configStore, err := config.NewStoreFromDSN(dsn, readOnly, configDefaults, true)
if err != nil {
return fmt.Errorf("failed to apply Config option: %w", err)
}
ps.configStore = configStore
return nil
}
}
func SetFileStore(filestore filestore.FileBackend) Option {
return func(ps *PlatformService) error {
ps.filestore = filestore
return nil
}
}
// ConfigStore applies the given config store, typically to replace the traditional sources with a memory store for testing.
func ConfigStore(configStore *config.Store) Option {
return func(ps *PlatformService) error {
ps.configStore = configStore
return nil
}
}
func StartMetrics() Option {
return func(ps *PlatformService) error {
ps.startMetrics = true
return nil
}
}
func SetLogger(logger *mlog.Logger) Option {
return func(ps *PlatformService) error {
ps.SetLogger(logger)
return nil
}
}
func SetCluster(cluster einterfaces.ClusterInterface) Option {
return func(ps *PlatformService) error {
ps.clusterIFace = cluster
return nil
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (ps *PlatformService) StartSearchEngine() (string, string) {
if ps.SearchEngine.ElasticsearchEngine != nil && ps.SearchEngine.ElasticsearchEngine.IsActive() {
ps.Go(func() {
if err := ps.SearchEngine.ElasticsearchEngine.Start(); err != nil {
ps.Log().Error(err.Error())
}
})
}
configListenerId := ps.AddConfigListener(func(oldConfig *model.Config, newConfig *model.Config) {
if ps.SearchEngine == nil {
return
}
ps.SearchEngine.UpdateConfig(newConfig)
if ps.SearchEngine.ElasticsearchEngine != nil && !*oldConfig.ElasticsearchSettings.EnableIndexing && *newConfig.ElasticsearchSettings.EnableIndexing {
ps.Go(func() {
if err := ps.SearchEngine.ElasticsearchEngine.Start(); err != nil {
mlog.Error(err.Error())
}
})
} else if ps.SearchEngine.ElasticsearchEngine != nil && *oldConfig.ElasticsearchSettings.EnableIndexing && !*newConfig.ElasticsearchSettings.EnableIndexing {
ps.Go(func() {
if err := ps.SearchEngine.ElasticsearchEngine.Stop(); err != nil {
mlog.Error(err.Error())
}
})
} else if ps.SearchEngine.ElasticsearchEngine != nil && *oldConfig.ElasticsearchSettings.Password != *newConfig.ElasticsearchSettings.Password || *oldConfig.ElasticsearchSettings.Username != *newConfig.ElasticsearchSettings.Username || *oldConfig.ElasticsearchSettings.ConnectionURL != *newConfig.ElasticsearchSettings.ConnectionURL || *oldConfig.ElasticsearchSettings.Sniff != *newConfig.ElasticsearchSettings.Sniff {
ps.Go(func() {
if *oldConfig.ElasticsearchSettings.EnableIndexing {
if err := ps.SearchEngine.ElasticsearchEngine.Stop(); err != nil {
mlog.Error(err.Error())
}
if err := ps.SearchEngine.ElasticsearchEngine.Start(); err != nil {
mlog.Error(err.Error())
}
}
})
}
})
licenseListenerId := ps.AddLicenseListener(func(oldLicense, newLicense *model.License) {
if ps.SearchEngine == nil {
return
}
if oldLicense == nil && newLicense != nil {
if ps.SearchEngine.ElasticsearchEngine != nil && ps.SearchEngine.ElasticsearchEngine.IsActive() {
ps.Go(func() {
if err := ps.SearchEngine.ElasticsearchEngine.Start(); err != nil {
mlog.Error(err.Error())
}
})
}
} else if oldLicense != nil && newLicense == nil {
if ps.SearchEngine.ElasticsearchEngine != nil {
ps.Go(func() {
if err := ps.SearchEngine.ElasticsearchEngine.Stop(); err != nil {
mlog.Error(err.Error())
}
})
}
}
})
return configListenerId, licenseListenerId
}
func (ps *PlatformService) StopSearchEngine() {
ps.RemoveConfigListener(ps.searchConfigListenerId)
ps.RemoveLicenseListener(ps.searchLicenseListenerId)
if ps.SearchEngine != nil && ps.SearchEngine.ElasticsearchEngine != nil && ps.SearchEngine.ElasticsearchEngine.IsActive() {
ps.SearchEngine.ElasticsearchEngine.Stop()
}
if ps.SearchEngine != nil && ps.SearchEngine.BleveEngine != nil && ps.SearchEngine.BleveEngine.IsActive() {
ps.SearchEngine.BleveEngine.Stop()
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"fmt"
"hash/maphash"
"net/http"
"runtime"
"sync"
"sync/atomic"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/featureflag"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/localcachelayer"
"github.com/mattermost/mattermost-server/v6/server/channels/store/retrylayer"
"github.com/mattermost/mattermost-server/v6/server/channels/store/searchlayer"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/channels/store/timerlayer"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/services/cache"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine/bleveengine"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// PlatformService is the service for the platform related tasks. It is
// responsible for non-entity related functionalities that are required
// by a product such as database access, configuration access, licensing etc.
type PlatformService struct {
sqlStore *sqlstore.SqlStore
Store store.Store
newStore func() (store.Store, error)
WebSocketRouter *WebSocketRouter
configStore *config.Store
filestore filestore.FileBackend
cacheProvider cache.Provider
statusCache cache.Cache
sessionCache cache.Cache
sessionPool sync.Pool
asymmetricSigningKey atomic.Value
clientConfig atomic.Value
clientConfigHash atomic.Value
limitedClientConfig atomic.Value
logger *mlog.Logger
notificationsLogger *mlog.Logger
startMetrics bool
metrics *platformMetrics
metricsIFace einterfaces.MetricsInterface
featureFlagSynchronizerMutex sync.Mutex
featureFlagSynchronizer *featureflag.Synchronizer
featureFlagStop chan struct{}
featureFlagStopped chan struct{}
licenseValue atomic.Value
clientLicenseValue atomic.Value
licenseListeners map[string]func(*model.License, *model.License)
licenseManager einterfaces.LicenseInterface
telemetryId string
configListenerId string
licenseListenerId string
clusterLeaderListeners sync.Map
clusterIFace einterfaces.ClusterInterface
Busy *Busy
SearchEngine *searchengine.Broker
searchConfigListenerId string
searchLicenseListenerId string
Jobs *jobs.JobServer
hubs []*Hub
hashSeed maphash.Seed
goroutineCount int32
goroutineExitSignal chan struct{}
goroutineBuffered chan struct{}
additionalClusterHandlers map[model.ClusterEvent]einterfaces.ClusterMessageHandler
sharedChannelService SharedChannelServiceIFace
pluginEnv HookRunner
}
type HookRunner interface {
RunMultiHook(hookRunnerFunc func(hooks plugin.Hooks) bool, hookId int)
GetPluginsEnvironment() *plugin.Environment
}
// New creates a new PlatformService.
func New(sc ServiceConfig, options ...Option) (*PlatformService, error) {
// Step 0: Create the PlatformService.
// ConfigStore is and should be handled on a upper level.
ps := &PlatformService{
Store: sc.Store,
configStore: sc.ConfigStore,
clusterIFace: sc.Cluster,
hashSeed: maphash.MakeSeed(),
goroutineExitSignal: make(chan struct{}, 1),
goroutineBuffered: make(chan struct{}, runtime.NumCPU()),
WebSocketRouter: &WebSocketRouter{
handlers: make(map[string]webSocketHandler),
},
sessionPool: sync.Pool{
New: func() any {
return &model.Session{}
},
},
licenseListeners: map[string]func(*model.License, *model.License){},
additionalClusterHandlers: map[model.ClusterEvent]einterfaces.ClusterMessageHandler{},
}
// Step 1: Cache provider.
// At the moment we only have this implementation
// in the future the cache provider will be built based on the loaded config
ps.cacheProvider = cache.NewProvider()
if err2 := ps.cacheProvider.Connect(); err2 != nil {
return nil, fmt.Errorf("unable to connect to cache provider: %w", err2)
}
// Apply options, some of the options overrides the default config actually.
for _, option := range options {
if err := option(ps); err != nil {
return nil, fmt.Errorf("failed to apply option: %w", err)
}
}
// the config store is not set, we need to create a new one
if ps.configStore == nil {
innerStore, err := config.NewFileStore("config.json", true)
if err != nil {
return nil, fmt.Errorf("failed to load config from file: %w", err)
}
configStore, err := config.NewStoreFromBacking(innerStore, nil, false)
if err != nil {
return nil, fmt.Errorf("failed to load config from file: %w", err)
}
ps.configStore = configStore
}
// Step 2: Start logging.
if err := ps.initLogging(); err != nil {
return nil, fmt.Errorf("failed to initialize logging: %w", err)
}
// This is called after initLogging() to avoid a race condition.
mlog.Info("Server is initializing...", mlog.String("go_version", runtime.Version()))
// Step 3: Search Engine
searchEngine := searchengine.NewBroker(ps.Config())
bleveEngine := bleveengine.NewBleveEngine(ps.Config())
if err := bleveEngine.Start(); err != nil {
return nil, err
}
searchEngine.RegisterBleveEngine(bleveEngine)
ps.SearchEngine = searchEngine
// Step 4: Init Enterprise
// Depends on step 3 (s.SearchEngine must be non-nil)
ps.initEnterprise()
// Step 5: Init Metrics
if metricsInterfaceFn != nil && ps.metricsIFace == nil { // if the metrics interface is set by options, do not override it
ps.metricsIFace = metricsInterfaceFn(ps, *ps.configStore.Get().SqlSettings.DriverName, *ps.configStore.Get().SqlSettings.DataSource)
}
// Step 6: Store.
// Depends on Step 0 (config), 1 (cacheProvider), 3 (search engine), 5 (metrics) and cluster.
if ps.newStore == nil {
ps.newStore = func() (store.Store, error) {
ps.sqlStore = sqlstore.New(ps.Config().SqlSettings, ps.metricsIFace)
lcl, err2 := localcachelayer.NewLocalCacheLayer(
retrylayer.New(ps.sqlStore),
ps.metricsIFace,
ps.clusterIFace,
ps.cacheProvider,
)
if err2 != nil {
return nil, fmt.Errorf("cannot create local cache layer: %w", err2)
}
searchStore := searchlayer.NewSearchLayer(
lcl,
ps.SearchEngine,
ps.Config(),
)
ps.AddConfigListener(func(prevCfg, cfg *model.Config) {
searchStore.UpdateConfig(cfg)
})
license := ps.License()
ps.sqlStore.UpdateLicense(license)
ps.AddLicenseListener(func(oldLicense, newLicense *model.License) {
ps.sqlStore.UpdateLicense(newLicense)
})
return timerlayer.New(
searchStore,
ps.metricsIFace,
), nil
}
}
license := ps.License()
// Step 3: Initialize filestore
if ps.filestore == nil {
insecure := ps.Config().ServiceSettings.EnableInsecureOutgoingConnections
backend, err2 := filestore.NewFileBackend(ps.Config().FileSettings.ToFileBackendSettings(license != nil && *license.Features.Compliance, insecure != nil && *insecure))
if err2 != nil {
return nil, fmt.Errorf("failed to initialize filebackend: %w", err2)
}
ps.filestore = backend
}
var err error
ps.Store, err = ps.newStore()
if err != nil {
return nil, fmt.Errorf("cannot create store: %w", err)
}
// Needed before loading license
ps.statusCache, err = ps.cacheProvider.NewCache(&cache.CacheOptions{
Size: model.StatusCacheSize,
Striped: true,
StripedBuckets: maxInt(runtime.NumCPU()-1, 1),
})
if err != nil {
return nil, fmt.Errorf("unable to create status cache: %w", err)
}
ps.sessionCache, err = ps.cacheProvider.NewCache(&cache.CacheOptions{
Size: model.SessionCacheSize,
Striped: true,
StripedBuckets: maxInt(runtime.NumCPU()-1, 1),
})
if err != nil {
return nil, fmt.Errorf("could not create session cache: %w", err)
}
// Step 7: Init License
if model.BuildEnterpriseReady == "true" {
ps.LoadLicense()
}
// Step 8: Init Metrics Server depends on step 6 (store) and 7 (license)
if ps.startMetrics {
if mErr := ps.resetMetrics(); mErr != nil {
return nil, mErr
}
ps.configStore.AddListener(func(oldCfg, newCfg *model.Config) {
if *oldCfg.MetricsSettings.Enable != *newCfg.MetricsSettings.Enable || *oldCfg.MetricsSettings.ListenAddress != *newCfg.MetricsSettings.ListenAddress {
if mErr := ps.resetMetrics(); mErr != nil {
mlog.Warn("Failed to reset metrics", mlog.Err(mErr))
}
}
})
}
// Step 9: Init AsymmetricSigningKey depends on step 6 (store)
if err = ps.EnsureAsymmetricSigningKey(); err != nil {
return nil, fmt.Errorf("unable to ensure asymmetric signing key: %w", err)
}
ps.Busy = NewBusy(ps.clusterIFace)
// Enable developer settings if this is a "dev" build
if model.BuildNumber == "dev" {
ps.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableDeveloper = true })
}
ps.AddLicenseListener(func(oldLicense, newLicense *model.License) {
if (oldLicense == nil && newLicense == nil) || !ps.startMetrics {
return
}
if oldLicense != nil && newLicense != nil && *oldLicense.Features.Metrics == *newLicense.Features.Metrics {
return
}
if err := ps.RestartMetrics(); err != nil {
ps.logger.Error("Failed to reset metrics server", mlog.Err(err))
}
})
ps.SearchEngine.UpdateConfig(ps.Config())
searchConfigListenerId, searchLicenseListenerId := ps.StartSearchEngine()
ps.searchConfigListenerId = searchConfigListenerId
ps.searchLicenseListenerId = searchLicenseListenerId
return ps, nil
}
func (ps *PlatformService) Start() error {
ps.hubStart()
ps.configListenerId = ps.AddConfigListener(func(_, _ *model.Config) {
ps.regenerateClientConfig()
message := model.NewWebSocketEvent(model.WebsocketEventConfigChanged, "", "", "", nil, "")
message.Add("config", ps.ClientConfigWithComputed())
ps.Go(func() {
ps.Publish(message)
})
if err := ps.ReconfigureLogger(); err != nil {
mlog.Error("Error re-configuring logging after config change", mlog.Err(err))
return
}
})
ps.licenseListenerId = ps.AddLicenseListener(func(oldLicense, newLicense *model.License) {
ps.regenerateClientConfig()
message := model.NewWebSocketEvent(model.WebsocketEventLicenseChanged, "", "", "", nil, "")
message.Add("license", ps.GetSanitizedClientLicense())
ps.Go(func() {
ps.Publish(message)
})
})
return nil
}
func (ps *PlatformService) ShutdownMetrics() error {
if ps.metrics != nil {
return ps.metrics.stopMetricsServer()
}
return nil
}
func (ps *PlatformService) ShutdownConfig() error {
ps.RemoveConfigListener(ps.configListenerId)
if ps.configStore != nil {
err := ps.configStore.Close()
if err != nil {
return fmt.Errorf("failed to close config store: %w", err)
}
}
return nil
}
func (ps *PlatformService) SetTelemetryId(id string) {
ps.telemetryId = id
}
func (ps *PlatformService) SetLogger(logger *mlog.Logger) {
ps.logger = logger
}
func (ps *PlatformService) initEnterprise() {
if clusterInterface != nil && ps.clusterIFace == nil {
ps.clusterIFace = clusterInterface(ps)
}
if elasticsearchInterface != nil {
ps.SearchEngine.RegisterElasticsearchEngine(elasticsearchInterface(ps))
}
if licenseInterface != nil {
ps.licenseManager = licenseInterface(ps)
}
}
func (ps *PlatformService) TotalWebsocketConnections() int {
// This method is only called after the hub is initialized.
// Therefore, no mutex is needed to protect s.hubs.
count := int64(0)
for _, hub := range ps.hubs {
count = count + atomic.LoadInt64(&hub.connectionCount)
}
return int(count)
}
func (ps *PlatformService) Shutdown() error {
ps.HubStop()
ps.RemoveLicenseListener(ps.licenseListenerId)
// we need to wait the goroutines to finish before closing the store
// and this needs to be called after hub stop because hub generates goroutines
// when it is active. If we wait first we have no mechanism to prevent adding
// more go routines hence they still going to be invoked.
ps.waitForGoroutines()
if ps.Store != nil {
ps.Store.Close()
}
if ps.cacheProvider != nil {
if err := ps.cacheProvider.Close(); err != nil {
return fmt.Errorf("unable to cleanly shutdown cache: %w", err)
}
}
return nil
}
func (ps *PlatformService) CacheProvider() cache.Provider {
return ps.cacheProvider
}
func (ps *PlatformService) StatusCache() cache.Cache {
return ps.statusCache
}
// SetSqlStore is used for plugin testing
func (ps *PlatformService) SetSqlStore(s *sqlstore.SqlStore) {
ps.sqlStore = s
}
func (ps *PlatformService) SetSharedChannelService(s SharedChannelServiceIFace) {
ps.sharedChannelService = s
}
func (ps *PlatformService) SetPluginsEnvironment(runner HookRunner) {
ps.pluginEnv = runner
}
// GetPluginStatuses meant to be used by cluster implementation
func (ps *PlatformService) GetPluginStatuses() (model.PluginStatuses, *model.AppError) {
if ps.pluginEnv == nil || ps.pluginEnv.GetPluginsEnvironment() == nil {
return nil, model.NewAppError("GetPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
pluginStatuses, err := ps.pluginEnv.GetPluginsEnvironment().Statuses()
if err != nil {
return nil, model.NewAppError("GetPluginStatuses", "app.plugin.get_statuses.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Add our cluster ID
for _, status := range pluginStatuses {
if ps.Cluster() != nil {
status.ClusterId = ps.Cluster().GetClusterId()
} else {
status.ClusterId = ""
}
}
return pluginStatuses, nil
}
func (ps *PlatformService) FileBackend() filestore.FileBackend {
return ps.filestore
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"context"
"fmt"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (ps *PlatformService) ReturnSessionToPool(session *model.Session) {
if session != nil {
session.Id = ""
ps.sessionPool.Put(session)
}
}
func (ps *PlatformService) CreateSession(session *model.Session) (*model.Session, error) {
session.Token = ""
session, err := ps.Store.Session().Save(session)
if err != nil {
return nil, err
}
ps.AddSessionToCache(session)
return session, nil
}
func (ps *PlatformService) GetSessionContext(ctx context.Context, token string) (*model.Session, error) {
return ps.Store.Session().Get(ctx, token)
}
func (ps *PlatformService) GetSessions(userID string) ([]*model.Session, error) {
return ps.Store.Session().GetSessions(userID)
}
func (ps *PlatformService) AddSessionToCache(session *model.Session) {
ps.sessionCache.SetWithExpiry(session.Token, session, time.Duration(int64(*ps.Config().ServiceSettings.SessionCacheInMinutes))*time.Minute)
}
func (ps *PlatformService) SessionCacheLength() int {
if l, err := ps.sessionCache.Len(); err == nil {
return l
}
return 0
}
func (ps *PlatformService) ClearUserSessionCacheLocal(userID string) {
if keys, err := ps.sessionCache.Keys(); err == nil {
var session *model.Session
for _, key := range keys {
if err := ps.sessionCache.Get(key, &session); err == nil {
if session.UserId == userID {
ps.sessionCache.Remove(key)
if m := ps.metricsIFace; m != nil {
m.IncrementMemCacheInvalidationCounterSession()
}
}
}
}
}
}
func (ps *PlatformService) ClearAllUsersSessionCacheLocal() {
ps.sessionCache.Purge()
}
func (ps *PlatformService) ClearUserSessionCache(userID string) {
ps.ClearUserSessionCacheLocal(userID)
if ps.clusterIFace != nil {
msg := &model.ClusterMessage{
Event: model.ClusterEventClearSessionCacheForUser,
SendType: model.ClusterSendReliable,
Data: []byte(userID),
}
ps.clusterIFace.SendClusterMessage(msg)
}
}
func (ps *PlatformService) ClearAllUsersSessionCache() {
ps.ClearAllUsersSessionCacheLocal()
if ps.clusterIFace != nil {
msg := &model.ClusterMessage{
Event: model.ClusterEventClearSessionCacheForAllUsers,
SendType: model.ClusterSendReliable,
}
ps.clusterIFace.SendClusterMessage(msg)
}
}
func (ps *PlatformService) GetSession(token string) (*model.Session, error) {
var session = ps.sessionPool.Get().(*model.Session)
if err := ps.sessionCache.Get(token, session); err == nil {
if m := ps.metricsIFace; m != nil {
m.IncrementMemCacheHitCounterSession()
}
} else {
if m := ps.metricsIFace; m != nil {
m.IncrementMemCacheMissCounterSession()
}
}
if session.Id != "" {
return session, nil
}
return ps.GetSessionContext(sqlstore.WithMaster(context.Background()), token)
}
func (ps *PlatformService) GetSessionByID(sessionID string) (*model.Session, error) {
return ps.Store.Session().Get(context.Background(), sessionID)
}
func (ps *PlatformService) RevokeSessionsFromAllUsers() error {
// revoke tokens before sessions so they can't be used to relogin
nErr := ps.Store.OAuth().RemoveAllAccessData()
if nErr != nil {
return fmt.Errorf("%s: %w", nErr.Error(), DeleteAllAccessDataError)
}
err := ps.Store.Session().RemoveAllSessions()
if err != nil {
return err
}
ps.ClearAllUsersSessionCache()
return nil
}
func (ps *PlatformService) RevokeSessionsForDeviceId(userID string, deviceID string, currentSessionId string) error {
sessions, err := ps.Store.Session().GetSessions(userID)
if err != nil {
return err
}
for _, session := range sessions {
if session.DeviceId == deviceID && session.Id != currentSessionId {
mlog.Debug("Revoking sessionId for userId. Re-login with the same device Id", mlog.String("session_id", session.Id), mlog.String("user_id", userID))
if err := ps.RevokeSession(session); err != nil {
mlog.Warn("Could not revoke session for device", mlog.String("device_id", deviceID), mlog.Err(err))
}
}
}
return nil
}
func (ps *PlatformService) RevokeSession(session *model.Session) error {
if session.IsOAuth {
if err := ps.RevokeAccessToken(session.Token); err != nil {
return err
}
} else {
if err := ps.Store.Session().Remove(session.Id); err != nil {
return fmt.Errorf("%s: %w", err.Error(), DeleteSessionError)
}
}
ps.ClearUserSessionCache(session.UserId)
return nil
}
func (ps *PlatformService) RevokeAccessToken(token string) error {
session, _ := ps.GetSession(token)
defer ps.ReturnSessionToPool(session)
schan := make(chan error, 1)
go func() {
schan <- ps.Store.Session().Remove(token)
close(schan)
}()
if _, err := ps.Store.OAuth().GetAccessData(token); err != nil {
return fmt.Errorf("%s: %w", err.Error(), GetTokenError)
}
if err := ps.Store.OAuth().RemoveAccessData(token); err != nil {
return fmt.Errorf("%s: %w", err.Error(), DeleteTokenError)
}
if err := <-schan; err != nil {
return fmt.Errorf("%s: %w", err.Error(), DeleteSessionError)
}
if session != nil {
ps.ClearUserSessionCache(session.UserId)
}
return nil
}
// SetSessionExpireInHours sets the session's expiry the specified number of hours
// relative to either the session creation date or the current time, depending
// on the `ExtendSessionOnActivity` config setting.
func (ps *PlatformService) SetSessionExpireInHours(session *model.Session, hours int) {
if session.CreateAt == 0 || *ps.Config().ServiceSettings.ExtendSessionLengthWithActivity {
session.ExpiresAt = model.GetMillis() + (1000 * 60 * 60 * int64(hours))
} else {
session.ExpiresAt = session.CreateAt + (1000 * 60 * 60 * int64(hours))
}
}
func (ps *PlatformService) ExtendSessionExpiry(session *model.Session, newExpiry int64) error {
if err := ps.Store.Session().UpdateExpiresAt(session.Id, newExpiry); err != nil {
return err
}
// Update local cache. No need to invalidate cache for cluster as the session cache timeout
// ensures each node will get an extended expiry within the next 10 minutes.
// Worst case is another node may generate a redundant expiry update.
session.ExpiresAt = newExpiry
ps.AddSessionToCache(session)
return nil
}
func (ps *PlatformService) UpdateSessionsIsGuest(userID string, isGuest bool) error {
sessions, err := ps.GetSessions(userID)
if err != nil {
return err
}
for _, session := range sessions {
session.AddProp(model.SessionPropIsGuest, fmt.Sprintf("%t", isGuest))
err := ps.Store.Session().UpdateProps(session)
if err != nil {
mlog.Warn("Unable to update isGuest session", mlog.Err(err))
continue
}
ps.AddSessionToCache(session)
}
return nil
}
func (ps *PlatformService) RevokeAllSessions(userID string) error {
sessions, err := ps.Store.Session().GetSessions(userID)
if err != nil {
return fmt.Errorf("%s: %w", err.Error(), GetSessionError)
}
for _, session := range sessions {
if session.IsOAuth {
ps.RevokeAccessToken(session.Token)
} else {
if err := ps.Store.Session().Remove(session.Id); err != nil {
return fmt.Errorf("%s: %w", err.Error(), DeleteSessionError)
}
}
}
ps.ClearUserSessionCache(userID)
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/sharedchannel"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var sharedChannelEventsForSync model.StringArray = []string{
model.WebsocketEventPosted,
model.WebsocketEventPostEdited,
model.WebsocketEventPostDeleted,
model.WebsocketEventReactionAdded,
model.WebsocketEventReactionRemoved,
}
var sharedChannelEventsForInvitation model.StringArray = []string{
model.WebsocketEventDirectAdded,
}
// SharedChannelSyncHandler is called when a websocket event is received by a cluster node.
// Only on the leader node it will notify the sync service to perform necessary updates to the remote for the given
// shared channel.
func (ps *PlatformService) SharedChannelSyncHandler(event *model.WebSocketEvent) {
syncService := ps.sharedChannelService
if syncService == nil {
return
}
if isEligibleForEvents(syncService, event, sharedChannelEventsForSync) {
err := handleContentSync(ps, syncService, event)
if err != nil {
mlog.Warn(
err.Error(),
mlog.String("event", event.EventType()),
mlog.String("action", "content_sync"),
)
}
} else if isEligibleForEvents(syncService, event, sharedChannelEventsForInvitation) {
err := handleInvitation(ps, syncService, event)
if err != nil {
mlog.Warn(
err.Error(),
mlog.String("event", event.EventType()),
mlog.String("action", "invitation"),
)
}
}
}
func isEligibleForEvents(syncService SharedChannelServiceIFace, event *model.WebSocketEvent, events model.StringArray) bool {
return syncServiceEnabled(syncService) &&
eventHasChannel(event) &&
events.Contains(event.EventType())
}
func eventHasChannel(event *model.WebSocketEvent) bool {
return event.GetBroadcast() != nil &&
event.GetBroadcast().ChannelId != ""
}
func syncServiceEnabled(syncService SharedChannelServiceIFace) bool {
return syncService != nil &&
syncService.Active()
}
func handleContentSync(ps *PlatformService, syncService SharedChannelServiceIFace, event *model.WebSocketEvent) error {
channel, err := findChannel(ps, event.GetBroadcast().ChannelId)
if err != nil {
return err
}
if channel != nil && channel.IsShared() {
syncService.NotifyChannelChanged(channel.Id)
}
return nil
}
func handleInvitation(ps *PlatformService, syncService SharedChannelServiceIFace, event *model.WebSocketEvent) error {
channel, err := findChannel(ps, event.GetBroadcast().ChannelId)
if err != nil {
return err
}
if channel == nil || !channel.IsShared() {
return nil
}
creator, err := getUserFromEvent(ps, event, "creator_id")
if err != nil {
return err
}
// This is a termination condition, since on the other end when we are processing
// the invite we are re-triggering a model.WEBSOCKET_EVENT_DIRECT_ADDED, which will call this handler.
// When the creator is remote, it means that this is a DM that was not originated from the current server
// and therefore we do not need to do anything.
if creator == nil || creator.IsRemote() {
return nil
}
participant, err := getUserFromEvent(ps, event, "teammate_id")
if err != nil {
return err
}
if participant == nil || participant.RemoteId == nil {
return nil
}
rc, err := ps.Store.RemoteCluster().Get(*participant.RemoteId)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("couldn't find remote cluster %s, for creating shared channel invitation for a DM", *participant.RemoteId))
}
return syncService.SendChannelInvite(channel, creator.Id, rc, sharedchannel.WithDirectParticipantID(creator.Id), sharedchannel.WithDirectParticipantID(participant.Id))
}
func getUserFromEvent(ps *PlatformService, event *model.WebSocketEvent, key string) (*model.User, error) {
userID, ok := event.GetData()[key].(string)
if !ok || userID == "" {
return nil, fmt.Errorf("received websocket message that is eligible for sending an invitation but message does not have `%s` present", key)
}
user, err := ps.Store.User().Get(context.Background(), userID)
if err != nil {
return nil, errors.Wrap(err, "couldn't find user for creating shared channel invitation for a DM")
}
return user, nil
}
func findChannel(server *PlatformService, channelId string) (*model.Channel, error) {
channel, err := server.Store.Channel().Get(channelId, true)
if err != nil {
return nil, errors.Wrap(err, "received websocket message that is eligible for shared channel sync but channel does not exist")
}
return channel, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/sharedchannel"
)
// SharedChannelServiceIFace is the interface to the shared channel service
type SharedChannelServiceIFace interface {
Shutdown() error
Start() error
NotifyChannelChanged(channelId string)
NotifyUserProfileChanged(userID string)
SendChannelInvite(channel *model.Channel, userId string, rc *model.RemoteCluster, options ...sharedchannel.InviteOption) error
Active() bool
}
type MockOptionSharedChannelService func(service *mockSharedChannelService)
func MockOptionSharedChannelServiceWithActive(active bool) MockOptionSharedChannelService {
return func(mrcs *mockSharedChannelService) {
mrcs.active = active
}
}
func NewMockSharedChannelService(service SharedChannelServiceIFace, options ...MockOptionSharedChannelService) *mockSharedChannelService {
mrcs := &mockSharedChannelService{service, true, []string{}, []string{}, 0}
for _, option := range options {
option(mrcs)
}
return mrcs
}
type mockSharedChannelService struct {
SharedChannelServiceIFace
active bool
channelNotifications []string
userProfileNotifications []string
numInvitations int
}
func (mrcs *mockSharedChannelService) NotifyChannelChanged(channelId string) {
mrcs.channelNotifications = append(mrcs.channelNotifications, channelId)
}
func (mrcs *mockSharedChannelService) NotifyUserProfileChanged(userId string) {
mrcs.userProfileNotifications = append(mrcs.userProfileNotifications, userId)
}
func (mrcs *mockSharedChannelService) Shutdown() error {
return nil
}
func (mrcs *mockSharedChannelService) Start() error {
return nil
}
func (mrcs *mockSharedChannelService) Active() bool {
return mrcs.active
}
func (mrcs *mockSharedChannelService) SendChannelInvite(channel *model.Channel, userId string, rc *model.RemoteCluster, options ...sharedchannel.InviteOption) error {
mrcs.numInvitations += 1
return nil
}
func (mrcs *mockSharedChannelService) NumInvitations() int {
return mrcs.numInvitations
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"encoding/json"
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (ps *PlatformService) AddStatusCacheSkipClusterSend(status *model.Status) {
ps.statusCache.Set(status.UserId, status)
}
func (ps *PlatformService) AddStatusCache(status *model.Status) {
ps.AddStatusCacheSkipClusterSend(status)
if ps.Cluster() != nil {
statusJSON, err := json.Marshal(status)
if err != nil {
ps.logger.Warn("Failed to encode status to JSON", mlog.Err(err))
}
msg := &model.ClusterMessage{
Event: model.ClusterEventUpdateStatus,
SendType: model.ClusterSendBestEffort,
Data: statusJSON,
}
ps.Cluster().SendClusterMessage(msg)
}
}
func (ps *PlatformService) GetAllStatuses() map[string]*model.Status {
if !*ps.Config().ServiceSettings.EnableUserStatuses {
return map[string]*model.Status{}
}
statusMap := map[string]*model.Status{}
if userIDs, err := ps.statusCache.Keys(); err == nil {
for _, userID := range userIDs {
status := ps.GetStatusFromCache(userID)
if status != nil {
statusMap[userID] = status
}
}
}
return statusMap
}
func (ps *PlatformService) GetStatusesByIds(userIDs []string) (map[string]any, *model.AppError) {
if !*ps.Config().ServiceSettings.EnableUserStatuses {
return map[string]any{}, nil
}
statusMap := map[string]any{}
metrics := ps.Metrics()
missingUserIds := []string{}
for _, userID := range userIDs {
var status *model.Status
if err := ps.statusCache.Get(userID, &status); err == nil {
statusMap[userID] = status.Status
if metrics != nil {
metrics.IncrementMemCacheHitCounter("Status")
}
} else {
missingUserIds = append(missingUserIds, userID)
if metrics != nil {
metrics.IncrementMemCacheMissCounter("Status")
}
}
}
if len(missingUserIds) > 0 {
statuses, err := ps.Store.Status().GetByIds(missingUserIds)
if err != nil {
return nil, model.NewAppError("GetStatusesByIds", "app.status.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, s := range statuses {
ps.AddStatusCacheSkipClusterSend(s)
statusMap[s.UserId] = s.Status
}
}
// For the case where the user does not have a row in the Status table and cache
for _, userID := range missingUserIds {
if _, ok := statusMap[userID]; !ok {
statusMap[userID] = model.StatusOffline
}
}
return statusMap, nil
}
// GetUserStatusesByIds used by apiV4
func (ps *PlatformService) GetUserStatusesByIds(userIDs []string) ([]*model.Status, *model.AppError) {
if !*ps.Config().ServiceSettings.EnableUserStatuses {
return []*model.Status{}, nil
}
var statusMap []*model.Status
metrics := ps.Metrics()
missingUserIds := []string{}
for _, userID := range userIDs {
var status *model.Status
if err := ps.statusCache.Get(userID, &status); err == nil {
statusMap = append(statusMap, status)
if metrics != nil {
metrics.IncrementMemCacheHitCounter("Status")
}
} else {
missingUserIds = append(missingUserIds, userID)
if metrics != nil {
metrics.IncrementMemCacheMissCounter("Status")
}
}
}
if len(missingUserIds) > 0 {
statuses, err := ps.Store.Status().GetByIds(missingUserIds)
if err != nil {
return nil, model.NewAppError("GetUserStatusesByIds", "app.status.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, s := range statuses {
ps.AddStatusCacheSkipClusterSend(s)
}
statusMap = append(statusMap, statuses...)
}
// For the case where the user does not have a row in the Status table and cache
// remove the existing ids from missingUserIds and then create a offline state for the missing ones
// This also return the status offline for the non-existing Ids in the system
for i := 0; i < len(missingUserIds); i++ {
missingUserId := missingUserIds[i]
for _, userMap := range statusMap {
if missingUserId == userMap.UserId {
missingUserIds = append(missingUserIds[:i], missingUserIds[i+1:]...)
i--
break
}
}
}
for _, userID := range missingUserIds {
statusMap = append(statusMap, &model.Status{UserId: userID, Status: "offline"})
}
return statusMap, nil
}
func (ps *PlatformService) BroadcastStatus(status *model.Status) {
if ps.Busy.IsBusy() {
// this is considered a non-critical service and will be disabled when server busy.
return
}
event := model.NewWebSocketEvent(model.WebsocketEventStatusChange, "", "", status.UserId, nil, "")
event.Add("status", status.Status)
event.Add("user_id", status.UserId)
ps.Publish(event)
}
func (ps *PlatformService) SaveAndBroadcastStatus(status *model.Status) {
ps.AddStatusCache(status)
if err := ps.Store.Status().SaveOrUpdate(status); err != nil {
mlog.Warn("Failed to save status", mlog.String("user_id", status.UserId), mlog.Err(err))
}
ps.BroadcastStatus(status)
}
func (ps *PlatformService) GetStatusFromCache(userID string) *model.Status {
var status *model.Status
if err := ps.statusCache.Get(userID, &status); err == nil {
statusCopy := &model.Status{}
*statusCopy = *status
return statusCopy
}
return nil
}
func (ps *PlatformService) GetStatus(userID string) (*model.Status, *model.AppError) {
if !*ps.Config().ServiceSettings.EnableUserStatuses {
return &model.Status{}, nil
}
status := ps.GetStatusFromCache(userID)
if status != nil {
return status, nil
}
status, err := ps.Store.Status().Get(userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetStatus", "app.status.get.missing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetStatus", "app.status.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return status, nil
}
// SetStatusLastActivityAt sets the last activity at for a user on the local app server and updates
// status to away if needed. Used by the WS to set status to away if an 'online' device disconnects
// while an 'away' device is still connected
func (ps *PlatformService) SetStatusLastActivityAt(userID string, activityAt int64) {
var status *model.Status
var err *model.AppError
if status, err = ps.GetStatus(userID); err != nil {
return
}
status.LastActivityAt = activityAt
ps.AddStatusCacheSkipClusterSend(status)
ps.SetStatusAwayIfNeeded(userID, false)
}
func (ps *PlatformService) UpdateLastActivityAtIfNeeded(session model.Session) {
now := model.GetMillis()
ps.UpdateWebConnUserActivity(session, now)
if now-session.LastActivityAt < model.SessionActivityTimeout {
return
}
if err := ps.Store.Session().UpdateLastActivityAt(session.Id, now); err != nil {
mlog.Warn("Failed to update LastActivityAt", mlog.String("user_id", session.UserId), mlog.String("session_id", session.Id), mlog.Err(err))
}
session.LastActivityAt = now
ps.AddSessionToCache(&session)
}
func (ps *PlatformService) SetStatusOnline(userID string, manual bool) {
if !*ps.Config().ServiceSettings.EnableUserStatuses {
return
}
broadcast := false
var oldStatus string = model.StatusOffline
var oldTime int64
var oldManual bool
var status *model.Status
var err *model.AppError
if status, err = ps.GetStatus(userID); err != nil {
status = &model.Status{UserId: userID, Status: model.StatusOnline, Manual: false, LastActivityAt: model.GetMillis(), ActiveChannel: ""}
broadcast = true
} else {
if status.Manual && !manual {
return // manually set status always overrides non-manual one
}
if status.Status != model.StatusOnline {
broadcast = true
}
oldStatus = status.Status
oldTime = status.LastActivityAt
oldManual = status.Manual
status.Status = model.StatusOnline
status.Manual = false // for "online" there's no manual setting
status.LastActivityAt = model.GetMillis()
}
ps.AddStatusCache(status)
// Only update the database if the status has changed, the status has been manually set,
// or enough time has passed since the previous action
if status.Status != oldStatus || status.Manual != oldManual || status.LastActivityAt-oldTime > model.StatusMinUpdateTime {
if broadcast {
if err := ps.Store.Status().SaveOrUpdate(status); err != nil {
mlog.Warn("Failed to save status", mlog.String("user_id", userID), mlog.Err(err), mlog.String("user_id", userID))
}
} else {
if err := ps.Store.Status().UpdateLastActivityAt(status.UserId, status.LastActivityAt); err != nil {
mlog.Error("Failed to save status", mlog.String("user_id", userID), mlog.Err(err), mlog.String("user_id", userID))
}
}
}
if broadcast {
ps.BroadcastStatus(status)
}
}
func (ps *PlatformService) SetStatusOffline(userID string, manual bool) {
if !*ps.Config().ServiceSettings.EnableUserStatuses {
return
}
status, err := ps.GetStatus(userID)
if err == nil && status.Manual && !manual {
return // manually set status always overrides non-manual one
}
status = &model.Status{UserId: userID, Status: model.StatusOffline, Manual: manual, LastActivityAt: model.GetMillis(), ActiveChannel: ""}
ps.SaveAndBroadcastStatus(status)
}
func (ps *PlatformService) SetStatusAwayIfNeeded(userID string, manual bool) {
if !*ps.Config().ServiceSettings.EnableUserStatuses {
return
}
status, err := ps.GetStatus(userID)
if err != nil {
status = &model.Status{UserId: userID, Status: model.StatusOffline, Manual: manual, LastActivityAt: 0, ActiveChannel: ""}
}
if !manual && status.Manual {
return // manually set status always overrides non-manual one
}
if !manual {
if status.Status == model.StatusAway {
return
}
if !ps.isUserAway(status.LastActivityAt) {
return
}
}
status.Status = model.StatusAway
status.Manual = manual
status.ActiveChannel = ""
ps.SaveAndBroadcastStatus(status)
}
// SetStatusDoNotDisturbTimed takes endtime in unix epoch format in UTC
// and sets status of given userId to dnd which will be restored back after endtime
func (ps *PlatformService) SetStatusDoNotDisturbTimed(userId string, endtime int64) {
if !*ps.Config().ServiceSettings.EnableUserStatuses {
return
}
status, err := ps.GetStatus(userId)
if err != nil {
status = &model.Status{UserId: userId, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
}
status.PrevStatus = status.Status
status.Status = model.StatusDnd
status.Manual = true
status.DNDEndTime = endtime
ps.SaveAndBroadcastStatus(status)
}
func (ps *PlatformService) SetStatusDoNotDisturb(userID string) {
if !*ps.Config().ServiceSettings.EnableUserStatuses {
return
}
status, err := ps.GetStatus(userID)
if err != nil {
status = &model.Status{UserId: userID, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
}
status.Status = model.StatusDnd
status.Manual = true
ps.SaveAndBroadcastStatus(status)
}
func (ps *PlatformService) SetStatusOutOfOffice(userID string) {
if !*ps.Config().ServiceSettings.EnableUserStatuses {
return
}
status, err := ps.GetStatus(userID)
if err != nil {
status = &model.Status{UserId: userID, Status: model.StatusOutOfOffice, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
}
status.Status = model.StatusOutOfOffice
status.Manual = true
ps.SaveAndBroadcastStatus(status)
}
func (ps *PlatformService) isUserAway(lastActivityAt int64) bool {
return model.GetMillis()-lastActivityAt >= *ps.Config().TeamSettings.UserStatusAwayTimeout*1000
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"crypto/sha256"
"encoding/base64"
)
func getKeyHash(key string) string {
hash := sha256.New()
hash.Write([]byte(key))
return base64.StdEncoding.EncodeToString(hash.Sum(nil))
}
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/gorilla/websocket"
"github.com/vmihailenco/msgpack/v5"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
sendQueueSize = 256
sendSlowWarn = (sendQueueSize * 50) / 100
sendFullWarn = (sendQueueSize * 95) / 100
writeWaitTime = 30 * time.Second
pongWaitTime = 100 * time.Second
pingInterval = (pongWaitTime * 6) / 10
authCheckInterval = 5 * time.Second
webConnMemberCacheTime = 1000 * 60 * 30 // 30 minutes
deadQueueSize = 128 // Approximated from /proc/sys/net/core/wmem_default / 2048 (avg msg size)
websocketSuppressWarnThreshold = time.Minute
)
const (
reconnectFound = "success"
reconnectNotFound = "failure"
reconnectLossless = "lossless"
)
const websocketMessagePluginPrefix = "custom_"
type pluginWSPostedHook struct {
connectionID string
userID string
req *model.WebSocketRequest
}
type WebConnConfig struct {
WebSocket *websocket.Conn
Session model.Session
TFunc i18n.TranslateFunc
Locale string
ConnectionID string
Active bool
ReuseCount int
// These aren't necessary to be exported to api layer.
sequence int
activeQueue chan model.WebSocketMessage
deadQueue []*model.WebSocketEvent
deadQueuePointer int
}
// WebConn represents a single websocket connection to a user.
// It contains all the necessary state to manage sending/receiving data to/from
// a websocket.
type WebConn struct {
sessionExpiresAt int64 // This should stay at the top for 64-bit alignment of 64-bit words accessed atomically
Platform *PlatformService
Suite SuiteIFace
HookRunner HookRunner
WebSocket *websocket.Conn
T i18n.TranslateFunc
Locale string
Sequence int64
UserId string
allChannelMembers map[string]string
lastAllChannelMembersTime int64
lastUserActivityAt int64
send chan model.WebSocketMessage
// deadQueue behaves like a queue of a finite size
// which is used to store all messages that are sent via the websocket.
// It basically acts as the user-space socket buffer, and is used
// to resuscitate any messages that might have got lost when the connection is broken.
// It is implemented by using a circular buffer to keep it fast.
deadQueue []*model.WebSocketEvent
// Pointer which indicates the next slot to insert.
// It is only to be incremented during writing or clearing the queue.
deadQueuePointer int
// active indicates whether there is an open websocket connection attached
// to this webConn or not.
// It is not used as an atomic, because there is no need to.
// So do not use this outside the web hub.
active bool
// reuseCount indicates how many times this connection has been reused.
// This is used to differentiate between a fresh connection and
// a reused connection.
// It's theoretically possible for this number to wrap around. But we
// leave that as an edge-case.
reuseCount int
sessionToken atomic.Value
session atomic.Value
connectionID atomic.Value
endWritePump chan struct{}
pumpFinished chan struct{}
pluginPosted chan pluginWSPostedHook
// These counters are to suppress spammy websocket.slow
// and websocket.full logs which happen continuously, if they
// do happen. To improve the situation, we log them only once
// per minute.
lastLogTimeSlow time.Time
lastLogTimeFull time.Time
}
// CheckConnResult indicates whether a connectionID was present in the hub or not.
// And if so, contains the active and dead queue details.
type CheckConnResult struct {
ConnectionID string
UserID string
ActiveQueue chan model.WebSocketMessage
DeadQueue []*model.WebSocketEvent
DeadQueuePointer int
ReuseCount int
}
// PopulateWebConnConfig checks if the connection id already exists in the hub,
// and if so, accordingly populates the other fields of the webconn.
func (ps *PlatformService) PopulateWebConnConfig(s *model.Session, cfg *WebConnConfig, seqVal string) (*WebConnConfig, error) {
if !model.IsValidId(cfg.ConnectionID) {
return nil, fmt.Errorf("invalid connection id: %s", cfg.ConnectionID)
}
// This does not handle reconnect requests across nodes in a cluster.
// It falls back to the non-reliable case in that scenario.
res := ps.CheckWebConn(s.UserId, cfg.ConnectionID)
if res == nil {
// If the connection is not present, then we assume either timeout,
// or server restart. In that case, we set a new one.
cfg.ConnectionID = model.NewId()
} else {
// Connection is present, we get the active queue, dead queue
cfg.activeQueue = res.ActiveQueue
cfg.deadQueue = res.DeadQueue
cfg.deadQueuePointer = res.DeadQueuePointer
cfg.Active = false
cfg.ReuseCount = res.ReuseCount
// Now we get the sequence number
if seqVal == "" {
// Sequence_number must be sent with connection id.
// A client must be either non-compliant or fully compliant.
return nil, errors.New("sequence number not present in websocket request")
}
var err error
cfg.sequence, err = strconv.Atoi(seqVal)
if err != nil || cfg.sequence < 0 {
return nil, fmt.Errorf("invalid sequence number %s in query param: %v", seqVal, err)
}
}
return cfg, nil
}
// NewWebConn returns a new WebConn instance.
func (ps *PlatformService) NewWebConn(cfg *WebConnConfig, suite SuiteIFace, runner HookRunner) *WebConn {
if cfg.Session.UserId != "" {
ps.Go(func() {
ps.SetStatusOnline(cfg.Session.UserId, false)
ps.UpdateLastActivityAtIfNeeded(cfg.Session)
})
}
// Disable TCP_NO_DELAY for higher throughput
var tcpConn *net.TCPConn
switch conn := cfg.WebSocket.UnderlyingConn().(type) {
case *net.TCPConn:
tcpConn = conn
case *tls.Conn:
newConn, ok := conn.NetConn().(*net.TCPConn)
if ok {
tcpConn = newConn
}
}
if tcpConn != nil {
err := tcpConn.SetNoDelay(false)
if err != nil {
mlog.Warn("Error in setting NoDelay socket opts", mlog.Err(err))
}
}
if cfg.activeQueue == nil {
cfg.activeQueue = make(chan model.WebSocketMessage, sendQueueSize)
}
if cfg.deadQueue == nil {
cfg.deadQueue = make([]*model.WebSocketEvent, deadQueueSize)
}
wc := &WebConn{
Platform: ps,
Suite: suite,
HookRunner: runner,
send: cfg.activeQueue,
deadQueue: cfg.deadQueue,
deadQueuePointer: cfg.deadQueuePointer,
Sequence: int64(cfg.sequence),
WebSocket: cfg.WebSocket,
lastUserActivityAt: model.GetMillis(),
UserId: cfg.Session.UserId,
T: cfg.TFunc,
Locale: cfg.Locale,
active: cfg.Active,
reuseCount: cfg.ReuseCount,
endWritePump: make(chan struct{}),
pumpFinished: make(chan struct{}),
pluginPosted: make(chan pluginWSPostedHook, 10),
lastLogTimeSlow: time.Now(),
lastLogTimeFull: time.Now(),
}
wc.SetSession(&cfg.Session)
wc.SetSessionToken(cfg.Session.Token)
wc.SetSessionExpiresAt(cfg.Session.ExpiresAt)
wc.SetConnectionID(cfg.ConnectionID)
wc.Platform.Go(func() {
wc.HookRunner.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.OnWebSocketConnect(wc.GetConnectionID(), wc.UserId)
return true
}, plugin.OnWebSocketConnectID)
})
return wc
}
func (wc *WebConn) pluginPostedConsumer(wg *sync.WaitGroup) {
defer wg.Done()
for msg := range wc.pluginPosted {
wc.HookRunner.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.WebSocketMessageHasBeenPosted(msg.connectionID, msg.userID, msg.req)
return true
}, plugin.WebSocketMessageHasBeenPostedID)
}
}
// Close closes the WebConn.
func (wc *WebConn) Close() {
wc.WebSocket.Close()
<-wc.pumpFinished
}
// GetSessionExpiresAt returns the time at which the session expires.
func (wc *WebConn) GetSessionExpiresAt() int64 {
return atomic.LoadInt64(&wc.sessionExpiresAt)
}
// SetSessionExpiresAt sets the time at which the session expires.
func (wc *WebConn) SetSessionExpiresAt(v int64) {
atomic.StoreInt64(&wc.sessionExpiresAt, v)
}
// GetSessionToken returns the session token of the connection.
func (wc *WebConn) GetSessionToken() string {
return wc.sessionToken.Load().(string)
}
// SetSessionToken sets the session token of the connection.
func (wc *WebConn) SetSessionToken(v string) {
wc.sessionToken.Store(v)
}
// SetConnectionID sets the connection id of the connection.
func (wc *WebConn) SetConnectionID(id string) {
wc.connectionID.Store(id)
}
// GetConnectionID returns the connection id of the connection.
func (wc *WebConn) GetConnectionID() string {
return wc.connectionID.Load().(string)
}
// areAllInactive returns whether all of the connections
// are inactive or not.
func areAllInactive(conns []*WebConn) bool {
for _, conn := range conns {
if conn.active {
return false
}
}
return true
}
// GetSession returns the session of the connection.
func (wc *WebConn) GetSession() *model.Session {
return wc.session.Load().(*model.Session)
}
// SetSession sets the session of the connection.
func (wc *WebConn) SetSession(v *model.Session) {
if v != nil {
v = v.DeepCopy()
}
wc.session.Store(v)
}
// Pump starts the WebConn instance. After this, the websocket
// is ready to send/receive messages.
func (wc *WebConn) Pump() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
wc.writePump()
}()
wg.Add(1)
go wc.pluginPostedConsumer(&wg)
wc.readPump()
close(wc.endWritePump)
close(wc.pluginPosted)
wg.Wait()
wc.Platform.HubUnregister(wc)
close(wc.pumpFinished)
wc.Platform.Go(func() {
wc.HookRunner.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.OnWebSocketDisconnect(wc.GetConnectionID(), wc.UserId)
return true
}, plugin.OnWebSocketDisconnectID)
})
}
func (wc *WebConn) readPump() {
defer func() {
wc.WebSocket.Close()
}()
wc.WebSocket.SetReadLimit(model.SocketMaxMessageSizeKb)
wc.WebSocket.SetReadDeadline(time.Now().Add(pongWaitTime))
wc.WebSocket.SetPongHandler(func(string) error {
if err := wc.WebSocket.SetReadDeadline(time.Now().Add(pongWaitTime)); err != nil {
return err
}
if wc.IsAuthenticated() {
wc.Platform.Go(func() {
wc.Platform.SetStatusAwayIfNeeded(wc.UserId, false)
})
}
return nil
})
for {
msgType, rd, err := wc.WebSocket.NextReader()
if err != nil {
wc.logSocketErr("websocket.NextReader", err)
return
}
var decoder interface {
Decode(v any) error
}
if msgType == websocket.TextMessage {
decoder = json.NewDecoder(rd)
} else {
decoder = msgpack.NewDecoder(rd)
}
var req model.WebSocketRequest
if err = decoder.Decode(&req); err != nil {
wc.logSocketErr("websocket.Decode", err)
return
}
// Messages which actions are prefixed with the plugin prefix
// should only be dispatched to the plugins
if !strings.HasPrefix(req.Action, websocketMessagePluginPrefix) {
wc.Platform.WebSocketRouter.ServeWebSocket(wc, &req)
}
clonedReq, err := req.Clone()
if err != nil {
wc.logSocketErr("websocket.cloneRequest", err)
continue
}
wc.pluginPosted <- pluginWSPostedHook{wc.GetConnectionID(), wc.UserId, clonedReq}
}
}
func (wc *WebConn) writePump() {
ticker := time.NewTicker(pingInterval)
authTicker := time.NewTicker(authCheckInterval)
defer func() {
ticker.Stop()
authTicker.Stop()
wc.WebSocket.Close()
}()
if wc.Sequence != 0 {
if ok, index := wc.isInDeadQueue(wc.Sequence); ok {
if err := wc.drainDeadQueue(index); err != nil {
wc.logSocketErr("websocket.drainDeadQueue", err)
return
}
if m := wc.Platform.metricsIFace; m != nil {
m.IncrementWebsocketReconnectEvent(reconnectFound)
}
} else if wc.hasMsgLoss() {
// If the seq number is not in dead queue, but it was supposed to be,
// then generate a different connection ID,
// and set sequence to 0, and clear dead queue.
wc.clearDeadQueue()
wc.SetConnectionID(model.NewId())
wc.Sequence = 0
// Send hello message
msg := wc.createHelloMessage()
wc.addToDeadQueue(msg)
if err := wc.writeMessage(msg); err != nil {
wc.logSocketErr("websocket.sendHello", err)
return
}
if m := wc.Platform.metricsIFace; m != nil {
m.IncrementWebsocketReconnectEvent(reconnectNotFound)
}
} else {
if m := wc.Platform.metricsIFace; m != nil {
m.IncrementWebsocketReconnectEvent(reconnectLossless)
}
}
}
var buf bytes.Buffer
// 2k is seen to be a good heuristic under which 98.5% of message sizes remain.
buf.Grow(1024 * 2)
enc := json.NewEncoder(&buf)
for {
select {
case msg, ok := <-wc.send:
if !ok {
wc.writeMessageBuf(websocket.CloseMessage, []byte{})
return
}
evt, evtOk := msg.(*model.WebSocketEvent)
buf.Reset()
var err error
if evtOk {
evt = evt.SetSequence(wc.Sequence)
err = evt.Encode(enc)
wc.Sequence++
} else {
err = enc.Encode(msg)
}
if err != nil {
mlog.Warn("Error in encoding websocket message", mlog.Err(err))
continue
}
if len(wc.send) >= sendFullWarn && time.Since(wc.lastLogTimeFull) > websocketSuppressWarnThreshold {
logData := []mlog.Field{
mlog.String("user_id", wc.UserId),
mlog.String("type", msg.EventType()),
mlog.Int("size", buf.Len()),
}
if evtOk {
logData = append(logData, mlog.String("channel_id", evt.GetBroadcast().ChannelId))
}
mlog.Warn("websocket.full", logData...)
wc.lastLogTimeFull = time.Now()
}
if evtOk {
wc.addToDeadQueue(evt)
}
if err := wc.writeMessageBuf(websocket.TextMessage, buf.Bytes()); err != nil {
wc.logSocketErr("websocket.send", err)
return
}
if m := wc.Platform.metricsIFace; m != nil {
m.IncrementWebSocketBroadcast(msg.EventType())
}
case <-ticker.C:
if err := wc.writeMessageBuf(websocket.PingMessage, []byte{}); err != nil {
wc.logSocketErr("websocket.ticker", err)
return
}
case <-wc.endWritePump:
return
case <-authTicker.C:
if wc.GetSessionToken() == "" {
mlog.Debug("websocket.authTicker: did not authenticate", mlog.Any("ip_address", wc.WebSocket.RemoteAddr()))
return
}
authTicker.Stop()
}
}
}
// writeMessageBuf is a helper utility that wraps the write to the socket
// along with setting the write deadline.
func (wc *WebConn) writeMessageBuf(msgType int, data []byte) error {
wc.WebSocket.SetWriteDeadline(time.Now().Add(writeWaitTime))
return wc.WebSocket.WriteMessage(msgType, data)
}
func (wc *WebConn) writeMessage(msg *model.WebSocketEvent) error {
// We don't use the encoder from the write pump because it's unwieldy to pass encoders
// around, and this is only called during initialization of the webConn.
var buf bytes.Buffer
err := msg.Encode(json.NewEncoder(&buf))
if err != nil {
mlog.Warn("Error in encoding websocket message", mlog.Err(err))
return nil
}
wc.Sequence++
return wc.writeMessageBuf(websocket.TextMessage, buf.Bytes())
}
// addToDeadQueue appends a message to the dead queue.
func (wc *WebConn) addToDeadQueue(msg *model.WebSocketEvent) {
wc.deadQueue[wc.deadQueuePointer] = msg
wc.deadQueuePointer = (wc.deadQueuePointer + 1) % deadQueueSize
}
// hasMsgLoss indicates whether the next wanted sequence is right after
// the latest element in the dead queue, which would mean there is no message loss.
func (wc *WebConn) hasMsgLoss() bool {
var index int
// deadQueuePointer = 0 means either no msg written or the pointer
// has rolled over to its starting position.
if wc.deadQueuePointer == 0 {
// If last entry is nil, it means no msg is written.
if wc.deadQueue[deadQueueSize-1] == nil {
return false
}
// If it's not nil, that means it has rolled over to start, and we
// check the last position.
index = deadQueueSize - 1
} else { // deadQueuePointer != 0 means it's somewhere in the middle.
index = wc.deadQueuePointer - 1
}
if wc.deadQueue[index].GetSequence() == wc.Sequence-1 {
return false
}
return true
}
// isInDeadQueue checks whether a given sequence number is in the dead queue or not.
// And if it is, it returns that index.
func (wc *WebConn) isInDeadQueue(seq int64) (bool, int) {
// Can be optimized to traverse backwards from deadQueuePointer
// Hopefully, traversing 128 elements is not too much overhead.
for i := 0; i < deadQueueSize; i++ {
elem := wc.deadQueue[i]
if elem == nil {
return false, 0
}
if elem.GetSequence() == seq {
return true, i
}
}
return false, 0
}
func (wc *WebConn) clearDeadQueue() {
for i := 0; i < deadQueueSize; i++ {
if wc.deadQueue[i] == nil {
break
}
wc.deadQueue[i] = nil
}
wc.deadQueuePointer = 0
}
// drainDeadQueue will write all messages from a given index to the socket.
// It is called with the assumption that the item with wc.Sequence is present
// in it, because otherwise it would have been cleared from WebConn.
func (wc *WebConn) drainDeadQueue(index int) error {
if wc.deadQueue[0] == nil {
// Empty queue
return nil
}
// This means pointer hasn't rolled over.
if wc.deadQueue[wc.deadQueuePointer] == nil {
// Clear till the end of queue.
for i := index; i < wc.deadQueuePointer; i++ {
if err := wc.writeMessage(wc.deadQueue[i]); err != nil {
return err
}
}
return nil
}
// We go on until next sequence number is smaller than previous one.
// Which means it has rolled over.
currPtr := index
for {
if err := wc.writeMessage(wc.deadQueue[currPtr]); err != nil {
return err
}
oldSeq := wc.deadQueue[currPtr].GetSequence() // TODO: possibly move this
currPtr = (currPtr + 1) % deadQueueSize // to for loop condition
newSeq := wc.deadQueue[currPtr].GetSequence()
if oldSeq > newSeq {
break
}
}
return nil
}
// InvalidateCache resets all internal data of the WebConn.
func (wc *WebConn) InvalidateCache() {
wc.allChannelMembers = nil
wc.lastAllChannelMembersTime = 0
wc.SetSession(nil)
wc.SetSessionExpiresAt(0)
}
// IsAuthenticated returns whether the given WebConn is authenticated or not.
func (wc *WebConn) IsAuthenticated() bool {
// Check the expiry to see if we need to check for a new session
if wc.GetSessionExpiresAt() < model.GetMillis() {
if wc.GetSessionToken() == "" {
return false
}
session, err := wc.Suite.GetSession(wc.GetSessionToken())
if err != nil {
if err.StatusCode >= http.StatusBadRequest && err.StatusCode < http.StatusInternalServerError {
mlog.Debug("Invalid session.", mlog.Err(err))
} else {
mlog.Error("Could not get session", mlog.String("session_token", wc.GetSessionToken()), mlog.Err(err))
}
wc.SetSessionToken("")
wc.SetSession(nil)
wc.SetSessionExpiresAt(0)
return false
}
wc.SetSession(session)
wc.SetSessionExpiresAt(session.ExpiresAt)
}
return true
}
func (wc *WebConn) createHelloMessage() *model.WebSocketEvent {
ee := wc.Platform.LicenseManager() != nil
msg := model.NewWebSocketEvent(model.WebsocketEventHello, "", "", wc.UserId, nil, "")
msg.Add("server_version", fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion,
model.BuildNumber,
wc.Platform.ClientConfigHash(),
ee))
msg.Add("connection_id", wc.connectionID.Load())
return msg
}
func (wc *WebConn) ShouldSendEventToGuest(msg *model.WebSocketEvent) bool {
var userID string
var canSee bool
switch msg.EventType() {
case model.WebsocketEventUserUpdated:
user, ok := msg.GetData()["user"].(*model.User)
if !ok {
mlog.Debug("webhub.shouldSendEvent: user not found in message", mlog.Any("user", msg.GetData()["user"]))
return false
}
userID = user.Id
case model.WebsocketEventNewUser:
userID = msg.GetData()["user_id"].(string)
default:
return true
}
canSee, err := wc.Suite.UserCanSeeOtherUser(wc.UserId, userID)
if err != nil {
mlog.Error("webhub.shouldSendEvent.", mlog.Err(err))
return false
}
return canSee
}
// ShouldSendEvent returns whether the message should be sent or not.
func (wc *WebConn) ShouldSendEvent(msg *model.WebSocketEvent) bool {
// IMPORTANT: Do not send event if WebConn does not have a session
if !wc.IsAuthenticated() {
return false
}
// When the pump starts to get slow we'll drop non-critical
// messages. We should skip those frames before they are
// queued to wc.send buffered channel.
if len(wc.send) >= sendSlowWarn {
switch msg.EventType() {
case model.WebsocketEventTyping,
model.WebsocketEventStatusChange,
model.WebsocketEventChannelViewed:
if time.Since(wc.lastLogTimeSlow) > websocketSuppressWarnThreshold {
mlog.Warn(
"websocket.slow: dropping message",
mlog.String("user_id", wc.UserId),
mlog.String("type", msg.EventType()),
)
// Reset timer to now.
wc.lastLogTimeSlow = time.Now()
}
return false
}
}
// If the event contains sanitized data, only send to users that don't have permission to
// see sensitive data. Prevents admin clients from receiving events with bad data
var hasReadPrivateDataPermission *bool
if msg.GetBroadcast().ContainsSanitizedData {
hasReadPrivateDataPermission = model.NewBool(wc.Suite.RolesGrantPermission(wc.GetSession().GetUserRoles(), model.PermissionManageSystem.Id))
if *hasReadPrivateDataPermission {
return false
}
}
// If the event contains sensitive data, only send to users with permission to see it
if msg.GetBroadcast().ContainsSensitiveData {
if hasReadPrivateDataPermission == nil {
hasReadPrivateDataPermission = model.NewBool(wc.Suite.RolesGrantPermission(wc.GetSession().GetUserRoles(), model.PermissionManageSystem.Id))
}
if !*hasReadPrivateDataPermission {
return false
}
}
// If the event is destined to a specific connection
if msg.GetBroadcast().ConnectionId != "" {
return wc.GetConnectionID() == msg.GetBroadcast().ConnectionId
}
if wc.GetConnectionID() == msg.GetBroadcast().OmitConnectionId {
return false
}
// If the event is destined to a specific user
if msg.GetBroadcast().UserId != "" {
return wc.UserId == msg.GetBroadcast().UserId
}
// if the user is omitted don't send the message
if len(msg.GetBroadcast().OmitUsers) > 0 {
if _, ok := msg.GetBroadcast().OmitUsers[wc.UserId]; ok {
return false
}
}
// Only report events to users who are in the channel for the event
if msg.GetBroadcast().ChannelId != "" {
if model.GetMillis()-wc.lastAllChannelMembersTime > webConnMemberCacheTime {
wc.allChannelMembers = nil
wc.lastAllChannelMembersTime = 0
}
if wc.allChannelMembers == nil {
result, err := wc.Platform.Store.Channel().GetAllChannelMembersForUser(wc.UserId, false, false)
if err != nil {
mlog.Error("webhub.shouldSendEvent.", mlog.Err(err))
return false
}
wc.allChannelMembers = result
wc.lastAllChannelMembersTime = model.GetMillis()
}
if _, ok := wc.allChannelMembers[msg.GetBroadcast().ChannelId]; ok {
return true
}
return false
}
// Only report events to users who are in the team for the event
if msg.GetBroadcast().TeamId != "" {
return wc.isMemberOfTeam(msg.GetBroadcast().TeamId)
}
if wc.GetSession().Props[model.SessionPropIsGuest] == "true" {
return wc.ShouldSendEventToGuest(msg)
}
return true
}
// IsMemberOfTeam returns whether the user of the WebConn
// is a member of the given teamID or not.
func (wc *WebConn) isMemberOfTeam(teamID string) bool {
currentSession := wc.GetSession()
if currentSession == nil || currentSession.Token == "" {
session, err := wc.Suite.GetSession(wc.GetSessionToken())
if err != nil {
if err.StatusCode >= http.StatusBadRequest && err.StatusCode < http.StatusInternalServerError {
mlog.Debug("Invalid session.", mlog.Err(err))
} else {
mlog.Error("Could not get session", mlog.String("session_token", wc.GetSessionToken()), mlog.Err(err))
}
return false
}
wc.SetSession(session)
currentSession = session
}
return currentSession.GetTeamByTeamId(teamID) != nil
}
func (wc *WebConn) logSocketErr(source string, err error) {
// browsers will appear as CloseNoStatusReceived
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) {
mlog.Debug(source+": client side closed socket", mlog.String("user_id", wc.UserId))
} else {
mlog.Debug(source+": closing websocket", mlog.String("user_id", wc.UserId), mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"hash/maphash"
"runtime"
"runtime/debug"
"strconv"
"sync/atomic"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
broadcastQueueSize = 4096
inactiveConnReaperInterval = 5 * time.Minute
)
type SuiteIFace interface {
GetSession(token string) (*model.Session, *model.AppError)
RolesGrantPermission(roleNames []string, permissionId string) bool
UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError)
}
type webConnActivityMessage struct {
userID string
sessionToken string
activityAt int64
}
type webConnDirectMessage struct {
conn *WebConn
msg model.WebSocketMessage
}
type webConnSessionMessage struct {
userID string
sessionToken string
isRegistered chan bool
}
type webConnCheckMessage struct {
userID string
connectionID string
result chan *CheckConnResult
}
// Hub is the central place to manage all websocket connections in the server.
// It handles different websocket events and sending messages to individual
// user connections.
type Hub struct {
// connectionCount should be kept first.
// See https://github.com/mattermost/mattermost-server/pull/7281
connectionCount int64
platform *PlatformService
connectionIndex int
register chan *WebConn
unregister chan *WebConn
broadcast chan *model.WebSocketEvent
stop chan struct{}
didStop chan struct{}
invalidateUser chan string
activity chan *webConnActivityMessage
directMsg chan *webConnDirectMessage
explicitStop bool
checkRegistered chan *webConnSessionMessage
checkConn chan *webConnCheckMessage
}
// newWebHub creates a new Hub.
func newWebHub(ps *PlatformService) *Hub {
return &Hub{
platform: ps,
register: make(chan *WebConn),
unregister: make(chan *WebConn),
broadcast: make(chan *model.WebSocketEvent, broadcastQueueSize),
stop: make(chan struct{}),
didStop: make(chan struct{}),
invalidateUser: make(chan string),
activity: make(chan *webConnActivityMessage),
directMsg: make(chan *webConnDirectMessage),
checkRegistered: make(chan *webConnSessionMessage),
checkConn: make(chan *webConnCheckMessage),
}
}
// hubStart starts all the hubs.
func (ps *PlatformService) hubStart() {
// Total number of hubs is twice the number of CPUs.
numberOfHubs := runtime.NumCPU() * 2
ps.logger.Info("Starting websocket hubs", mlog.Int("number_of_hubs", numberOfHubs))
hubs := make([]*Hub, numberOfHubs)
for i := 0; i < numberOfHubs; i++ {
hubs[i] = newWebHub(ps)
hubs[i].connectionIndex = i
hubs[i].Start()
}
// Assigning to the hubs slice without any mutex is fine because it is only assigned once
// during the start of the program and always read from after that.
ps.hubs = hubs
}
func (ps *PlatformService) InvalidateCacheForWebhook(webhookID string) {
ps.Store.Webhook().InvalidateWebhookCache(webhookID)
}
// HubStop stops all the hubs.
func (ps *PlatformService) HubStop() {
ps.logger.Info("stopping websocket hub connections")
for _, hub := range ps.hubs {
hub.Stop()
}
}
// GetHubForUserId returns the hub for a given user id.
func (ps *PlatformService) GetHubForUserId(userID string) *Hub {
if len(ps.hubs) == 0 {
return nil
}
// TODO: check if caching the userID -> hub mapping
// is worth the memory tradeoff.
// https://mattermost.atlassian.net/browse/MM-26629.
var hash maphash.Hash
hash.SetSeed(ps.hashSeed)
hash.Write([]byte(userID))
index := hash.Sum64() % uint64(len(ps.hubs))
return ps.hubs[int(index)]
}
// HubRegister registers a connection to a hub.
func (ps *PlatformService) HubRegister(webConn *WebConn) {
hub := ps.GetHubForUserId(webConn.UserId)
if hub != nil {
if metrics := ps.metricsIFace; metrics != nil {
metrics.IncrementWebSocketBroadcastUsersRegistered(strconv.Itoa(hub.connectionIndex), 1)
}
hub.Register(webConn)
}
}
// HubUnregister unregisters a connection from a hub.
func (ps *PlatformService) HubUnregister(webConn *WebConn) {
hub := ps.GetHubForUserId(webConn.UserId)
if hub != nil {
if metrics := ps.metricsIFace; metrics != nil {
metrics.DecrementWebSocketBroadcastUsersRegistered(strconv.Itoa(hub.connectionIndex), 1)
}
hub.Unregister(webConn)
}
}
func (ps *PlatformService) InvalidateCacheForChannel(channel *model.Channel) {
ps.Store.Channel().InvalidateChannel(channel.Id)
ps.invalidateCacheForChannelByNameSkipClusterSend(channel.TeamId, channel.Name)
if ps.clusterIFace != nil {
nameMsg := &model.ClusterMessage{
Event: model.ClusterEventInvalidateCacheForChannelByName,
SendType: model.ClusterSendBestEffort,
Props: make(map[string]string),
}
nameMsg.Props["name"] = channel.Name
if channel.TeamId == "" {
nameMsg.Props["id"] = "dm"
} else {
nameMsg.Props["id"] = channel.TeamId
}
ps.clusterIFace.SendClusterMessage(nameMsg)
}
}
func (ps *PlatformService) InvalidateCacheForChannelMembers(channelID string) {
ps.Store.User().InvalidateProfilesInChannelCache(channelID)
ps.Store.Channel().InvalidateMemberCount(channelID)
ps.Store.Channel().InvalidateGuestCount(channelID)
}
func (ps *PlatformService) InvalidateCacheForChannelMembersNotifyProps(channelID string) {
ps.invalidateCacheForChannelMembersNotifyPropsSkipClusterSend(channelID)
if ps.clusterIFace != nil {
msg := &model.ClusterMessage{
Event: model.ClusterEventInvalidateCacheForChannelMembersNotifyProps,
SendType: model.ClusterSendBestEffort,
Data: []byte(channelID),
}
ps.clusterIFace.SendClusterMessage(msg)
}
}
func (ps *PlatformService) InvalidateCacheForChannelPosts(channelID string) {
ps.Store.Channel().InvalidatePinnedPostCount(channelID)
ps.Store.Post().InvalidateLastPostTimeCache(channelID)
}
func (ps *PlatformService) InvalidateCacheForUser(userID string) {
ps.InvalidateCacheForUserSkipClusterSend(userID)
ps.Store.User().InvalidateProfilesInChannelCacheByUser(userID)
ps.Store.User().InvalidateProfileCacheForUser(userID)
if ps.clusterIFace != nil {
msg := &model.ClusterMessage{
Event: model.ClusterEventInvalidateCacheForUser,
SendType: model.ClusterSendBestEffort,
Data: []byte(userID),
}
ps.clusterIFace.SendClusterMessage(msg)
}
}
func (ps *PlatformService) InvalidateCacheForUserTeams(userID string) {
ps.invalidateWebConnSessionCacheForUser(userID)
ps.Store.Team().InvalidateAllTeamIdsForUser(userID)
if ps.clusterIFace != nil {
msg := &model.ClusterMessage{
Event: model.ClusterEventInvalidateCacheForUserTeams,
SendType: model.ClusterSendBestEffort,
Data: []byte(userID),
}
ps.clusterIFace.SendClusterMessage(msg)
}
}
// UpdateWebConnUserActivity sets the LastUserActivityAt of the hub for the given session.
func (ps *PlatformService) UpdateWebConnUserActivity(session model.Session, activityAt int64) {
hub := ps.GetHubForUserId(session.UserId)
if hub != nil {
hub.UpdateActivity(session.UserId, session.Token, activityAt)
}
}
// SessionIsRegistered determines if a specific session has been registered
func (ps *PlatformService) SessionIsRegistered(session model.Session) bool {
hub := ps.GetHubForUserId(session.UserId)
if hub != nil {
return hub.IsRegistered(session.UserId, session.Token)
}
return false
}
func (ps *PlatformService) CheckWebConn(userID, connectionID string) *CheckConnResult {
hub := ps.GetHubForUserId(userID)
if hub != nil {
return hub.CheckConn(userID, connectionID)
}
return nil
}
// Register registers a connection to the hub.
func (h *Hub) Register(webConn *WebConn) {
select {
case h.register <- webConn:
case <-h.stop:
}
}
// Unregister unregisters a connection from the hub.
func (h *Hub) Unregister(webConn *WebConn) {
select {
case h.unregister <- webConn:
case <-h.stop:
}
}
// Determines if a user's session is registered a connection from the hub.
func (h *Hub) IsRegistered(userID, sessionToken string) bool {
ws := &webConnSessionMessage{
userID: userID,
sessionToken: sessionToken,
isRegistered: make(chan bool),
}
select {
case h.checkRegistered <- ws:
return <-ws.isRegistered
case <-h.stop:
}
return false
}
func (h *Hub) CheckConn(userID, connectionID string) *CheckConnResult {
req := &webConnCheckMessage{
userID: userID,
connectionID: connectionID,
result: make(chan *CheckConnResult),
}
select {
case h.checkConn <- req:
return <-req.result
case <-h.stop:
}
return nil
}
// Broadcast broadcasts the message to all connections in the hub.
func (h *Hub) Broadcast(message *model.WebSocketEvent) {
// XXX: The hub nil check is because of the way we setup our tests. We call
// `app.NewServer()` which returns a server, but only after that, we call
// `wsapi.Init()` to initialize the hub. But in the `NewServer` call
// itself proceeds to broadcast some messages happily. This needs to be
// fixed once the wsapi cyclic dependency with server/app goes away.
// And possibly, we can look into doing the hub initialization inside
// NewServer itself.
if h != nil && message != nil {
if metrics := h.platform.metricsIFace; metrics != nil {
metrics.IncrementWebSocketBroadcastBufferSize(strconv.Itoa(h.connectionIndex), 1)
}
select {
case h.broadcast <- message:
case <-h.stop:
}
}
}
// InvalidateUser invalidates the cache for the given user.
func (h *Hub) InvalidateUser(userID string) {
select {
case h.invalidateUser <- userID:
case <-h.stop:
}
}
// UpdateActivity sets the LastUserActivityAt field for the connection
// of the user.
func (h *Hub) UpdateActivity(userID, sessionToken string, activityAt int64) {
select {
case h.activity <- &webConnActivityMessage{
userID: userID,
sessionToken: sessionToken,
activityAt: activityAt,
}:
case <-h.stop:
}
}
// SendMessage sends the given message to the given connection.
func (h *Hub) SendMessage(conn *WebConn, msg model.WebSocketMessage) {
select {
case h.directMsg <- &webConnDirectMessage{
conn: conn,
msg: msg,
}:
case <-h.stop:
}
}
// Stop stops the hub.
func (h *Hub) Stop() {
close(h.stop)
<-h.didStop
}
// Start starts the hub.
func (h *Hub) Start() {
var doStart func()
var doRecoverableStart func()
var doRecover func()
doStart = func() {
mlog.Debug("Hub is starting", mlog.Int("index", h.connectionIndex))
ticker := time.NewTicker(inactiveConnReaperInterval)
defer ticker.Stop()
connIndex := newHubConnectionIndex(inactiveConnReaperInterval)
for {
select {
case webSessionMessage := <-h.checkRegistered:
conns := connIndex.ForUser(webSessionMessage.userID)
var isRegistered bool
for _, conn := range conns {
if !conn.active {
continue
}
if conn.GetSessionToken() == webSessionMessage.sessionToken {
isRegistered = true
}
}
webSessionMessage.isRegistered <- isRegistered
case req := <-h.checkConn:
var res *CheckConnResult
conn := connIndex.RemoveInactiveByConnectionID(req.userID, req.connectionID)
if conn != nil {
res = &CheckConnResult{
ConnectionID: req.connectionID,
UserID: req.userID,
ActiveQueue: conn.send,
DeadQueue: conn.deadQueue,
DeadQueuePointer: conn.deadQueuePointer,
ReuseCount: conn.reuseCount + 1,
}
}
req.result <- res
case <-ticker.C:
connIndex.RemoveInactiveConnections()
case webConn := <-h.register:
// Mark the current one as active.
// There is no need to check if it was inactive or not,
// we will anyways need to make it active.
webConn.active = true
connIndex.Add(webConn)
atomic.StoreInt64(&h.connectionCount, int64(connIndex.AllActive()))
if webConn.IsAuthenticated() && webConn.reuseCount == 0 {
// The hello message should only be sent when the reuseCount is 0.
// i.e in server restart, or long timeout, or fresh connection case.
// In case of seq number not found in dead queue, it is handled by
// the webconn write pump.
webConn.send <- webConn.createHelloMessage()
}
case webConn := <-h.unregister:
// If already removed (via queue full), then removing again becomes a noop.
// But if not removed, mark inactive.
webConn.active = false
atomic.StoreInt64(&h.connectionCount, int64(connIndex.AllActive()))
if webConn.UserId == "" {
continue
}
conns := connIndex.ForUser(webConn.UserId)
if len(conns) == 0 || areAllInactive(conns) {
h.platform.Go(func() {
h.platform.SetStatusOffline(webConn.UserId, false)
})
continue
}
var latestActivity int64 = 0
for _, conn := range conns {
if !conn.active {
continue
}
if conn.lastUserActivityAt > latestActivity {
latestActivity = conn.lastUserActivityAt
}
}
if h.platform.isUserAway(latestActivity) {
h.platform.Go(func() {
h.platform.SetStatusLastActivityAt(webConn.UserId, latestActivity)
})
}
case userID := <-h.invalidateUser:
for _, webConn := range connIndex.ForUser(userID) {
webConn.InvalidateCache()
}
case activity := <-h.activity:
for _, webConn := range connIndex.ForUser(activity.userID) {
if !webConn.active {
continue
}
if webConn.GetSessionToken() == activity.sessionToken {
webConn.lastUserActivityAt = activity.activityAt
}
}
case directMsg := <-h.directMsg:
if !connIndex.Has(directMsg.conn) {
continue
}
select {
case directMsg.conn.send <- directMsg.msg:
default:
mlog.Error("webhub.broadcast: cannot send, closing websocket for user", mlog.String("user_id", directMsg.conn.UserId))
close(directMsg.conn.send)
connIndex.Remove(directMsg.conn)
}
case msg := <-h.broadcast:
if metrics := h.platform.metricsIFace; metrics != nil {
metrics.DecrementWebSocketBroadcastBufferSize(strconv.Itoa(h.connectionIndex), 1)
}
msg = msg.PrecomputeJSON()
broadcast := func(webConn *WebConn) {
if !connIndex.Has(webConn) {
return
}
if webConn.ShouldSendEvent(msg) {
select {
case webConn.send <- msg:
default:
mlog.Error("webhub.broadcast: cannot send, closing websocket for user", mlog.String("user_id", webConn.UserId))
close(webConn.send)
connIndex.Remove(webConn)
}
}
}
if connID := msg.GetBroadcast().ConnectionId; connID != "" {
if webConn := connIndex.byConnectionId[connID]; webConn != nil {
broadcast(webConn)
continue
}
} else if msg.GetBroadcast().UserId != "" {
candidates := connIndex.ForUser(msg.GetBroadcast().UserId)
for _, webConn := range candidates {
broadcast(webConn)
}
continue
}
candidates := connIndex.All()
for webConn := range candidates {
broadcast(webConn)
}
case <-h.stop:
for webConn := range connIndex.All() {
webConn.Close()
h.platform.SetStatusOffline(webConn.UserId, false)
}
h.explicitStop = true
close(h.didStop)
return
}
}
}
doRecoverableStart = func() {
defer doRecover()
doStart()
}
doRecover = func() {
if !h.explicitStop {
if r := recover(); r != nil {
mlog.Error("Recovering from Hub panic.", mlog.Any("panic", r))
} else {
mlog.Error("Webhub stopped unexpectedly. Recovering.")
}
mlog.Error(string(debug.Stack()))
go doRecoverableStart()
}
}
go doRecoverableStart()
}
// hubConnectionIndex provides fast addition, removal, and iteration of web connections.
// It requires 3 functionalities which need to be very fast:
// - check if a connection exists or not.
// - get all connections for a given userID.
// - get all connections.
type hubConnectionIndex struct {
// byUserId stores the list of connections for a given userID
byUserId map[string][]*WebConn
// byConnection serves the dual purpose of storing the index of the webconn
// in the value of byUserId map, and also to get all connections.
byConnection map[*WebConn]int
byConnectionId map[string]*WebConn
// staleThreshold is the limit beyond which inactive connections
// will be deleted.
staleThreshold time.Duration
}
func newHubConnectionIndex(interval time.Duration) *hubConnectionIndex {
return &hubConnectionIndex{
byUserId: make(map[string][]*WebConn),
byConnection: make(map[*WebConn]int),
byConnectionId: make(map[string]*WebConn),
staleThreshold: interval,
}
}
func (i *hubConnectionIndex) Add(wc *WebConn) {
i.byUserId[wc.UserId] = append(i.byUserId[wc.UserId], wc)
i.byConnection[wc] = len(i.byUserId[wc.UserId]) - 1
i.byConnectionId[wc.GetConnectionID()] = wc
}
func (i *hubConnectionIndex) Remove(wc *WebConn) {
wc.Platform.ReturnSessionToPool(wc.GetSession())
userConnIndex, ok := i.byConnection[wc]
if !ok {
return
}
// get the conn slice.
userConnections := i.byUserId[wc.UserId]
// get the last connection.
last := userConnections[len(userConnections)-1]
// set the slot that we are trying to remove to be the last connection.
userConnections[userConnIndex] = last
// remove the last connection pointer from slice.
userConnections[len(userConnections)-1] = nil
// remove the last connection from the slice.
i.byUserId[wc.UserId] = userConnections[:len(userConnections)-1]
// set the index of the connection that was moved to the new index.
i.byConnection[last] = userConnIndex
delete(i.byConnection, wc)
delete(i.byConnectionId, wc.GetConnectionID())
}
func (i *hubConnectionIndex) Has(wc *WebConn) bool {
_, ok := i.byConnection[wc]
return ok
}
// ForUser returns all connections for a user ID.
func (i *hubConnectionIndex) ForUser(id string) []*WebConn {
return i.byUserId[id]
}
// All returns the full webConn index.
func (i *hubConnectionIndex) All() map[*WebConn]int {
return i.byConnection
}
// RemoveInactiveByConnectionID removes an inactive connection for the given
// userID and connectionID.
func (i *hubConnectionIndex) RemoveInactiveByConnectionID(userID, connectionID string) *WebConn {
// To handle empty sessions.
if userID == "" {
return nil
}
for _, conn := range i.ForUser(userID) {
if conn.GetConnectionID() == connectionID && !conn.active {
i.Remove(conn)
return conn
}
}
return nil
}
// RemoveInactiveConnections removes all inactive connections whose lastUserActivityAt
// exceeded staleThreshold.
func (i *hubConnectionIndex) RemoveInactiveConnections() {
now := model.GetMillis()
for conn := range i.byConnection {
if !conn.active && now-conn.lastUserActivityAt > i.staleThreshold.Milliseconds() {
i.Remove(conn)
}
}
}
// AllActive returns the number of active connections.
// This is only called during register/unregister so we can take
// a bit of perf hit here.
func (i *hubConnectionIndex) AllActive() int {
cnt := 0
for conn := range i.byConnection {
if conn.active {
cnt++
}
}
return cnt
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type webSocketHandler interface {
ServeWebSocket(*WebConn, *model.WebSocketRequest)
}
type WebSocketRouter struct {
handlers map[string]webSocketHandler
}
func (wr *WebSocketRouter) Handle(action string, handler webSocketHandler) {
wr.handlers[action] = handler
}
func (wr *WebSocketRouter) ServeWebSocket(conn *WebConn, r *model.WebSocketRequest) {
if r.Action == "" {
err := model.NewAppError("ServeWebSocket", "api.web_socket_router.no_action.app_error", nil, "", http.StatusBadRequest)
returnWebSocketError(conn.Platform, conn, r, err)
return
}
if r.Seq <= 0 {
err := model.NewAppError("ServeWebSocket", "api.web_socket_router.bad_seq.app_error", nil, "", http.StatusBadRequest)
returnWebSocketError(conn.Platform, conn, r, err)
return
}
if r.Action == model.WebsocketAuthenticationChallenge {
if conn.GetSessionToken() != "" {
return
}
token, ok := r.Data["token"].(string)
if !ok {
conn.WebSocket.Close()
return
}
session, err := conn.Suite.GetSession(token)
if err != nil {
conn.WebSocket.Close()
return
}
conn.SetSession(session)
conn.SetSessionToken(session.Token)
conn.UserId = session.UserId
conn.Platform.HubRegister(conn)
conn.Platform.Go(func() {
conn.Platform.SetStatusOnline(session.UserId, false)
conn.Platform.UpdateLastActivityAtIfNeeded(*session)
})
resp := model.NewWebSocketResponse(model.StatusOk, r.Seq, nil)
hub := conn.Platform.GetHubForUserId(conn.UserId)
if hub == nil {
return
}
hub.SendMessage(conn, resp)
return
}
if !conn.IsAuthenticated() {
err := model.NewAppError("ServeWebSocket", "api.web_socket_router.not_authenticated.app_error", nil, "", http.StatusUnauthorized)
returnWebSocketError(conn.Platform, conn, r, err)
return
}
handler, ok := wr.handlers[r.Action]
if !ok {
err := model.NewAppError("ServeWebSocket", "api.web_socket_router.bad_action.app_error", nil, "", http.StatusInternalServerError)
returnWebSocketError(conn.Platform, conn, r, err)
return
}
handler.ServeWebSocket(conn, r)
}
func returnWebSocketError(ps *PlatformService, conn *WebConn, r *model.WebSocketRequest, err *model.AppError) {
logF := mlog.Error
if err.StatusCode >= http.StatusBadRequest && err.StatusCode < http.StatusInternalServerError {
logF = mlog.Debug
}
logF(
"websocket routing error.",
mlog.Int64("seq", r.Seq),
mlog.String("user_id", conn.UserId),
mlog.String("system_message", err.SystemMessage(i18n.T)),
mlog.Err(err),
)
hub := ps.GetHubForUserId(conn.UserId)
if hub == nil {
return
}
err.DetailedError = ""
errorResp := model.NewWebSocketError(r.Seq, err)
hub.SendMessage(conn, errorResp)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/base64"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
"github.com/blang/semver"
"github.com/gorilla/mux"
svg "github.com/h2non/go-is-svg"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/services/marketplace"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const prepackagedPluginsDir = "prepackaged_plugins"
type pluginSignaturePath struct {
pluginID string
path string
signaturePath string
}
// Ensure routerService implements `product.RouterService`
var _ product.RouterService = (*routerService)(nil)
type routerService struct {
mu sync.Mutex
routerMap map[string]*mux.Router
}
func newRouterService() *routerService {
return &routerService{
routerMap: make(map[string]*mux.Router),
}
}
func (rs *routerService) RegisterRouter(productID string, sub *mux.Router) {
rs.mu.Lock()
defer rs.mu.Unlock()
rs.routerMap[productID] = sub
}
func (rs *routerService) getHandler(productID string) (http.Handler, bool) {
handler, ok := rs.routerMap[productID]
return handler, ok
}
// GetPluginsEnvironment returns the plugin environment for use if plugins are enabled and
// initialized.
//
// To get the plugins environment when the plugins are disabled, manually acquire the plugins
// lock instead.
func (ch *Channels) GetPluginsEnvironment() *plugin.Environment {
if !*ch.cfgSvc.Config().PluginSettings.Enable {
return nil
}
ch.pluginsLock.RLock()
defer ch.pluginsLock.RUnlock()
return ch.pluginsEnvironment
}
// GetPluginsEnvironment returns the plugin environment for use if plugins are enabled and
// initialized.
//
// To get the plugins environment when the plugins are disabled, manually acquire the plugins
// lock instead.
func (a *App) GetPluginsEnvironment() *plugin.Environment {
return a.ch.GetPluginsEnvironment()
}
func (ch *Channels) SetPluginsEnvironment(pluginsEnvironment *plugin.Environment) {
ch.pluginsLock.Lock()
defer ch.pluginsLock.Unlock()
ch.pluginsEnvironment = pluginsEnvironment
ch.srv.Platform().SetPluginsEnvironment(ch)
}
func (ch *Channels) syncPluginsActiveState() {
// Acquiring lock manually, as plugins might be disabled. See GetPluginsEnvironment.
ch.pluginsLock.RLock()
pluginsEnvironment := ch.pluginsEnvironment
ch.pluginsLock.RUnlock()
if pluginsEnvironment == nil {
return
}
config := ch.cfgSvc.Config().PluginSettings
if *config.Enable {
availablePlugins, err := pluginsEnvironment.Available()
if err != nil {
ch.srv.Log().Error("Unable to get available plugins", mlog.Err(err))
return
}
// Determine which plugins need to be activated or deactivated.
disabledPlugins := []*model.BundleInfo{}
enabledPlugins := []*model.BundleInfo{}
for _, plugin := range availablePlugins {
pluginID := plugin.Manifest.Id
pluginEnabled := false
if state, ok := config.PluginStates[pluginID]; ok {
pluginEnabled = state.Enable
}
if hasOverride, value := ch.getPluginStateOverride(pluginID); hasOverride {
pluginEnabled = value
}
if pluginEnabled {
enabledPlugins = append(enabledPlugins, plugin)
} else {
disabledPlugins = append(disabledPlugins, plugin)
}
}
// Concurrently activate/deactivate each plugin appropriately.
var wg sync.WaitGroup
// Deactivate any plugins that have been disabled.
for _, plugin := range disabledPlugins {
wg.Add(1)
go func(plugin *model.BundleInfo) {
defer wg.Done()
deactivated := pluginsEnvironment.Deactivate(plugin.Manifest.Id)
if deactivated && plugin.Manifest.HasClient() {
message := model.NewWebSocketEvent(model.WebsocketEventPluginDisabled, "", "", "", nil, "")
message.Add("manifest", plugin.Manifest.ClientManifest())
ch.srv.platform.Publish(message)
}
}(plugin)
}
// Activate any plugins that have been enabled
for _, plugin := range enabledPlugins {
wg.Add(1)
go func(plugin *model.BundleInfo) {
defer wg.Done()
pluginID := plugin.Manifest.Id
updatedManifest, activated, err := pluginsEnvironment.Activate(pluginID)
if err != nil {
plugin.WrapLogger(ch.srv.Log()).Error("Unable to activate plugin", mlog.Err(err))
return
}
if activated {
// Notify all cluster clients if ready
if err := ch.notifyPluginEnabled(updatedManifest); err != nil {
ch.srv.Log().Error("Failed to notify cluster on plugin enable", mlog.Err(err))
}
}
}(plugin)
}
wg.Wait()
} else { // If plugins are disabled, shutdown plugins.
pluginsEnvironment.Shutdown()
}
if err := ch.notifyPluginStatusesChanged(); err != nil {
mlog.Warn("failed to notify plugin status changed", mlog.Err(err))
}
}
func (a *App) NewPluginAPI(c *request.Context, manifest *model.Manifest) plugin.API {
return NewPluginAPI(a, c, manifest)
}
func (a *App) InitPlugins(c *request.Context, pluginDir, webappPluginDir string) {
a.ch.initPlugins(c, pluginDir, webappPluginDir)
}
func (ch *Channels) initPlugins(c *request.Context, pluginDir, webappPluginDir string) {
// Acquiring lock manually, as plugins might be disabled. See GetPluginsEnvironment.
defer func() {
ch.srv.Platform().SetPluginsEnvironment(ch)
}()
ch.pluginsLock.RLock()
pluginsEnvironment := ch.pluginsEnvironment
ch.pluginsLock.RUnlock()
if pluginsEnvironment != nil || !*ch.cfgSvc.Config().PluginSettings.Enable {
ch.syncPluginsActiveState()
if pluginsEnvironment != nil {
pluginsEnvironment.TogglePluginHealthCheckJob(*ch.cfgSvc.Config().PluginSettings.EnableHealthCheck)
}
return
}
ch.srv.Log().Info("Starting up plugins")
if err := os.Mkdir(pluginDir, 0744); err != nil && !os.IsExist(err) {
mlog.Error("Failed to start up plugins", mlog.Err(err))
return
}
if err := os.Mkdir(webappPluginDir, 0744); err != nil && !os.IsExist(err) {
mlog.Error("Failed to start up plugins", mlog.Err(err))
return
}
newAPIFunc := func(manifest *model.Manifest) plugin.API {
return New(ServerConnector(ch)).NewPluginAPI(c, manifest)
}
env, err := plugin.NewEnvironment(
newAPIFunc,
NewDriverImpl(ch.srv),
pluginDir,
webappPluginDir,
*ch.cfgSvc.Config().ExperimentalSettings.PatchPluginsReactDOM,
ch.srv.Log(),
ch.srv.GetMetrics(),
)
if err != nil {
mlog.Error("Failed to start up plugins", mlog.Err(err))
return
}
ch.pluginsLock.Lock()
ch.pluginsEnvironment = env
ch.pluginsLock.Unlock()
ch.pluginsEnvironment.TogglePluginHealthCheckJob(*ch.cfgSvc.Config().PluginSettings.EnableHealthCheck)
if err := ch.syncPlugins(); err != nil {
mlog.Error("Failed to sync plugins from the file store", mlog.Err(err))
}
plugins := ch.processPrepackagedPlugins(prepackagedPluginsDir)
pluginsEnvironment = ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
mlog.Info("Plugins environment not found, server is likely shutting down")
return
}
pluginsEnvironment.SetPrepackagedPlugins(plugins)
ch.installFeatureFlagPlugins()
// Sync plugin active state when config changes. Also notify plugins.
ch.pluginsLock.Lock()
ch.RemoveConfigListener(ch.pluginConfigListenerID)
ch.pluginConfigListenerID = ch.AddConfigListener(func(old, new *model.Config) {
// If plugin status remains unchanged, only then run this.
// Because (*App).InitPlugins is already run as a config change hook.
if *old.PluginSettings.Enable == *new.PluginSettings.Enable {
ch.installFeatureFlagPlugins()
ch.syncPluginsActiveState()
}
ch.RunMultiHook(func(hooks plugin.Hooks) bool {
if err := hooks.OnConfigurationChange(); err != nil {
ch.srv.Log().Error("Plugin OnConfigurationChange hook failed", mlog.Err(err))
}
return true
}, plugin.OnConfigurationChangeID)
})
ch.pluginsLock.Unlock()
ch.syncPluginsActiveState()
}
// SyncPlugins synchronizes the plugins installed locally
// with the plugin bundles available in the file store.
func (a *App) SyncPlugins() *model.AppError {
return a.ch.syncPlugins()
}
// SyncPlugins synchronizes the plugins installed locally
// with the plugin bundles available in the file store.
func (ch *Channels) syncPlugins() *model.AppError {
mlog.Info("Syncing plugins from the file store")
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return model.NewAppError("SyncPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
availablePlugins, err := pluginsEnvironment.Available()
if err != nil {
return model.NewAppError("SyncPlugins", "app.plugin.sync.read_local_folder.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
var wg sync.WaitGroup
for _, plugin := range availablePlugins {
wg.Add(1)
go func(pluginID string) {
defer wg.Done()
// Only handle managed plugins with .filestore flag file.
_, err := os.Stat(filepath.Join(*ch.cfgSvc.Config().PluginSettings.Directory, pluginID, managedPluginFileName))
if os.IsNotExist(err) {
mlog.Warn("Skipping sync for unmanaged plugin", mlog.String("plugin_id", pluginID))
} else if err != nil {
mlog.Error("Skipping sync for plugin after failure to check if managed", mlog.String("plugin_id", pluginID), mlog.Err(err))
} else {
mlog.Debug("Removing local installation of managed plugin before sync", mlog.String("plugin_id", pluginID))
if err := ch.removePluginLocally(pluginID); err != nil {
mlog.Error("Failed to remove local installation of managed plugin before sync", mlog.String("plugin_id", pluginID), mlog.Err(err))
}
}
}(plugin.Manifest.Id)
}
wg.Wait()
// Install plugins from the file store.
pluginSignaturePathMap, appErr := ch.getPluginsFromFolder()
if appErr != nil {
return appErr
}
for _, plugin := range pluginSignaturePathMap {
wg.Add(1)
go func(plugin *pluginSignaturePath) {
defer wg.Done()
reader, appErr := ch.srv.fileReader(plugin.path)
if appErr != nil {
mlog.Error("Failed to open plugin bundle from file store.", mlog.String("bundle", plugin.path), mlog.Err(appErr))
return
}
defer reader.Close()
var signature filestore.ReadCloseSeeker
if *ch.cfgSvc.Config().PluginSettings.RequirePluginSignature {
signature, appErr = ch.srv.fileReader(plugin.signaturePath)
if appErr != nil {
mlog.Error("Failed to open plugin signature from file store.", mlog.Err(appErr))
return
}
defer signature.Close()
}
mlog.Info("Syncing plugin from file store", mlog.String("bundle", plugin.path))
if _, err := ch.installPluginLocally(reader, signature, installPluginLocallyAlways); err != nil {
mlog.Error("Failed to sync plugin from file store", mlog.String("bundle", plugin.path), mlog.Err(err))
}
}(plugin)
}
wg.Wait()
return nil
}
func (ch *Channels) ShutDownPlugins() {
// Acquiring lock manually, as plugins might be disabled. See GetPluginsEnvironment.
ch.pluginsLock.RLock()
pluginsEnvironment := ch.pluginsEnvironment
ch.pluginsLock.RUnlock()
if pluginsEnvironment == nil {
return
}
mlog.Info("Shutting down plugins")
pluginsEnvironment.Shutdown()
ch.RemoveConfigListener(ch.pluginConfigListenerID)
ch.pluginConfigListenerID = ""
// Acquiring lock manually before cleaning up PluginsEnvironment.
ch.pluginsLock.Lock()
defer ch.pluginsLock.Unlock()
if ch.pluginsEnvironment == pluginsEnvironment {
ch.pluginsEnvironment = nil
} else {
mlog.Warn("Another PluginsEnvironment detected while shutting down plugins.")
}
}
func (a *App) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) {
pluginsEnvironment := a.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return nil, model.NewAppError("GetActivePluginManifests", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
plugins := pluginsEnvironment.Active()
manifests := make([]*model.Manifest, len(plugins))
for i, plugin := range plugins {
manifests[i] = plugin.Manifest
}
return manifests, nil
}
// EnablePlugin will set the config for an installed plugin to enabled, triggering asynchronous
// activation if inactive anywhere in the cluster.
// Notifies cluster peers through config change.
func (a *App) EnablePlugin(id string) *model.AppError {
return a.ch.enablePlugin(id)
}
func (ch *Channels) enablePlugin(id string) *model.AppError {
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return model.NewAppError("EnablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
availablePlugins, err := pluginsEnvironment.Available()
if err != nil {
return model.NewAppError("EnablePlugin", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
id = strings.ToLower(id)
var manifest *model.Manifest
for _, p := range availablePlugins {
if p.Manifest.Id == id {
manifest = p.Manifest
break
}
}
if manifest == nil {
return model.NewAppError("EnablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusNotFound)
}
ch.cfgSvc.UpdateConfig(func(cfg *model.Config) {
cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: true}
})
// This call will implicitly invoke SyncPluginsActiveState which will activate enabled plugins.
if _, _, err := ch.cfgSvc.SaveConfig(ch.cfgSvc.Config(), true); err != nil {
if err.Id == "ent.cluster.save_config.error" {
return model.NewAppError("EnablePlugin", "app.plugin.cluster.save_config.app_error", nil, "", http.StatusInternalServerError)
}
return model.NewAppError("EnablePlugin", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
// DisablePlugin will set the config for an installed plugin to disabled, triggering deactivation if active.
// Notifies cluster peers through config change.
func (a *App) DisablePlugin(id string) *model.AppError {
appErr := a.ch.disablePlugin(id)
if appErr != nil {
return appErr
}
return nil
}
func (ch *Channels) disablePlugin(id string) *model.AppError {
// find all collectionTypes registered by plugin
for collectionTypeToRemove, existingPluginId := range ch.collectionTypes {
if existingPluginId != id {
continue
}
// find all topicTypes for existing collectionType
for topicTypeToRemove, existingCollectionType := range ch.topicTypes {
if existingCollectionType == collectionTypeToRemove {
delete(ch.topicTypes, topicTypeToRemove)
}
}
delete(ch.collectionTypes, collectionTypeToRemove)
}
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return model.NewAppError("DisablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
availablePlugins, err := pluginsEnvironment.Available()
if err != nil {
return model.NewAppError("DisablePlugin", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
id = strings.ToLower(id)
var manifest *model.Manifest
for _, p := range availablePlugins {
if p.Manifest.Id == id {
manifest = p.Manifest
break
}
}
if manifest == nil {
return model.NewAppError("DisablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusNotFound)
}
ch.cfgSvc.UpdateConfig(func(cfg *model.Config) {
cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: false}
})
ch.unregisterPluginCommands(id)
// This call will implicitly invoke SyncPluginsActiveState which will deactivate disabled plugins.
if _, _, err := ch.cfgSvc.SaveConfig(ch.cfgSvc.Config(), true); err != nil {
return model.NewAppError("DisablePlugin", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) {
pluginsEnvironment := a.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return nil, model.NewAppError("GetPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
availablePlugins, err := pluginsEnvironment.Available()
if err != nil {
return nil, model.NewAppError("GetPlugins", "app.plugin.get_plugins.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
resp := &model.PluginsResponse{Active: []*model.PluginInfo{}, Inactive: []*model.PluginInfo{}}
for _, plugin := range availablePlugins {
if plugin.Manifest == nil {
continue
}
info := &model.PluginInfo{
Manifest: *plugin.Manifest,
}
if pluginsEnvironment.IsActive(plugin.Manifest.Id) {
resp.Active = append(resp.Active, info)
} else {
resp.Inactive = append(resp.Inactive, info)
}
}
return resp, nil
}
// GetMarketplacePlugins returns a list of plugins from the marketplace-server,
// and plugins that are installed locally.
func (a *App) GetMarketplacePlugins(filter *model.MarketplacePluginFilter) ([]*model.MarketplacePlugin, *model.AppError) {
plugins := map[string]*model.MarketplacePlugin{}
if *a.Config().PluginSettings.EnableRemoteMarketplace && !filter.LocalOnly {
p, appErr := a.getRemotePlugins()
if appErr != nil {
return nil, appErr
}
plugins = p
}
if !filter.RemoteOnly {
// Some plugin don't work on cloud. The remote Marketplace is aware of this fact,
// but prepackaged plugins are not. Hence, on a cloud installation prepackaged plugins
// shouldn't be shown in the Marketplace modal.
// This is a short term fix. The long term solution is to have a separate set of
// prepacked plugins for cloud: https://mattermost.atlassian.net/browse/MM-31331.
license := a.Srv().License()
if license == nil || !license.IsCloud() {
appErr := a.mergePrepackagedPlugins(plugins)
if appErr != nil {
return nil, appErr
}
}
appErr := a.mergeLocalPlugins(plugins)
if appErr != nil {
return nil, appErr
}
}
// Filter plugins.
var result []*model.MarketplacePlugin
for _, p := range plugins {
if pluginMatchesFilter(p.Manifest, filter.Filter) {
result = append(result, p)
}
}
// Sort result alphabetically.
sort.SliceStable(result, func(i, j int) bool {
return strings.ToLower(result[i].Manifest.Name) < strings.ToLower(result[j].Manifest.Name)
})
return result, nil
}
// getPrepackagedPlugin returns a pre-packaged plugin.
//
// If version is empty, the first matching plugin is returned.
func (ch *Channels) getPrepackagedPlugin(pluginID, version string) (*plugin.PrepackagedPlugin, *model.AppError) {
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return nil, model.NewAppError("getPrepackagedPlugin", "app.plugin.config.app_error", nil, "plugin environment is nil", http.StatusInternalServerError)
}
prepackagedPlugins := pluginsEnvironment.PrepackagedPlugins()
for _, p := range prepackagedPlugins {
if p.Manifest.Id == pluginID && (version == "" || p.Manifest.Version == version) {
return p, nil
}
}
return nil, model.NewAppError("getPrepackagedPlugin", "app.plugin.marketplace_plugins.not_found.app_error", nil, "", http.StatusInternalServerError)
}
// getRemoteMarketplacePlugin returns plugin from marketplace-server.
//
// If version is empty, the latest compatible version is used.
func (ch *Channels) getRemoteMarketplacePlugin(pluginID, version string) (*model.BaseMarketplacePlugin, *model.AppError) {
marketplaceClient, err := marketplace.NewClient(
*ch.cfgSvc.Config().PluginSettings.MarketplaceURL,
ch.srv.HTTPService(),
)
if err != nil {
return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_client.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
filter := ch.getBaseMarketplaceFilter()
filter.PluginId = pluginID
var plugin *model.BaseMarketplacePlugin
if version != "" {
plugin, err = marketplaceClient.GetPlugin(filter, version)
} else {
plugin, err = marketplaceClient.GetLatestPlugin(filter)
}
if err != nil {
return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_plugins.not_found.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return plugin, nil
}
func (a *App) getRemotePlugins() (map[string]*model.MarketplacePlugin, *model.AppError) {
result := map[string]*model.MarketplacePlugin{}
pluginsEnvironment := a.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return nil, model.NewAppError("getRemotePlugins", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError)
}
marketplaceClient, err := marketplace.NewClient(
*a.Config().PluginSettings.MarketplaceURL,
a.HTTPService(),
)
if err != nil {
return nil, model.NewAppError("getRemotePlugins", "app.plugin.marketplace_client.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
filter := a.getBaseMarketplaceFilter()
// Fetch all plugins from marketplace.
filter.PerPage = -1
marketplacePlugins, err := marketplaceClient.GetPlugins(filter)
if err != nil {
return nil, model.NewAppError("getRemotePlugins", "app.plugin.marketplace_client.failed_to_fetch", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, p := range marketplacePlugins {
if p.Manifest == nil {
continue
}
result[p.Manifest.Id] = &model.MarketplacePlugin{BaseMarketplacePlugin: p}
}
return result, nil
}
// mergePrepackagedPlugins merges pre-packaged plugins to remote marketplace plugins list.
func (a *App) mergePrepackagedPlugins(remoteMarketplacePlugins map[string]*model.MarketplacePlugin) *model.AppError {
pluginsEnvironment := a.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return model.NewAppError("mergePrepackagedPlugins", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError)
}
for _, prepackaged := range pluginsEnvironment.PrepackagedPlugins() {
if prepackaged.Manifest == nil {
continue
}
prepackagedMarketplace := &model.MarketplacePlugin{
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
HomepageURL: prepackaged.Manifest.HomepageURL,
IconData: prepackaged.IconData,
ReleaseNotesURL: prepackaged.Manifest.ReleaseNotesURL,
Manifest: prepackaged.Manifest,
},
}
// If not available in marketplace, add the prepackaged
if remoteMarketplacePlugins[prepackaged.Manifest.Id] == nil {
remoteMarketplacePlugins[prepackaged.Manifest.Id] = prepackagedMarketplace
continue
}
// If available in the marketplace, only overwrite if newer.
prepackagedVersion, err := semver.Parse(prepackaged.Manifest.Version)
if err != nil {
return model.NewAppError("mergePrepackagedPlugins", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
marketplacePlugin := remoteMarketplacePlugins[prepackaged.Manifest.Id]
marketplaceVersion, err := semver.Parse(marketplacePlugin.Manifest.Version)
if err != nil {
return model.NewAppError("mergePrepackagedPlugins", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if prepackagedVersion.GT(marketplaceVersion) {
remoteMarketplacePlugins[prepackaged.Manifest.Id] = prepackagedMarketplace
}
}
return nil
}
// mergeLocalPlugins merges locally installed plugins to remote marketplace plugins list.
func (a *App) mergeLocalPlugins(remoteMarketplacePlugins map[string]*model.MarketplacePlugin) *model.AppError {
pluginsEnvironment := a.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return model.NewAppError("GetMarketplacePlugins", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError)
}
localPlugins, err := pluginsEnvironment.Available()
if err != nil {
return model.NewAppError("GetMarketplacePlugins", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, plugin := range localPlugins {
if plugin.Manifest == nil {
continue
}
if remoteMarketplacePlugins[plugin.Manifest.Id] != nil {
// Remote plugin is installed.
remoteMarketplacePlugins[plugin.Manifest.Id].InstalledVersion = plugin.Manifest.Version
continue
}
iconData := ""
if plugin.Manifest.IconPath != "" {
iconData, err = getIcon(filepath.Join(plugin.Path, plugin.Manifest.IconPath))
if err != nil {
mlog.Warn("Error loading local plugin icon", mlog.String("plugin", plugin.Manifest.Id), mlog.String("icon_path", plugin.Manifest.IconPath), mlog.Err(err))
}
}
var labels []model.MarketplaceLabel
if *a.Config().PluginSettings.EnableRemoteMarketplace {
// Labels should not (yet) be localized as the labels sent by the Marketplace are not (yet) localizable.
labels = append(labels, model.MarketplaceLabel{
Name: "Local",
Description: "This plugin is not listed in the marketplace",
})
}
remoteMarketplacePlugins[plugin.Manifest.Id] = &model.MarketplacePlugin{
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
HomepageURL: plugin.Manifest.HomepageURL,
IconData: iconData,
ReleaseNotesURL: plugin.Manifest.ReleaseNotesURL,
Labels: labels,
Manifest: plugin.Manifest,
},
InstalledVersion: plugin.Manifest.Version,
}
}
return nil
}
func (a *App) getBaseMarketplaceFilter() *model.MarketplacePluginFilter {
return a.ch.getBaseMarketplaceFilter()
}
func (ch *Channels) getBaseMarketplaceFilter() *model.MarketplacePluginFilter {
filter := &model.MarketplacePluginFilter{
ServerVersion: model.CurrentVersion,
}
license := ch.srv.License()
if license != nil && license.HasEnterpriseMarketplacePlugins() {
filter.EnterprisePlugins = true
}
if license != nil && license.IsCloud() {
filter.Cloud = true
}
if model.BuildEnterpriseReady == "true" {
filter.BuildEnterpriseReady = true
}
filter.Platform = runtime.GOOS + "-" + runtime.GOARCH
return filter
}
func pluginMatchesFilter(manifest *model.Manifest, filter string) bool {
filter = strings.TrimSpace(strings.ToLower(filter))
if filter == "" {
return true
}
if strings.ToLower(manifest.Id) == filter {
return true
}
if strings.Contains(strings.ToLower(manifest.Name), filter) {
return true
}
if strings.Contains(strings.ToLower(manifest.Description), filter) {
return true
}
return false
}
// notifyPluginEnabled notifies connected websocket clients across all peers if the version of the given
// plugin is same across them.
//
// When a peer finds itself in agreement with all other peers as to the version of the given plugin,
// it will notify all connected websocket clients (across all peers) to trigger the (re-)installation.
// There is a small chance that this never occurs, because the last server to finish installing dies before it can announce.
// There is also a chance that multiple servers notify, but the webapp handles this idempotently.
func (ch *Channels) notifyPluginEnabled(manifest *model.Manifest) error {
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return errors.New("pluginsEnvironment is nil")
}
if !manifest.HasClient() || !pluginsEnvironment.IsActive(manifest.Id) {
return nil
}
var statuses model.PluginStatuses
if ch.srv.platform.Cluster() != nil {
var err *model.AppError
statuses, err = ch.srv.platform.Cluster().GetPluginStatuses()
if err != nil {
return err
}
}
localStatus, err := ch.GetPluginStatus(manifest.Id)
if err != nil {
return err
}
statuses = append(statuses, localStatus)
// This will not guard against the race condition of enabling a plugin immediately after installation.
// As GetPluginStatuses() will not return the new plugin (since other peers are racing to install),
// this peer will end up checking status against itself and will notify all webclients (including peer webclients),
// which may result in a 404.
for _, status := range statuses {
if status.PluginId == manifest.Id && status.Version != manifest.Version {
mlog.Debug("Not ready to notify webclients", mlog.String("cluster_id", status.ClusterId), mlog.String("plugin_id", manifest.Id))
return nil
}
}
// Notify all cluster peer clients.
message := model.NewWebSocketEvent(model.WebsocketEventPluginEnabled, "", "", "", nil, "")
message.Add("manifest", manifest.ClientManifest())
ch.srv.platform.Publish(message)
return nil
}
func (ch *Channels) getPluginsFromFolder() (map[string]*pluginSignaturePath, *model.AppError) {
fileStorePaths, appErr := ch.srv.listDirectory(fileStorePluginFolder, false)
if appErr != nil {
return nil, model.NewAppError("getPluginsFromDir", "app.plugin.sync.list_filestore.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
}
return ch.getPluginsFromFilePaths(fileStorePaths), nil
}
func (ch *Channels) getPluginsFromFilePaths(fileStorePaths []string) map[string]*pluginSignaturePath {
pluginSignaturePathMap := make(map[string]*pluginSignaturePath)
fsPrefix := ""
if *ch.cfgSvc.Config().FileSettings.DriverName == model.ImageDriverS3 {
ptr := ch.cfgSvc.Config().FileSettings.AmazonS3PathPrefix
if ptr != nil && *ptr != "" {
fsPrefix = *ptr + "/"
}
}
for _, path := range fileStorePaths {
path = strings.TrimPrefix(path, fsPrefix)
if strings.HasSuffix(path, ".tar.gz") {
id := strings.TrimSuffix(filepath.Base(path), ".tar.gz")
helper := &pluginSignaturePath{
pluginID: id,
path: path,
signaturePath: "",
}
pluginSignaturePathMap[id] = helper
}
}
for _, path := range fileStorePaths {
path = strings.TrimPrefix(path, fsPrefix)
if strings.HasSuffix(path, ".tar.gz.sig") {
id := strings.TrimSuffix(filepath.Base(path), ".tar.gz.sig")
if val, ok := pluginSignaturePathMap[id]; !ok {
mlog.Warn("Unknown signature", mlog.String("path", path))
} else {
val.signaturePath = path
}
}
}
return pluginSignaturePathMap
}
func (ch *Channels) processPrepackagedPlugins(pluginsDir string) []*plugin.PrepackagedPlugin {
prepackagedPluginsDir, found := fileutils.FindDir(pluginsDir)
if !found {
return nil
}
var fileStorePaths []string
err := filepath.Walk(prepackagedPluginsDir, func(walkPath string, info os.FileInfo, err error) error {
fileStorePaths = append(fileStorePaths, walkPath)
return nil
})
if err != nil {
mlog.Error("Failed to walk prepackaged plugins", mlog.Err(err))
return nil
}
pluginSignaturePathMap := ch.getPluginsFromFilePaths(fileStorePaths)
plugins := make([]*plugin.PrepackagedPlugin, 0, len(pluginSignaturePathMap))
prepackagedPlugins := make(chan *plugin.PrepackagedPlugin, len(pluginSignaturePathMap))
var wg sync.WaitGroup
for _, psPath := range pluginSignaturePathMap {
wg.Add(1)
go func(psPath *pluginSignaturePath) {
defer wg.Done()
p, err := ch.processPrepackagedPlugin(psPath)
if err != nil {
mlog.Error("Failed to install prepackaged plugin", mlog.String("path", psPath.path), mlog.Err(err))
return
}
prepackagedPlugins <- p
}(psPath)
}
wg.Wait()
close(prepackagedPlugins)
for p := range prepackagedPlugins {
plugins = append(plugins, p)
}
return plugins
}
// processPrepackagedPlugin will return the prepackaged plugin metadata and will also
// install the prepackaged plugin if it had been previously enabled and AutomaticPrepackagedPlugins is true.
func (ch *Channels) processPrepackagedPlugin(pluginPath *pluginSignaturePath) (*plugin.PrepackagedPlugin, error) {
mlog.Debug("Processing prepackaged plugin", mlog.String("path", pluginPath.path))
fileReader, err := os.Open(pluginPath.path)
if err != nil {
return nil, errors.Wrapf(err, "Failed to open prepackaged plugin %s", pluginPath.path)
}
defer fileReader.Close()
tmpDir, err := os.MkdirTemp("", "plugintmp")
if err != nil {
return nil, errors.Wrap(err, "Failed to create temp dir plugintmp")
}
defer os.RemoveAll(tmpDir)
plugin, pluginDir, err := getPrepackagedPlugin(pluginPath, fileReader, tmpDir)
if err != nil {
return nil, errors.Wrapf(err, "Failed to get prepackaged plugin %s", pluginPath.path)
}
// Skip installing the plugin at all if automatic prepackaged plugins is disabled
if !*ch.cfgSvc.Config().PluginSettings.AutomaticPrepackagedPlugins {
return plugin, nil
}
// Skip installing if the plugin is has not been previously enabled.
pluginState := ch.cfgSvc.Config().PluginSettings.PluginStates[plugin.Manifest.Id]
if pluginState == nil || !pluginState.Enable {
return plugin, nil
}
mlog.Debug("Installing prepackaged plugin", mlog.String("path", pluginPath.path))
if _, err := ch.installExtractedPlugin(plugin.Manifest, pluginDir, installPluginLocallyOnlyIfNewOrUpgrade); err != nil {
return nil, errors.Wrapf(err, "Failed to install extracted prepackaged plugin %s", pluginPath.path)
}
return plugin, nil
}
// installFeatureFlagPlugins handles the automatic installation/upgrade of plugins from feature flags
func (ch *Channels) installFeatureFlagPlugins() {
ffControledPlugins := ch.cfgSvc.Config().FeatureFlags.Plugins()
// Respect the automatic prepackaged disable setting
if !*ch.cfgSvc.Config().PluginSettings.AutomaticPrepackagedPlugins {
return
}
for pluginID, version := range ffControledPlugins {
// Skip installing if the plugin has been previously disabled.
pluginState := ch.cfgSvc.Config().PluginSettings.PluginStates[pluginID]
if pluginState != nil && !pluginState.Enable {
ch.srv.Log().Debug("Not auto installing/upgrade because plugin was disabled", mlog.String("plugin_id", pluginID), mlog.String("version", version))
continue
}
// Check if we already installed this version as InstallMarketplacePlugin can't handle re-installs well.
pluginStatus, err := ch.GetPluginStatus(pluginID)
pluginExists := err == nil
if pluginExists && pluginStatus.Version == version {
continue
}
if version != "" && version != "control" {
// If we are on-prem skip installation if this is a downgrade
license := ch.srv.License()
inCloud := license != nil && *license.Features.Cloud
if !inCloud && pluginExists {
parsedVersion, err := semver.Parse(version)
if err != nil {
ch.srv.Log().Debug("Bad version from feature flag", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", version))
return
}
parsedExistingVersion, err := semver.Parse(pluginStatus.Version)
if err != nil {
ch.srv.Log().Debug("Bad version from plugin manifest", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", pluginStatus.Version))
return
}
if parsedVersion.LTE(parsedExistingVersion) {
ch.srv.Log().Debug("Skip installation because given version was a downgrade and on-prem installations should not downgrade.", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", pluginStatus.Version))
return
}
}
_, err := ch.InstallMarketplacePlugin(&model.InstallMarketplacePluginRequest{
Id: pluginID,
Version: version,
})
if err != nil {
ch.srv.Log().Debug("Unable to install plugin from FF manifest", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", version))
} else {
if err := ch.enablePlugin(pluginID); err != nil {
ch.srv.Log().Debug("Unable to enable plugin installed from feature flag.", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", version))
} else {
ch.srv.Log().Debug("Installed and enabled plugin.", mlog.String("plugin_id", pluginID), mlog.String("version", version))
}
}
}
}
}
// getPrepackagedPlugin builds a PrepackagedPlugin from the plugin at the given path, additionally returning the directory in which it was extracted.
func getPrepackagedPlugin(pluginPath *pluginSignaturePath, pluginFile io.ReadSeeker, tmpDir string) (*plugin.PrepackagedPlugin, string, error) {
manifest, pluginDir, appErr := extractPlugin(pluginFile, tmpDir)
if appErr != nil {
return nil, "", errors.Wrapf(appErr, "Failed to extract plugin with path %s", pluginPath.path)
}
plugin := new(plugin.PrepackagedPlugin)
plugin.Manifest = manifest
plugin.Path = pluginPath.path
if pluginPath.signaturePath != "" {
sig := pluginPath.signaturePath
sigReader, sigErr := os.Open(sig)
if sigErr != nil {
return nil, "", errors.Wrapf(sigErr, "Failed to open prepackaged plugin signature %s", sig)
}
bytes, sigErr := io.ReadAll(sigReader)
if sigErr != nil {
return nil, "", errors.Wrapf(sigErr, "Failed to read prepackaged plugin signature %s", sig)
}
plugin.Signature = bytes
}
if manifest.IconPath != "" {
iconData, err := getIcon(filepath.Join(pluginDir, manifest.IconPath))
if err != nil {
mlog.Warn("Error loading local plugin icon", mlog.String("plugin", plugin.Manifest.Id), mlog.String("icon_path", plugin.Manifest.IconPath), mlog.Err(err))
}
plugin.IconData = iconData
}
return plugin, pluginDir, nil
}
func getIcon(iconPath string) (string, error) {
icon, err := os.ReadFile(iconPath)
if err != nil {
return "", errors.Wrapf(err, "failed to open icon at path %s", iconPath)
}
if !svg.Is(icon) {
return "", errors.Errorf("icon is not svg %s", iconPath)
}
return fmt.Sprintf("data:image/svg+xml;base64,%s", base64.StdEncoding.EncodeToString(icon)), nil
}
func (ch *Channels) getPluginStateOverride(pluginID string) (bool, bool) {
switch pluginID {
case model.PluginIdApps:
// Tie Apps proxy disabled status to the feature flag.
if !ch.cfgSvc.Config().FeatureFlags.AppsEnabled {
return true, false
}
case model.PluginIdCalls:
if !ch.cfgSvc.Config().FeatureFlags.CallsEnabled {
return true, false
}
}
return false, false
}
func (a *App) IsPluginActive(pluginName string) (bool, error) {
return a.Channels().IsPluginActive(pluginName)
}
func (ch *Channels) IsPluginActive(pluginName string) (bool, error) {
pluginStatus, err := ch.GetPluginStatus(pluginName)
if err != nil {
return false, err
}
return pluginStatus.State == model.PluginStateRunning, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path/filepath"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type PluginAPI struct {
id string
app *App
ctx *request.Context
logger mlog.Sugar
manifest *model.Manifest
}
func NewPluginAPI(a *App, c *request.Context, manifest *model.Manifest) *PluginAPI {
return &PluginAPI{
id: manifest.Id,
manifest: manifest,
ctx: c,
app: a,
logger: a.Log().Sugar(mlog.String("plugin_id", manifest.Id)),
}
}
func (api *PluginAPI) LoadPluginConfiguration(dest any) error {
finalConfig := make(map[string]any)
// First set final config to defaults
if api.manifest.SettingsSchema != nil {
for _, setting := range api.manifest.SettingsSchema.Settings {
finalConfig[strings.ToLower(setting.Key)] = setting.Default
}
}
// If we have settings given we override the defaults with them
for setting, value := range api.app.Config().PluginSettings.Plugins[api.id] {
finalConfig[strings.ToLower(setting)] = value
}
pluginSettingsJsonBytes, err := json.Marshal(finalConfig)
if err != nil {
api.logger.Error("Error marshaling config for plugin", mlog.Err(err))
return nil
}
err = json.Unmarshal(pluginSettingsJsonBytes, dest)
if err != nil {
api.logger.Error("Error unmarshaling config for plugin", mlog.Err(err))
}
return nil
}
func (api *PluginAPI) RegisterCommand(command *model.Command) error {
return api.app.RegisterPluginCommand(api.id, command)
}
func (api *PluginAPI) UnregisterCommand(teamID, trigger string) error {
api.app.UnregisterPluginCommand(api.id, teamID, trigger)
return nil
}
func (api *PluginAPI) ExecuteSlashCommand(commandArgs *model.CommandArgs) (*model.CommandResponse, error) {
user, appErr := api.app.GetUser(commandArgs.UserId)
if appErr != nil {
return nil, appErr
}
commandArgs.T = i18n.GetUserTranslations(user.Locale)
commandArgs.SiteURL = api.app.GetSiteURL()
response, appErr := api.app.ExecuteCommand(api.ctx, commandArgs)
if appErr != nil {
return response, appErr
}
return response, nil
}
func (api *PluginAPI) GetConfig() *model.Config {
return api.app.GetSanitizedConfig()
}
// GetUnsanitizedConfig gets the configuration for a system admin without removing secrets.
func (api *PluginAPI) GetUnsanitizedConfig() *model.Config {
return api.app.Config().Clone()
}
func (api *PluginAPI) SaveConfig(config *model.Config) *model.AppError {
_, _, err := api.app.SaveConfig(config, true)
return err
}
func (api *PluginAPI) GetPluginConfig() map[string]any {
cfg := api.app.GetSanitizedConfig()
if pluginConfig, isOk := cfg.PluginSettings.Plugins[api.manifest.Id]; isOk {
return pluginConfig
}
return map[string]any{}
}
func (api *PluginAPI) SavePluginConfig(pluginConfig map[string]any) *model.AppError {
cfg := api.app.GetSanitizedConfig()
cfg.PluginSettings.Plugins[api.manifest.Id] = pluginConfig
_, _, err := api.app.SaveConfig(cfg, true)
return err
}
func (api *PluginAPI) GetBundlePath() (string, error) {
bundlePath, err := filepath.Abs(filepath.Join(*api.GetConfig().PluginSettings.Directory, api.manifest.Id))
if err != nil {
return "", err
}
return bundlePath, err
}
func (api *PluginAPI) GetLicense() *model.License {
return api.app.Srv().License()
}
func (api *PluginAPI) IsEnterpriseReady() bool {
result, _ := strconv.ParseBool(model.BuildEnterpriseReady)
return result
}
func (api *PluginAPI) GetServerVersion() string {
return model.CurrentVersion
}
func (api *PluginAPI) GetSystemInstallDate() (int64, *model.AppError) {
return api.app.Srv().Platform().GetSystemInstallDate()
}
func (api *PluginAPI) GetDiagnosticId() string {
return api.app.TelemetryId()
}
func (api *PluginAPI) GetTelemetryId() string {
return api.app.TelemetryId()
}
func (api *PluginAPI) CreateTeam(team *model.Team) (*model.Team, *model.AppError) {
return api.app.CreateTeam(api.ctx, team)
}
func (api *PluginAPI) DeleteTeam(teamID string) *model.AppError {
return api.app.SoftDeleteTeam(teamID)
}
func (api *PluginAPI) GetTeams() ([]*model.Team, *model.AppError) {
return api.app.GetAllTeams()
}
func (api *PluginAPI) GetTeam(teamID string) (*model.Team, *model.AppError) {
return api.app.GetTeam(teamID)
}
func (api *PluginAPI) SearchTeams(term string) ([]*model.Team, *model.AppError) {
teams, _, err := api.app.SearchAllTeams(&model.TeamSearch{Term: term})
return teams, err
}
func (api *PluginAPI) GetTeamByName(name string) (*model.Team, *model.AppError) {
return api.app.GetTeamByName(name)
}
func (api *PluginAPI) GetTeamsUnreadForUser(userID string) ([]*model.TeamUnread, *model.AppError) {
return api.app.GetTeamsUnreadForUser("", userID, false)
}
func (api *PluginAPI) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
return api.app.UpdateTeam(team)
}
func (api *PluginAPI) GetTeamsForUser(userID string) ([]*model.Team, *model.AppError) {
return api.app.GetTeamsForUser(userID)
}
func (api *PluginAPI) CreateTeamMember(teamID, userID string) (*model.TeamMember, *model.AppError) {
return api.app.AddTeamMember(api.ctx, teamID, userID)
}
func (api *PluginAPI) CreateTeamMembers(teamID string, userIDs []string, requestorId string) ([]*model.TeamMember, *model.AppError) {
members, err := api.app.AddTeamMembers(api.ctx, teamID, userIDs, requestorId, false)
if err != nil {
return nil, err
}
return model.TeamMembersWithErrorToTeamMembers(members), nil
}
func (api *PluginAPI) CreateTeamMembersGracefully(teamID string, userIDs []string, requestorId string) ([]*model.TeamMemberWithError, *model.AppError) {
return api.app.AddTeamMembers(api.ctx, teamID, userIDs, requestorId, true)
}
func (api *PluginAPI) DeleteTeamMember(teamID, userID, requestorId string) *model.AppError {
return api.app.RemoveUserFromTeam(api.ctx, teamID, userID, requestorId)
}
func (api *PluginAPI) GetTeamMembers(teamID string, page, perPage int) ([]*model.TeamMember, *model.AppError) {
return api.app.GetTeamMembers(teamID, page*perPage, perPage, nil)
}
func (api *PluginAPI) GetTeamMember(teamID, userID string) (*model.TeamMember, *model.AppError) {
return api.app.GetTeamMember(teamID, userID)
}
func (api *PluginAPI) GetTeamMembersForUser(userID string, page int, perPage int) ([]*model.TeamMember, *model.AppError) {
return api.app.GetTeamMembersForUserWithPagination(userID, page, perPage)
}
func (api *PluginAPI) UpdateTeamMemberRoles(teamID, userID, newRoles string) (*model.TeamMember, *model.AppError) {
return api.app.UpdateTeamMemberRoles(teamID, userID, newRoles)
}
func (api *PluginAPI) GetTeamStats(teamID string) (*model.TeamStats, *model.AppError) {
return api.app.GetTeamStats(teamID, nil)
}
func (api *PluginAPI) CreateUser(user *model.User) (*model.User, *model.AppError) {
return api.app.CreateUser(api.ctx, user)
}
func (api *PluginAPI) DeleteUser(userID string) *model.AppError {
user, err := api.app.GetUser(userID)
if err != nil {
return err
}
_, err = api.app.UpdateActive(api.ctx, user, false)
return err
}
func (api *PluginAPI) GetUsers(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
return api.app.GetUsersFromProfiles(options)
}
func (api *PluginAPI) GetUser(userID string) (*model.User, *model.AppError) {
return api.app.GetUser(userID)
}
func (api *PluginAPI) GetUserByEmail(email string) (*model.User, *model.AppError) {
return api.app.GetUserByEmail(email)
}
func (api *PluginAPI) GetUserByUsername(name string) (*model.User, *model.AppError) {
return api.app.GetUserByUsername(name)
}
func (api *PluginAPI) GetUsersByUsernames(usernames []string) ([]*model.User, *model.AppError) {
return api.app.GetUsersByUsernames(usernames, true, nil)
}
func (api *PluginAPI) GetUsersInTeam(teamID string, page int, perPage int) ([]*model.User, *model.AppError) {
options := &model.UserGetOptions{InTeamId: teamID, Page: page, PerPage: perPage}
return api.app.GetUsersInTeam(options)
}
func (api *PluginAPI) GetPreferencesForUser(userID string) ([]model.Preference, *model.AppError) {
return api.app.GetPreferencesForUser(userID)
}
func (api *PluginAPI) UpdatePreferencesForUser(userID string, preferences []model.Preference) *model.AppError {
return api.app.UpdatePreferences(userID, preferences)
}
func (api *PluginAPI) DeletePreferencesForUser(userID string, preferences []model.Preference) *model.AppError {
return api.app.DeletePreferences(userID, preferences)
}
func (api *PluginAPI) GetSession(sessionID string) (*model.Session, *model.AppError) {
return api.app.GetSessionById(sessionID)
}
func (api *PluginAPI) CreateSession(session *model.Session) (*model.Session, *model.AppError) {
return api.app.CreateSession(session)
}
func (api *PluginAPI) ExtendSessionExpiry(sessionID string, expiresAt int64) *model.AppError {
session, err := api.app.ch.srv.platform.GetSessionByID(sessionID)
if err != nil {
return model.NewAppError("extendSessionExpiry", "app.session.get_sessions.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := api.app.ch.srv.platform.ExtendSessionExpiry(session, expiresAt); err != nil {
return model.NewAppError("extendSessionExpiry", "app.session.extend_session_expiry.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (api *PluginAPI) RevokeSession(sessionID string) *model.AppError {
return api.app.RevokeSessionById(sessionID)
}
func (api *PluginAPI) CreateUserAccessToken(token *model.UserAccessToken) (*model.UserAccessToken, *model.AppError) {
return api.app.CreateUserAccessToken(token)
}
func (api *PluginAPI) RevokeUserAccessToken(tokenID string) *model.AppError {
accessToken, err := api.app.GetUserAccessToken(tokenID, false)
if err != nil {
return err
}
return api.app.RevokeUserAccessToken(accessToken)
}
func (api *PluginAPI) UpdateUser(user *model.User) (*model.User, *model.AppError) {
return api.app.UpdateUser(api.ctx, user, true)
}
func (api *PluginAPI) UpdateUserActive(userID string, active bool) *model.AppError {
return api.app.UpdateUserActive(api.ctx, userID, active)
}
func (api *PluginAPI) GetUserStatus(userID string) (*model.Status, *model.AppError) {
return api.app.GetStatus(userID)
}
func (api *PluginAPI) GetUserStatusesByIds(userIDs []string) ([]*model.Status, *model.AppError) {
return api.app.GetUserStatusesByIds(userIDs)
}
func (api *PluginAPI) UpdateUserStatus(userID, status string) (*model.Status, *model.AppError) {
switch status {
case model.StatusOnline:
api.app.SetStatusOnline(userID, true)
case model.StatusOffline:
api.app.SetStatusOffline(userID, true)
case model.StatusAway:
api.app.SetStatusAwayIfNeeded(userID, true)
case model.StatusDnd:
api.app.SetStatusDoNotDisturb(userID)
default:
return nil, model.NewAppError("UpdateUserStatus", "plugin.api.update_user_status.bad_status", nil, "unrecognized status", http.StatusBadRequest)
}
return api.app.GetStatus(userID)
}
func (api *PluginAPI) SetUserStatusTimedDND(userID string, endTime int64) (*model.Status, *model.AppError) {
// read-after-write bug which will fail if there are replicas.
// it works for now because we have a cache in between.
// FIXME: make SetStatusDoNotDisturbTimed return updated status
api.app.SetStatusDoNotDisturbTimed(userID, endTime)
return api.app.GetStatus(userID)
}
func (api *PluginAPI) UpdateUserCustomStatus(userID string, customStatus *model.CustomStatus) *model.AppError {
return api.app.SetCustomStatus(api.ctx, userID, customStatus)
}
func (api *PluginAPI) RemoveUserCustomStatus(userID string) *model.AppError {
return api.app.RemoveCustomStatus(api.ctx, userID)
}
func (api *PluginAPI) GetUserCustomStatus(userID string) (*model.CustomStatus, *model.AppError) {
return api.app.GetCustomStatus(userID)
}
func (api *PluginAPI) GetUsersInChannel(channelID, sortBy string, page, perPage int) ([]*model.User, *model.AppError) {
switch sortBy {
case model.ChannelSortByUsername:
return api.app.GetUsersInChannel(&model.UserGetOptions{
InChannelId: channelID,
Page: page,
PerPage: perPage,
})
case model.ChannelSortByStatus:
return api.app.GetUsersInChannelByStatus(&model.UserGetOptions{
InChannelId: channelID,
Page: page,
PerPage: perPage,
})
default:
return nil, model.NewAppError("GetUsersInChannel", "plugin.api.get_users_in_channel", nil, "invalid sort option", http.StatusBadRequest)
}
}
func (api *PluginAPI) GetLDAPUserAttributes(userID string, attributes []string) (map[string]string, *model.AppError) {
if api.app.Ldap() == nil {
return nil, model.NewAppError("GetLdapUserAttributes", "ent.ldap.disabled.app_error", nil, "", http.StatusNotImplemented)
}
user, err := api.app.GetUser(userID)
if err != nil {
return nil, err
}
if user.AuthData == nil {
return map[string]string{}, nil
}
// Only bother running the query if the user's auth service is LDAP or it's SAML and sync is enabled.
if user.AuthService == model.UserAuthServiceLdap ||
(user.AuthService == model.UserAuthServiceSaml && *api.app.Config().SamlSettings.EnableSyncWithLdap) {
return api.app.Ldap().GetUserAttributes(*user.AuthData, attributes)
}
return map[string]string{}, nil
}
func (api *PluginAPI) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
return api.app.CreateChannel(api.ctx, channel, false)
}
func (api *PluginAPI) DeleteChannel(channelID string) *model.AppError {
channel, err := api.app.GetChannel(api.ctx, channelID)
if err != nil {
return err
}
return api.app.DeleteChannel(api.ctx, channel, "")
}
func (api *PluginAPI) GetPublicChannelsForTeam(teamID string, page, perPage int) ([]*model.Channel, *model.AppError) {
channels, err := api.app.GetPublicChannelsForTeam(api.ctx, teamID, page*perPage, perPage)
if err != nil {
return nil, err
}
return channels, err
}
func (api *PluginAPI) GetChannel(channelID string) (*model.Channel, *model.AppError) {
return api.app.GetChannel(api.ctx, channelID)
}
func (api *PluginAPI) GetChannelByName(teamID, name string, includeDeleted bool) (*model.Channel, *model.AppError) {
return api.app.GetChannelByName(api.ctx, name, teamID, includeDeleted)
}
func (api *PluginAPI) GetChannelByNameForTeamName(teamName, channelName string, includeDeleted bool) (*model.Channel, *model.AppError) {
return api.app.GetChannelByNameForTeamName(api.ctx, channelName, teamName, includeDeleted)
}
func (api *PluginAPI) GetChannelsForTeamForUser(teamID, userID string, includeDeleted bool) ([]*model.Channel, *model.AppError) {
channels, err := api.app.GetChannelsForTeamForUser(api.ctx, teamID, userID, &model.ChannelSearchOpts{
IncludeDeleted: includeDeleted,
LastDeleteAt: 0,
})
if err != nil {
return nil, err
}
return channels, err
}
func (api *PluginAPI) GetChannelStats(channelID string) (*model.ChannelStats, *model.AppError) {
memberCount, err := api.app.GetChannelMemberCount(api.ctx, channelID)
if err != nil {
return nil, err
}
guestCount, err := api.app.GetChannelMemberCount(api.ctx, channelID)
if err != nil {
return nil, err
}
return &model.ChannelStats{ChannelId: channelID, MemberCount: memberCount, GuestCount: guestCount}, nil
}
func (api *PluginAPI) GetDirectChannel(userID1, userID2 string) (*model.Channel, *model.AppError) {
return api.app.GetOrCreateDirectChannel(api.ctx, userID1, userID2)
}
func (api *PluginAPI) GetGroupChannel(userIDs []string) (*model.Channel, *model.AppError) {
return api.app.CreateGroupChannel(api.ctx, userIDs, "")
}
func (api *PluginAPI) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
return api.app.UpdateChannel(api.ctx, channel)
}
func (api *PluginAPI) SearchChannels(teamID string, term string) ([]*model.Channel, *model.AppError) {
channels, err := api.app.SearchChannels(api.ctx, teamID, term)
if err != nil {
return nil, err
}
return channels, err
}
func (api *PluginAPI) CreateChannelSidebarCategory(userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) {
return api.app.CreateSidebarCategory(api.ctx, userID, teamID, newCategory)
}
func (api *PluginAPI) GetChannelSidebarCategories(userID, teamID string) (*model.OrderedSidebarCategories, *model.AppError) {
return api.app.GetSidebarCategoriesForTeamForUser(api.ctx, userID, teamID)
}
func (api *PluginAPI) UpdateChannelSidebarCategories(userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) {
return api.app.UpdateSidebarCategories(api.ctx, userID, teamID, categories)
}
func (api *PluginAPI) SearchUsers(search *model.UserSearch) ([]*model.User, *model.AppError) {
pluginSearchUsersOptions := &model.UserSearchOptions{
IsAdmin: true,
AllowInactive: search.AllowInactive,
Limit: search.Limit,
}
return api.app.SearchUsers(search, pluginSearchUsersOptions)
}
func (api *PluginAPI) SearchPostsInTeam(teamID string, paramsList []*model.SearchParams) ([]*model.Post, *model.AppError) {
postList, err := api.app.SearchPostsInTeam(teamID, paramsList)
if err != nil {
return nil, err
}
return postList.ForPlugin().ToSlice(), nil
}
func (api *PluginAPI) SearchPostsInTeamForUser(teamID string, userID string, searchParams model.SearchParameter) (*model.PostSearchResults, *model.AppError) {
var terms string
if searchParams.Terms != nil {
terms = *searchParams.Terms
}
timeZoneOffset := 0
if searchParams.TimeZoneOffset != nil {
timeZoneOffset = *searchParams.TimeZoneOffset
}
isOrSearch := false
if searchParams.IsOrSearch != nil {
isOrSearch = *searchParams.IsOrSearch
}
page := 0
if searchParams.Page != nil {
page = *searchParams.Page
}
perPage := 100
if searchParams.PerPage != nil {
perPage = *searchParams.PerPage
}
includeDeletedChannels := false
if searchParams.IncludeDeletedChannels != nil {
includeDeletedChannels = *searchParams.IncludeDeletedChannels
}
results, appErr := api.app.SearchPostsForUser(api.ctx, terms, userID, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, model.ModifierMessages)
if results != nil {
results = results.ForPlugin()
}
return results, appErr
}
func (api *PluginAPI) AddChannelMember(channelID, userID string) (*model.ChannelMember, *model.AppError) {
channel, err := api.GetChannel(channelID)
if err != nil {
return nil, err
}
return api.app.AddChannelMember(api.ctx, userID, channel, ChannelMemberOpts{
// For now, don't allow overriding these via the plugin API.
UserRequestorID: "",
PostRootID: "",
})
}
func (api *PluginAPI) AddUserToChannel(channelID, userID, asUserID string) (*model.ChannelMember, *model.AppError) {
channel, err := api.GetChannel(channelID)
if err != nil {
return nil, err
}
return api.app.AddChannelMember(api.ctx, userID, channel, ChannelMemberOpts{
UserRequestorID: asUserID,
})
}
func (api *PluginAPI) GetChannelMember(channelID, userID string) (*model.ChannelMember, *model.AppError) {
return api.app.GetChannelMember(api.ctx, channelID, userID)
}
func (api *PluginAPI) GetChannelMembers(channelID string, page, perPage int) (model.ChannelMembers, *model.AppError) {
return api.app.GetChannelMembersPage(api.ctx, channelID, page, perPage)
}
func (api *PluginAPI) GetChannelMembersByIds(channelID string, userIDs []string) (model.ChannelMembers, *model.AppError) {
return api.app.GetChannelMembersByIds(api.ctx, channelID, userIDs)
}
func (api *PluginAPI) GetChannelMembersForUser(_, userID string, page, perPage int) ([]*model.ChannelMember, *model.AppError) {
// The team ID parameter was never used in the SQL query.
// But we keep this to maintain compatibility.
return api.app.GetChannelMembersForUserWithPagination(api.ctx, userID, page, perPage)
}
func (api *PluginAPI) UpdateChannelMemberRoles(channelID, userID, newRoles string) (*model.ChannelMember, *model.AppError) {
return api.app.UpdateChannelMemberRoles(api.ctx, channelID, userID, newRoles)
}
func (api *PluginAPI) UpdateChannelMemberNotifications(channelID, userID string, notifications map[string]string) (*model.ChannelMember, *model.AppError) {
return api.app.UpdateChannelMemberNotifyProps(api.ctx, notifications, channelID, userID)
}
func (api *PluginAPI) DeleteChannelMember(channelID, userID string) *model.AppError {
return api.app.LeaveChannel(api.ctx, channelID, userID)
}
func (api *PluginAPI) GetGroup(groupId string) (*model.Group, *model.AppError) {
return api.app.GetGroup(groupId, nil, nil)
}
func (api *PluginAPI) GetGroupByName(name string) (*model.Group, *model.AppError) {
return api.app.GetGroupByName(name, model.GroupSearchOpts{})
}
func (api *PluginAPI) GetGroupMemberUsers(groupID string, page, perPage int) ([]*model.User, *model.AppError) {
users, _, err := api.app.GetGroupMemberUsersPage(groupID, page, perPage, nil)
return users, err
}
func (api *PluginAPI) GetGroupsBySource(groupSource model.GroupSource) ([]*model.Group, *model.AppError) {
return api.app.GetGroupsBySource(groupSource)
}
func (api *PluginAPI) GetGroupsForUser(userID string) ([]*model.Group, *model.AppError) {
return api.app.GetGroupsByUserId(userID)
}
func (api *PluginAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
post.AddProp("from_plugin", "true")
post, appErr := api.app.CreatePostMissingChannel(api.ctx, post, true, true)
if post != nil {
post = post.ForPlugin()
}
return post, appErr
}
func (api *PluginAPI) AddReaction(reaction *model.Reaction) (*model.Reaction, *model.AppError) {
return api.app.SaveReactionForPost(api.ctx, reaction)
}
func (api *PluginAPI) RemoveReaction(reaction *model.Reaction) *model.AppError {
return api.app.DeleteReactionForPost(api.ctx, reaction)
}
func (api *PluginAPI) GetReactions(postID string) ([]*model.Reaction, *model.AppError) {
return api.app.GetReactionsForPost(postID)
}
func (api *PluginAPI) SendEphemeralPost(userID string, post *model.Post) *model.Post {
return api.app.SendEphemeralPost(api.ctx, userID, post).ForPlugin()
}
func (api *PluginAPI) UpdateEphemeralPost(userID string, post *model.Post) *model.Post {
return api.app.UpdateEphemeralPost(api.ctx, userID, post).ForPlugin()
}
func (api *PluginAPI) DeleteEphemeralPost(userID, postID string) {
api.app.DeleteEphemeralPost(userID, postID)
}
func (api *PluginAPI) DeletePost(postID string) *model.AppError {
_, err := api.app.DeletePost(api.ctx, postID, api.id)
return err
}
func (api *PluginAPI) GetPostThread(postID string) (*model.PostList, *model.AppError) {
list, appErr := api.app.GetPostThread(postID, model.GetPostsOptions{}, "")
if list != nil {
list = list.ForPlugin()
}
return list, appErr
}
func (api *PluginAPI) GetPost(postID string) (*model.Post, *model.AppError) {
post, appErr := api.app.GetSinglePost(postID, false)
if post != nil {
post = post.ForPlugin()
}
return post, appErr
}
func (api *PluginAPI) GetPostsSince(channelID string, time int64) (*model.PostList, *model.AppError) {
list, appErr := api.app.GetPostsSince(model.GetPostsSinceOptions{ChannelId: channelID, Time: time})
if list != nil {
list = list.ForPlugin()
}
return list, appErr
}
func (api *PluginAPI) GetPostsAfter(channelID, postID string, page, perPage int) (*model.PostList, *model.AppError) {
list, appErr := api.app.GetPostsAfterPost(model.GetPostsOptions{ChannelId: channelID, PostId: postID, Page: page, PerPage: perPage})
if list != nil {
list = list.ForPlugin()
}
return list, appErr
}
func (api *PluginAPI) GetPostsBefore(channelID, postID string, page, perPage int) (*model.PostList, *model.AppError) {
list, appErr := api.app.GetPostsBeforePost(model.GetPostsOptions{ChannelId: channelID, PostId: postID, Page: page, PerPage: perPage})
if list != nil {
list = list.ForPlugin()
}
return list, appErr
}
func (api *PluginAPI) GetPostsForChannel(channelID string, page, perPage int) (*model.PostList, *model.AppError) {
list, appErr := api.app.GetPostsPage(model.GetPostsOptions{ChannelId: channelID, Page: page, PerPage: perPage})
if list != nil {
list = list.ForPlugin()
}
return list, appErr
}
func (api *PluginAPI) UpdatePost(post *model.Post) (*model.Post, *model.AppError) {
post, appErr := api.app.UpdatePost(api.ctx, post, false)
if post != nil {
post = post.ForPlugin()
}
return post, appErr
}
func (api *PluginAPI) GetProfileImage(userID string) ([]byte, *model.AppError) {
user, err := api.app.GetUser(userID)
if err != nil {
return nil, err
}
data, _, err := api.app.GetProfileImage(user)
return data, err
}
func (api *PluginAPI) SetProfileImage(userID string, data []byte) *model.AppError {
if _, err := api.app.GetUser(userID); err != nil {
return err
}
return api.app.SetProfileImageFromFile(api.ctx, userID, bytes.NewReader(data))
}
func (api *PluginAPI) GetEmojiList(sortBy string, page, perPage int) ([]*model.Emoji, *model.AppError) {
return api.app.GetEmojiList(api.ctx, page, perPage, sortBy)
}
func (api *PluginAPI) GetEmojiByName(name string) (*model.Emoji, *model.AppError) {
return api.app.GetEmojiByName(api.ctx, name)
}
func (api *PluginAPI) GetEmoji(emojiId string) (*model.Emoji, *model.AppError) {
return api.app.GetEmoji(api.ctx, emojiId)
}
func (api *PluginAPI) CopyFileInfos(userID string, fileIDs []string) ([]string, *model.AppError) {
return api.app.CopyFileInfos(userID, fileIDs)
}
func (api *PluginAPI) GetFileInfo(fileID string) (*model.FileInfo, *model.AppError) {
return api.app.GetFileInfo(fileID)
}
func (api *PluginAPI) GetFileInfos(page, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, *model.AppError) {
return api.app.GetFileInfos(page, perPage, opt)
}
func (api *PluginAPI) GetFileLink(fileID string) (string, *model.AppError) {
if !*api.app.Config().FileSettings.EnablePublicLink {
return "", model.NewAppError("GetFileLink", "plugin_api.get_file_link.disabled.app_error", nil, "", http.StatusNotImplemented)
}
info, err := api.app.GetFileInfo(fileID)
if err != nil {
return "", err
}
if info.PostId == "" {
return "", model.NewAppError("GetFileLink", "plugin_api.get_file_link.no_post.app_error", nil, "file_id="+info.Id, http.StatusBadRequest)
}
return api.app.GeneratePublicLink(api.app.GetSiteURL(), info), nil
}
func (api *PluginAPI) ReadFile(path string) ([]byte, *model.AppError) {
return api.app.ReadFile(path)
}
func (api *PluginAPI) GetFile(fileID string) ([]byte, *model.AppError) {
return api.app.GetFile(fileID)
}
func (api *PluginAPI) UploadFile(data []byte, channelID string, filename string) (*model.FileInfo, *model.AppError) {
return api.app.UploadFile(api.ctx, data, channelID, filename)
}
func (api *PluginAPI) GetEmojiImage(emojiId string) ([]byte, string, *model.AppError) {
return api.app.GetEmojiImage(api.ctx, emojiId)
}
func (api *PluginAPI) GetTeamIcon(teamID string) ([]byte, *model.AppError) {
team, err := api.app.GetTeam(teamID)
if err != nil {
return nil, err
}
data, err := api.app.GetTeamIcon(team)
if err != nil {
return nil, err
}
return data, nil
}
func (api *PluginAPI) SetTeamIcon(teamID string, data []byte) *model.AppError {
team, err := api.app.GetTeam(teamID)
if err != nil {
return err
}
return api.app.SetTeamIconFromFile(team, bytes.NewReader(data))
}
func (api *PluginAPI) OpenInteractiveDialog(dialog model.OpenDialogRequest) *model.AppError {
return api.app.OpenInteractiveDialog(dialog)
}
func (api *PluginAPI) RemoveTeamIcon(teamID string) *model.AppError {
_, err := api.app.GetTeam(teamID)
if err != nil {
return err
}
err = api.app.RemoveTeamIcon(teamID)
if err != nil {
return err
}
return nil
}
// Mail Section
func (api *PluginAPI) SendMail(to, subject, htmlBody string) *model.AppError {
if to == "" {
return model.NewAppError("SendMail", "plugin_api.send_mail.missing_to", nil, "", http.StatusBadRequest)
}
if subject == "" {
return model.NewAppError("SendMail", "plugin_api.send_mail.missing_subject", nil, "", http.StatusBadRequest)
}
if htmlBody == "" {
return model.NewAppError("SendMail", "plugin_api.send_mail.missing_htmlbody", nil, "", http.StatusBadRequest)
}
if err := api.app.Srv().EmailService.SendNotificationMail(to, subject, htmlBody); err != nil {
return model.NewAppError("SendMail", "plugin_api.send_mail.missing_htmlbody", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
// Plugin Section
func (api *PluginAPI) GetPlugins() ([]*model.Manifest, *model.AppError) {
plugins, err := api.app.GetPlugins()
if err != nil {
return nil, err
}
var manifests []*model.Manifest
for _, manifest := range plugins.Active {
manifests = append(manifests, &manifest.Manifest)
}
for _, manifest := range plugins.Inactive {
manifests = append(manifests, &manifest.Manifest)
}
return manifests, nil
}
func (api *PluginAPI) EnablePlugin(id string) *model.AppError {
return api.app.EnablePlugin(id)
}
func (api *PluginAPI) DisablePlugin(id string) *model.AppError {
return api.app.DisablePlugin(id)
}
func (api *PluginAPI) RemovePlugin(id string) *model.AppError {
return api.app.Channels().RemovePlugin(id)
}
func (api *PluginAPI) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) {
return api.app.GetPluginStatus(id)
}
func (api *PluginAPI) InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError) {
if !*api.app.Config().PluginSettings.Enable || !*api.app.Config().PluginSettings.EnableUploads {
return nil, model.NewAppError("installPlugin", "app.plugin.upload_disabled.app_error", nil, "", http.StatusNotImplemented)
}
fileBuffer, err := io.ReadAll(file)
if err != nil {
return nil, model.NewAppError("InstallPlugin", "api.plugin.upload.file.app_error", nil, "", http.StatusBadRequest)
}
return api.app.InstallPlugin(bytes.NewReader(fileBuffer), replace)
}
// KV Store Section
func (api *PluginAPI) KVSetWithOptions(key string, value []byte, options model.PluginKVSetOptions) (bool, *model.AppError) {
return api.app.SetPluginKeyWithOptions(api.id, key, value, options)
}
func (api *PluginAPI) KVSet(key string, value []byte) *model.AppError {
return api.app.SetPluginKey(api.id, key, value)
}
func (api *PluginAPI) KVCompareAndSet(key string, oldValue, newValue []byte) (bool, *model.AppError) {
return api.app.CompareAndSetPluginKey(api.id, key, oldValue, newValue)
}
func (api *PluginAPI) KVCompareAndDelete(key string, oldValue []byte) (bool, *model.AppError) {
return api.app.CompareAndDeletePluginKey(api.id, key, oldValue)
}
func (api *PluginAPI) KVSetWithExpiry(key string, value []byte, expireInSeconds int64) *model.AppError {
return api.app.SetPluginKeyWithExpiry(api.id, key, value, expireInSeconds)
}
func (api *PluginAPI) KVGet(key string) ([]byte, *model.AppError) {
return api.app.GetPluginKey(api.id, key)
}
func (api *PluginAPI) KVDelete(key string) *model.AppError {
return api.app.DeletePluginKey(api.id, key)
}
func (api *PluginAPI) KVDeleteAll() *model.AppError {
return api.app.DeleteAllKeysForPlugin(api.id)
}
func (api *PluginAPI) KVList(page, perPage int) ([]string, *model.AppError) {
return api.app.ListPluginKeys(api.id, page, perPage)
}
func (api *PluginAPI) PublishWebSocketEvent(event string, payload map[string]any, broadcast *model.WebsocketBroadcast) {
ev := model.NewWebSocketEvent(fmt.Sprintf("custom_%v_%v", api.id, event), "", "", "", nil, "")
ev = ev.SetBroadcast(broadcast).SetData(payload)
api.app.Publish(ev)
}
func (api *PluginAPI) HasPermissionTo(userID string, permission *model.Permission) bool {
return api.app.HasPermissionTo(userID, permission)
}
func (api *PluginAPI) HasPermissionToTeam(userID, teamID string, permission *model.Permission) bool {
return api.app.HasPermissionToTeam(userID, teamID, permission)
}
func (api *PluginAPI) HasPermissionToChannel(userID, channelID string, permission *model.Permission) bool {
return api.app.HasPermissionToChannel(api.ctx, userID, channelID, permission)
}
func (api *PluginAPI) RolesGrantPermission(roleNames []string, permissionId string) bool {
return api.app.RolesGrantPermission(roleNames, permissionId)
}
func (api *PluginAPI) LogDebug(msg string, keyValuePairs ...any) {
api.logger.Debugw(msg, keyValuePairs...)
}
func (api *PluginAPI) LogInfo(msg string, keyValuePairs ...any) {
api.logger.Infow(msg, keyValuePairs...)
}
func (api *PluginAPI) LogError(msg string, keyValuePairs ...any) {
api.logger.Errorw(msg, keyValuePairs...)
}
func (api *PluginAPI) LogWarn(msg string, keyValuePairs ...any) {
api.logger.Warnw(msg, keyValuePairs...)
}
func (api *PluginAPI) CreateBot(bot *model.Bot) (*model.Bot, *model.AppError) {
// Bots created by a plugin should use the plugin's ID for the creator field, unless
// otherwise specified by the plugin.
if bot.OwnerId == "" {
bot.OwnerId = api.id
}
// Bots cannot be owners of other bots
if user, err := api.app.GetUser(bot.OwnerId); err == nil {
if user.IsBot {
return nil, model.NewAppError("CreateBot", "plugin_api.bot_cant_create_bot", nil, "", http.StatusBadRequest)
}
}
return api.app.CreateBot(api.ctx, bot)
}
func (api *PluginAPI) PatchBot(userID string, botPatch *model.BotPatch) (*model.Bot, *model.AppError) {
return api.app.PatchBot(userID, botPatch)
}
func (api *PluginAPI) GetBot(userID string, includeDeleted bool) (*model.Bot, *model.AppError) {
return api.app.GetBot(userID, includeDeleted)
}
func (api *PluginAPI) GetBots(options *model.BotGetOptions) ([]*model.Bot, *model.AppError) {
bots, err := api.app.GetBots(options)
return []*model.Bot(bots), err
}
func (api *PluginAPI) UpdateBotActive(userID string, active bool) (*model.Bot, *model.AppError) {
return api.app.UpdateBotActive(api.ctx, userID, active)
}
func (api *PluginAPI) PermanentDeleteBot(userID string) *model.AppError {
return api.app.PermanentDeleteBot(userID)
}
func (api *PluginAPI) EnsureBotUser(bot *model.Bot) (string, error) {
// Bots created by a plugin should use the plugin's ID for the creator field.
bot.OwnerId = api.id
return api.app.EnsureBot(api.ctx, api.id, bot)
}
func (api *PluginAPI) PublishUserTyping(userID, channelID, parentId string) *model.AppError {
return api.app.PublishUserTyping(userID, channelID, parentId)
}
func (api *PluginAPI) PluginHTTP(request *http.Request) *http.Response {
split := strings.SplitN(request.URL.Path, "/", 3)
if len(split) != 3 {
return &http.Response{
StatusCode: http.StatusBadRequest,
Body: io.NopCloser(bytes.NewBufferString("Not enough URL. Form of URL should be /<pluginid>/*")),
}
}
destinationPluginId := split[1]
newURL, err := url.Parse("/" + split[2])
newURL.RawQuery = request.URL.Query().Encode()
request.URL = newURL
if destinationPluginId == "" || err != nil {
message := "No plugin specified. Form of URL should be /<pluginid>/*"
if err != nil {
message = "Form of URL should be /<pluginid>/* Error: " + err.Error()
}
return &http.Response{
StatusCode: http.StatusBadRequest,
Body: io.NopCloser(bytes.NewBufferString(message)),
}
}
responseTransfer := &PluginResponseWriter{}
api.app.ServeInterPluginRequest(responseTransfer, request, api.id, destinationPluginId)
return responseTransfer.GenerateResponse()
}
func (api *PluginAPI) CreateCommand(cmd *model.Command) (*model.Command, error) {
cmd.CreatorId = ""
cmd.PluginId = api.id
cmd, appErr := api.app.createCommand(cmd)
if appErr != nil {
return cmd, appErr
}
return cmd, nil
}
func (api *PluginAPI) ListCommands(teamID string) ([]*model.Command, error) {
ret := make([]*model.Command, 0)
cmds, err := api.ListPluginCommands(teamID)
if err != nil {
return nil, err
}
ret = append(ret, cmds...)
cmds, err = api.ListBuiltInCommands()
if err != nil {
return nil, err
}
ret = append(ret, cmds...)
cmds, err = api.ListCustomCommands(teamID)
if err != nil {
return nil, err
}
ret = append(ret, cmds...)
return ret, nil
}
func (api *PluginAPI) ListCustomCommands(teamID string) ([]*model.Command, error) {
// Plugins are allowed to bypass the a.Config().ServiceSettings.EnableCommands setting.
return api.app.Srv().Store().Command().GetByTeam(teamID)
}
func (api *PluginAPI) ListPluginCommands(teamID string) ([]*model.Command, error) {
commands := make([]*model.Command, 0)
seen := make(map[string]bool)
for _, cmd := range api.app.CommandsForTeam(teamID) {
if !seen[cmd.Trigger] {
seen[cmd.Trigger] = true
commands = append(commands, cmd)
}
}
return commands, nil
}
func (api *PluginAPI) ListBuiltInCommands() ([]*model.Command, error) {
commands := make([]*model.Command, 0)
seen := make(map[string]bool)
for _, value := range commandProviders {
if cmd := value.GetCommand(api.app, i18n.T); cmd != nil {
cpy := *cmd
if cpy.AutoComplete && !seen[cpy.Trigger] {
cpy.Sanitize()
seen[cpy.Trigger] = true
commands = append(commands, &cpy)
}
}
}
return commands, nil
}
func (api *PluginAPI) GetCommand(commandID string) (*model.Command, error) {
return api.app.Srv().Store().Command().Get(commandID)
}
func (api *PluginAPI) UpdateCommand(commandID string, updatedCmd *model.Command) (*model.Command, error) {
oldCmd, err := api.GetCommand(commandID)
if err != nil {
return nil, err
}
updatedCmd.Trigger = strings.ToLower(updatedCmd.Trigger)
updatedCmd.Id = oldCmd.Id
updatedCmd.Token = oldCmd.Token
updatedCmd.CreateAt = oldCmd.CreateAt
updatedCmd.UpdateAt = model.GetMillis()
updatedCmd.DeleteAt = oldCmd.DeleteAt
updatedCmd.PluginId = api.id
if updatedCmd.TeamId == "" {
updatedCmd.TeamId = oldCmd.TeamId
}
return api.app.Srv().Store().Command().Update(updatedCmd)
}
func (api *PluginAPI) DeleteCommand(commandID string) error {
err := api.app.Srv().Store().Command().Delete(commandID, model.GetMillis())
if err != nil {
return err
}
return nil
}
func (api *PluginAPI) CreateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
return api.app.CreateOAuthApp(app)
}
func (api *PluginAPI) GetOAuthApp(appID string) (*model.OAuthApp, *model.AppError) {
return api.app.GetOAuthApp(appID)
}
func (api *PluginAPI) UpdateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
oldApp, err := api.GetOAuthApp(app.Id)
if err != nil {
return nil, err
}
return api.app.UpdateOAuthApp(oldApp, app)
}
func (api *PluginAPI) DeleteOAuthApp(appID string) *model.AppError {
return api.app.DeleteOAuthApp(appID)
}
// PublishPluginClusterEvent broadcasts a plugin event to all other running instances of
// the calling plugin.
func (api *PluginAPI) PublishPluginClusterEvent(ev model.PluginClusterEvent,
opts model.PluginClusterEventSendOptions) error {
if api.app.Cluster() == nil {
return nil
}
msg := &model.ClusterMessage{
Event: model.ClusterEventPluginEvent,
SendType: opts.SendType,
WaitForAllToSend: false,
Props: map[string]string{
"PluginID": api.id,
"EventID": ev.Id,
},
Data: ev.Data,
}
// If TargetId is empty we broadcast to all other cluster nodes.
if opts.TargetId == "" {
api.app.Cluster().SendClusterMessage(msg)
} else {
if err := api.app.Cluster().SendClusterMessageToNode(opts.TargetId, msg); err != nil {
return fmt.Errorf("failed to send message to cluster node %q: %w", opts.TargetId, err)
}
}
return nil
}
// RequestTrialLicense requests a trial license and installs it in the server
func (api *PluginAPI) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError {
if *api.app.Config().ExperimentalSettings.RestrictSystemAdmin {
return model.NewAppError("RequestTrialLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden)
}
return api.app.Channels().RequestTrialLicense(requesterID, users, termsAccepted, receiveEmailsAccepted)
}
// GetCloudLimits returns any limits associated with the cloud instance
func (api *PluginAPI) GetCloudLimits() (*model.ProductLimits, error) {
if api.app.Cloud() == nil {
return &model.ProductLimits{}, nil
}
limits, err := api.app.Cloud().GetCloudLimits("")
return limits, err
}
// RegisterCollectionAndTopic informs the server that this plugin handles
// the given collection and topic types.
func (api *PluginAPI) RegisterCollectionAndTopic(collectionType, topicType string) error {
return api.app.RegisterCollectionAndTopic(api.id, collectionType, topicType)
}
func (api *PluginAPI) CreateUploadSession(us *model.UploadSession) (*model.UploadSession, error) {
us, err := api.app.CreateUploadSession(api.ctx, us)
if err != nil {
return nil, err
}
return us, nil
}
func (api *PluginAPI) UploadData(us *model.UploadSession, rd io.Reader) (*model.FileInfo, error) {
fi, err := api.app.UploadData(api.ctx, us, rd)
if err != nil {
return nil, err
}
return fi, nil
}
func (api *PluginAPI) GetUploadSession(uploadID string) (*model.UploadSession, error) {
// We want to fetch from master DB to avoid a potential read-after-write on the plugin side.
api.ctx.SetContext(WithMaster(api.ctx.Context()))
fi, err := api.app.GetUploadSession(api.ctx, uploadID)
if err != nil {
return nil, err
}
return fi, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"fmt"
"net/http"
"net/url"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type PluginCommand struct {
Command *model.Command
PluginId string
}
func (a *App) RegisterPluginCommand(pluginID string, command *model.Command) error {
if command.Trigger == "" {
return errors.New("invalid command")
}
if command.AutocompleteData != nil {
if err := command.AutocompleteData.IsValid(); err != nil {
return errors.Wrap(err, "invalid autocomplete data in command")
}
}
if command.AutocompleteData == nil {
command.AutocompleteData = model.NewAutocompleteData(command.Trigger, command.AutoCompleteHint, command.AutoCompleteDesc)
} else {
baseURL, err := url.Parse("/plugins/" + pluginID)
if err != nil {
return errors.Wrapf(err, "Can't parse url %s", "/plugins/"+pluginID)
}
err = command.AutocompleteData.UpdateRelativeURLsForPluginCommands(baseURL)
if err != nil {
return errors.Wrap(err, "Can't update relative urls for plugin commands")
}
}
command = &model.Command{
Trigger: strings.ToLower(command.Trigger),
TeamId: command.TeamId,
AutoComplete: command.AutoComplete,
AutoCompleteDesc: command.AutoCompleteDesc,
AutoCompleteHint: command.AutoCompleteHint,
DisplayName: command.DisplayName,
AutocompleteData: command.AutocompleteData,
AutocompleteIconData: command.AutocompleteIconData,
}
a.ch.pluginCommandsLock.Lock()
defer a.ch.pluginCommandsLock.Unlock()
for _, pc := range a.ch.pluginCommands {
if pc.Command.Trigger == command.Trigger && pc.Command.TeamId == command.TeamId {
if pc.PluginId == pluginID {
pc.Command = command
return nil
}
}
}
a.ch.pluginCommands = append(a.ch.pluginCommands, &PluginCommand{
Command: command,
PluginId: pluginID,
})
return nil
}
func (a *App) UnregisterPluginCommand(pluginID, teamID, trigger string) {
trigger = strings.ToLower(trigger)
a.ch.pluginCommandsLock.Lock()
defer a.ch.pluginCommandsLock.Unlock()
var remaining []*PluginCommand
for _, pc := range a.ch.pluginCommands {
if pc.Command.TeamId != teamID || pc.Command.Trigger != trigger {
remaining = append(remaining, pc)
}
}
a.ch.pluginCommands = remaining
}
func (ch *Channels) unregisterPluginCommands(pluginID string) {
ch.pluginCommandsLock.Lock()
defer ch.pluginCommandsLock.Unlock()
var remaining []*PluginCommand
for _, pc := range ch.pluginCommands {
if pc.PluginId != pluginID {
remaining = append(remaining, pc)
}
}
ch.pluginCommands = remaining
}
// CommandsForTeam returns all the plugin and product commands for the given team.
func (a *App) CommandsForTeam(teamID string) []*model.Command {
var commands []*model.Command
a.ch.pluginCommandsLock.RLock()
defer a.ch.pluginCommandsLock.RUnlock()
for _, pc := range a.ch.pluginCommands {
if pc.Command.TeamId == "" || pc.Command.TeamId == teamID {
commands = append(commands, pc.Command)
}
}
a.ch.productCommandsLock.RLock()
defer a.ch.productCommandsLock.RUnlock()
for _, pc := range a.ch.productCommands {
if pc.Command.TeamId == "" || pc.Command.TeamId == teamID {
commands = append(commands, pc.Command)
}
}
return commands
}
// tryExecutePluginCommand attempts to run a command provided by a plugin based on the given arguments. If no such
// command can be found, returns nil for all arguments.
func (a *App) tryExecutePluginCommand(c request.CTX, args *model.CommandArgs) (*model.Command, *model.CommandResponse, *model.AppError) {
parts := strings.Split(args.Command, " ")
trigger := parts[0][1:]
trigger = strings.ToLower(trigger)
var matched *PluginCommand
a.ch.pluginCommandsLock.RLock()
for _, pc := range a.ch.pluginCommands {
if (pc.Command.TeamId == "" || pc.Command.TeamId == args.TeamId) && pc.Command.Trigger == trigger {
matched = pc
break
}
}
a.ch.pluginCommandsLock.RUnlock()
if matched == nil {
return nil, nil, nil
}
pluginsEnvironment := a.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return nil, nil, nil
}
// Checking if plugin is working or not
if err := pluginsEnvironment.PerformHealthCheck(matched.PluginId); err != nil {
return matched.Command, nil, model.NewAppError("ExecutePluginCommand", "model.plugin_command_error.error.app_error", map[string]any{"Command": trigger}, "err= Plugin has recently crashed: "+matched.PluginId, http.StatusInternalServerError)
}
pluginHooks, err := pluginsEnvironment.HooksForPlugin(matched.PluginId)
if err != nil {
return matched.Command, nil, model.NewAppError("ExecutePluginCommand", "model.plugin_command.error.app_error", nil, "err="+err.Error(), http.StatusInternalServerError)
}
for username, userID := range a.MentionsToTeamMembers(c, args.Command, args.TeamId) {
args.AddUserMention(username, userID)
}
for channelName, channelID := range a.MentionsToPublicChannels(c, args.Command, args.TeamId) {
args.AddChannelMention(channelName, channelID)
}
response, appErr := pluginHooks.ExecuteCommand(pluginContext(c), args)
// Checking if plugin crashed after running the command
if err := pluginsEnvironment.PerformHealthCheck(matched.PluginId); err != nil {
errMessage := fmt.Sprintf("err= Plugin %s crashed due to /%s command", matched.PluginId, trigger)
return matched.Command, nil, model.NewAppError("ExecutePluginCommand", "model.plugin_command_crash.error.app_error", map[string]any{"Command": trigger, "PluginId": matched.PluginId}, errMessage, http.StatusInternalServerError)
}
// This is a response from the plugin, which may set an incorrect status code;
// e.g setting a status code of 0 will crash the server. So we always bucket everything under 500.
if appErr != nil && (appErr.StatusCode < 100 || appErr.StatusCode > 999) {
mlog.Warn("Invalid status code returned from plugin. Converting to internal server error.", mlog.String("plugin_id", matched.PluginId), mlog.Int("status_code", appErr.StatusCode))
appErr.StatusCode = http.StatusInternalServerError
}
return matched.Command, response, appErr
}
// Support for slash commands to MPA
//
// Key differences/points with plugin commands:
// - There's no need of health checks or unregisterProductCommands on products, they are compiled and assumed as active server side
// - HooksForProduct still returns a plugin.Hooks struct, it might make sense to improve the name/package
// - Plugin code had a check for a plugin crash after a command was executed, that has been omitted for products
type ProductCommand struct {
Command *model.Command
ProductID string
}
func (a *App) RegisterProductCommand(ProductID string, command *model.Command) error {
if command.Trigger == "" {
return errors.New("invalid command")
}
if command.AutocompleteData != nil {
if err := command.AutocompleteData.IsValid(); err != nil {
return errors.Wrap(err, "invalid autocomplete data in command")
}
}
if command.AutocompleteData == nil {
command.AutocompleteData = model.NewAutocompleteData(command.Trigger, command.AutoCompleteHint, command.AutoCompleteDesc)
} else {
baseURL, err := url.Parse("/plugins/" + ProductID)
if err != nil {
return errors.Wrapf(err, "Can't parse url %s", "/plugins/"+ProductID)
}
err = command.AutocompleteData.UpdateRelativeURLsForPluginCommands(baseURL)
if err != nil {
return errors.Wrap(err, "Can't update relative urls for plugin commands")
}
}
command = &model.Command{
Trigger: strings.ToLower(command.Trigger),
TeamId: command.TeamId,
AutoComplete: command.AutoComplete,
AutoCompleteDesc: command.AutoCompleteDesc,
AutoCompleteHint: command.AutoCompleteHint,
DisplayName: command.DisplayName,
AutocompleteData: command.AutocompleteData,
AutocompleteIconData: command.AutocompleteIconData,
}
a.ch.productCommandsLock.Lock()
defer a.ch.productCommandsLock.Unlock()
for _, pc := range a.ch.productCommands {
if pc.Command.Trigger == command.Trigger && pc.Command.TeamId == command.TeamId {
if pc.ProductID == ProductID {
pc.Command = command
return nil
}
}
}
a.ch.productCommands = append(a.ch.productCommands, &ProductCommand{
Command: command,
ProductID: ProductID,
})
return nil
}
// tryExecuteProductCommand attempts to run a command provided by a product based on the given arguments. If no such
// command can be found, returns nil for all arguments.
func (a *App) tryExecuteProductCommand(c request.CTX, args *model.CommandArgs) (*model.Command, *model.CommandResponse, *model.AppError) {
parts := strings.Split(args.Command, " ")
trigger := parts[0][1:]
trigger = strings.ToLower(trigger)
var matched *ProductCommand
a.ch.productCommandsLock.RLock()
for _, pc := range a.ch.productCommands {
if (pc.Command.TeamId == "" || pc.Command.TeamId == args.TeamId) && pc.Command.Trigger == trigger {
matched = pc
break
}
}
a.ch.productCommandsLock.RUnlock()
if matched == nil {
return nil, nil, nil
}
// The type returned is still plugin.Hooks, could make sense in the future to move Hooks
// to another package or change the abstraction
productHooks := a.HooksManager().HooksForProduct(matched.ProductID)
if productHooks == nil {
return matched.Command, nil, model.NewAppError("ExecutePropductCommand", "model.plugin_command.error.app_error", nil, "", http.StatusInternalServerError)
}
for username, userID := range a.MentionsToTeamMembers(c, args.Command, args.TeamId) {
args.AddUserMention(username, userID)
}
for channelName, channelID := range a.MentionsToPublicChannels(c, args.Command, args.TeamId) {
args.AddChannelMention(channelName, channelID)
}
response, appErr := productHooks.ExecuteCommand(pluginContext(c), args)
// This is a response from the product, which may set an incorrect status code;
// e.g setting a status code of 0 will crash the server. So we always bucket everything under 500.
if appErr != nil && (appErr.StatusCode < 100 || appErr.StatusCode > 999) {
mlog.Warn("Invalid status code returned from plugin. Converting to internal server error.", mlog.String("plugin_id", matched.ProductID), mlog.Int("status_code", appErr.StatusCode))
appErr.StatusCode = http.StatusInternalServerError
}
return matched.Command, response, appErr
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"database/sql"
"database/sql/driver"
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
)
// DriverImpl implements the plugin.Driver interface on the server-side.
// Each new request for a connection/statement/transaction etc, generates
// a new entry tracked centrally in a map. Further requests operate on the
// object ID.
type DriverImpl struct {
s *Server
connMut sync.RWMutex
connMap map[string]*sql.Conn
txMut sync.Mutex
txMap map[string]driver.Tx
stMut sync.RWMutex
stMap map[string]driver.Stmt
rowsMut sync.RWMutex
rowsMap map[string]driver.Rows
}
func NewDriverImpl(s *Server) *DriverImpl {
return &DriverImpl{
s: s,
connMap: make(map[string]*sql.Conn),
txMap: make(map[string]driver.Tx),
stMap: make(map[string]driver.Stmt),
rowsMap: make(map[string]driver.Rows),
}
}
func (d *DriverImpl) Conn(isMaster bool) (string, error) {
dbFunc := d.s.Platform().Store.GetInternalMasterDB
if !isMaster {
dbFunc = d.s.Platform().Store.GetInternalReplicaDB
}
timeout := time.Duration(*d.s.Config().SqlSettings.QueryTimeout) * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
conn, err := dbFunc().Conn(ctx)
if err != nil {
return "", err
}
connID := model.NewId()
d.connMut.Lock()
d.connMap[connID] = conn
d.connMut.Unlock()
return connID, nil
}
// According to https://golang.org/pkg/database/sql/#Conn, a client can call
// Close on a connection, concurrently while running a query.
//
// Therefore, we have to handle the case where the connection is no longer
// present in the map because it has been closed. ErrBadConn is a good choice
// here which indicates the sql package to retry on a new connection.
//
// ConnPing, ConnQuery, ConnClose, Tx, and Stmt do this.
func (d *DriverImpl) ConnPing(connID string) error {
d.connMut.RLock()
conn, ok := d.connMap[connID]
d.connMut.RUnlock()
if !ok {
return driver.ErrBadConn
}
return conn.Raw(func(innerConn any) error {
return innerConn.(driver.Pinger).Ping(context.Background())
})
}
func (d *DriverImpl) ConnQuery(connID, q string, args []driver.NamedValue) (_ string, err error) {
var rows driver.Rows
d.connMut.RLock()
conn, ok := d.connMap[connID]
d.connMut.RUnlock()
if !ok {
return "", driver.ErrBadConn
}
err = conn.Raw(func(innerConn any) error {
rows, err = innerConn.(driver.QueryerContext).QueryContext(context.Background(), q, args)
return err
})
if err != nil {
return "", err
}
rowsID := model.NewId()
d.rowsMut.Lock()
d.rowsMap[rowsID] = rows
d.rowsMut.Unlock()
return rowsID, nil
}
func (d *DriverImpl) ConnExec(connID, q string, args []driver.NamedValue) (_ plugin.ResultContainer, err error) {
var res driver.Result
var ret plugin.ResultContainer
d.connMut.RLock()
conn, ok := d.connMap[connID]
d.connMut.RUnlock()
if !ok {
return ret, driver.ErrBadConn
}
err = conn.Raw(func(innerConn any) error {
res, err = innerConn.(driver.ExecerContext).ExecContext(context.Background(), q, args)
return err
})
if err != nil {
return ret, err
}
ret.LastID, ret.LastIDError = res.LastInsertId()
ret.RowsAffected, ret.RowsAffectedError = res.RowsAffected()
return ret, nil
}
func (d *DriverImpl) ConnClose(connID string) error {
d.connMut.Lock()
conn, ok := d.connMap[connID]
if !ok {
d.connMut.Unlock()
return driver.ErrBadConn
}
delete(d.connMap, connID)
d.connMut.Unlock()
return conn.Close()
}
func (d *DriverImpl) Tx(connID string, opts driver.TxOptions) (_ string, err error) {
var tx driver.Tx
d.connMut.RLock()
conn, ok := d.connMap[connID]
d.connMut.RUnlock()
if !ok {
return "", driver.ErrBadConn
}
err = conn.Raw(func(innerConn any) error {
tx, err = innerConn.(driver.ConnBeginTx).BeginTx(context.Background(), opts)
return err
})
if err != nil {
return "", err
}
txID := model.NewId()
d.txMut.Lock()
d.txMap[txID] = tx
d.txMut.Unlock()
return txID, nil
}
func (d *DriverImpl) TxCommit(txID string) error {
d.txMut.Lock()
tx := d.txMap[txID]
delete(d.txMap, txID)
d.txMut.Unlock()
return tx.Commit()
}
func (d *DriverImpl) TxRollback(txID string) error {
d.txMut.Lock()
tx := d.txMap[txID]
delete(d.txMap, txID)
d.txMut.Unlock()
return tx.Rollback()
}
func (d *DriverImpl) Stmt(connID, q string) (_ string, err error) {
var stmt driver.Stmt
d.connMut.RLock()
conn, ok := d.connMap[connID]
d.connMut.RUnlock()
if !ok {
return "", driver.ErrBadConn
}
err = conn.Raw(func(innerConn any) error {
stmt, err = innerConn.(driver.Conn).Prepare(q)
return err
})
if err != nil {
return "", err
}
stID := model.NewId()
d.stMut.Lock()
d.stMap[stID] = stmt
d.stMut.Unlock()
return stID, nil
}
func (d *DriverImpl) StmtClose(stID string) error {
d.stMut.Lock()
err := d.stMap[stID].Close()
delete(d.stMap, stID)
d.stMut.Unlock()
return err
}
func (d *DriverImpl) StmtNumInput(stID string) int {
d.stMut.RLock()
defer d.stMut.RUnlock()
return d.stMap[stID].NumInput()
}
func (d *DriverImpl) StmtQuery(stID string, args []driver.NamedValue) (string, error) {
argVals := make([]driver.Value, len(args))
for i, a := range args {
argVals[i] = a.Value
}
d.stMut.RLock()
st := d.stMap[stID]
d.stMut.RUnlock()
rows, err := st.Query(argVals) //nolint:staticcheck
if err != nil {
return "", err
}
rowsID := model.NewId()
d.rowsMut.Lock()
d.rowsMap[rowsID] = rows
d.rowsMut.Unlock()
return rowsID, nil
}
func (d *DriverImpl) StmtExec(stID string, args []driver.NamedValue) (plugin.ResultContainer, error) {
argVals := make([]driver.Value, len(args))
for i, a := range args {
argVals[i] = a.Value
}
var ret plugin.ResultContainer
d.stMut.RLock()
st := d.stMap[stID]
d.stMut.RUnlock()
res, err := st.Exec(argVals) //nolint:staticcheck
if err != nil {
return ret, err
}
ret.LastID, ret.LastIDError = res.LastInsertId()
ret.RowsAffected, ret.RowsAffectedError = res.RowsAffected()
return ret, nil
}
func (d *DriverImpl) RowsColumns(rowsID string) []string {
d.rowsMut.RLock()
defer d.rowsMut.RUnlock()
return d.rowsMap[rowsID].Columns()
}
func (d *DriverImpl) RowsClose(rowsID string) error {
d.rowsMut.Lock()
defer d.rowsMut.Unlock()
err := d.rowsMap[rowsID].Close()
delete(d.rowsMap, rowsID)
return err
}
func (d *DriverImpl) RowsNext(rowsID string, dest []driver.Value) error {
d.rowsMut.RLock()
rows := d.rowsMap[rowsID]
d.rowsMut.RUnlock()
return rows.Next(dest)
}
func (d *DriverImpl) RowsHasNextResultSet(rowsID string) bool {
d.rowsMut.RLock()
defer d.rowsMut.RUnlock()
return d.rowsMap[rowsID].(driver.RowsNextResultSet).HasNextResultSet()
}
func (d *DriverImpl) RowsNextResultSet(rowsID string) error {
d.rowsMut.RLock()
defer d.rowsMut.RUnlock()
return d.rowsMap[rowsID].(driver.RowsNextResultSet).NextResultSet()
}
func (d *DriverImpl) RowsColumnTypeDatabaseTypeName(rowsID string, index int) string {
d.rowsMut.RLock()
defer d.rowsMut.RUnlock()
return d.rowsMap[rowsID].(driver.RowsColumnTypeDatabaseTypeName).ColumnTypeDatabaseTypeName(index)
}
func (d *DriverImpl) RowsColumnTypePrecisionScale(rowsID string, index int) (int64, int64, bool) {
d.rowsMut.RLock()
defer d.rowsMut.RUnlock()
return d.rowsMap[rowsID].(driver.RowsColumnTypePrecisionScale).ColumnTypePrecisionScale(index)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"github.com/mattermost/mattermost-server/v6/model"
)
func (ch *Channels) notifyClusterPluginEvent(event model.ClusterEvent, data model.PluginEventData) {
buf, _ := json.Marshal(data)
if ch.srv.platform.Cluster() != nil {
ch.srv.platform.Cluster().SendClusterMessage(&model.ClusterMessage{
Event: event,
SendType: model.ClusterSendReliable,
WaitForAllToSend: true,
Data: buf,
})
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Installing a managed plugin consists of copying the uploaded plugin (*.tar.gz) to the filestore,
// unpacking to the configured local directory (PluginSettings.Directory), and copying any webapp bundle therein
// to the configured local client directory (PluginSettings.ClientDirectory). The unpacking and copy occurs
// each time the server starts, ensuring it remains synchronized with the set of installed plugins.
//
// When a plugin is enabled, all connected websocket clients are notified so as to fetch any webapp bundle and
// load the client-side portion of the plugin. This works well in a single-server system, but requires careful
// coordination in a high-availability cluster with multiple servers. In particular, websocket clients must not be
// notified of the newly enabled plugin until all servers in the cluster have finished unpacking the plugin, otherwise
// the webapp bundle might not yet be available. Ideally, each server would just notify its own set of connected peers
// after it finishes this process, but nothing prevents those clients from re-connecting to a different server behind
// the load balancer that hasn't finished unpacking.
//
// To achieve this coordination, each server instead checks the status of its peers after unpacking. If it finds peers with
// differing versions of the plugin, it skips the notification. If it finds all peers with the same version of the plugin,
// it notifies all websocket clients connected to all peers. There's a small chance that this never occurs if the last
// server to finish unpacking dies before it can announce. There is also a chance that multiple servers decide to notify,
// but the webapp handles this idempotently.
//
// Complicating this flow further are the various means of notifying. In addition to websocket events, there are cluster
// messages between peers. There is a cluster message when the config changes and a plugin is enabled or disabled.
// There is a cluster message when installing or uninstalling a plugin. There is a cluster message when peer's plugin change
// its status. And finally the act of notifying websocket clients is propagated itself via a cluster message.
//
// The key methods involved in handling these notifications are notifyPluginEnabled and notifyPluginStatusesChanged.
// Note that none of this complexity applies to single-server systems or to plugins without a webapp bundle.
//
// Finally, in addition to managed plugins, note that there are unmanaged and prepackaged plugins.
// Unmanaged plugins are plugins installed manually to the configured local directory (PluginSettings.Directory).
// Prepackaged plugins are included with the server. They otherwise follow the above flow, except do not get uploaded
// to the filestore. Prepackaged plugins override all other plugins with the same plugin id, but only when the prepackaged
// plugin is newer. Managed plugins unconditionally override unmanaged plugins with the same plugin id.
package app
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"github.com/blang/semver"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// managedPluginFileName is the file name of the flag file that marks
// a local plugin folder as "managed" by the file store.
const managedPluginFileName = ".filestore"
// fileStorePluginFolder is the folder name in the file store of the plugin bundles installed.
const fileStorePluginFolder = "plugins"
func (ch *Channels) installPluginFromData(data model.PluginEventData) {
mlog.Debug("Installing plugin as per cluster message", mlog.String("plugin_id", data.Id))
pluginSignaturePathMap, appErr := ch.getPluginsFromFolder()
if appErr != nil {
mlog.Error("Failed to get plugin signatures from filestore. Can't install plugin from data.", mlog.Err(appErr))
return
}
plugin, ok := pluginSignaturePathMap[data.Id]
if !ok {
mlog.Error("Failed to get plugin signature from filestore. Can't install plugin from data.", mlog.String("plugin id", data.Id))
return
}
reader, appErr := ch.srv.fileReader(plugin.path)
if appErr != nil {
mlog.Error("Failed to open plugin bundle from file store.", mlog.String("bundle", plugin.path), mlog.Err(appErr))
return
}
defer reader.Close()
var signature filestore.ReadCloseSeeker
if *ch.cfgSvc.Config().PluginSettings.RequirePluginSignature {
signature, appErr = ch.srv.fileReader(plugin.signaturePath)
if appErr != nil {
mlog.Error("Failed to open plugin signature from file store.", mlog.Err(appErr))
return
}
defer signature.Close()
}
manifest, appErr := ch.installPluginLocally(reader, signature, installPluginLocallyAlways)
if appErr != nil {
mlog.Error("Failed to sync plugin from file store", mlog.String("bundle", plugin.path), mlog.Err(appErr))
return
}
if err := ch.notifyPluginEnabled(manifest); err != nil {
mlog.Error("Failed notify plugin enabled", mlog.Err(err))
}
if err := ch.notifyPluginStatusesChanged(); err != nil {
mlog.Error("Failed to notify plugin status changed", mlog.Err(err))
}
}
func (ch *Channels) removePluginFromData(data model.PluginEventData) {
mlog.Debug("Removing plugin as per cluster message", mlog.String("plugin_id", data.Id))
if err := ch.removePluginLocally(data.Id); err != nil {
mlog.Warn("Failed to remove plugin locally", mlog.Err(err), mlog.String("id", data.Id))
}
if err := ch.notifyPluginStatusesChanged(); err != nil {
mlog.Warn("failed to notify plugin status changed", mlog.Err(err))
}
}
// InstallPluginWithSignature verifies and installs plugin.
func (ch *Channels) installPluginWithSignature(pluginFile, signature io.ReadSeeker) (*model.Manifest, *model.AppError) {
return ch.installPlugin(pluginFile, signature, installPluginLocallyAlways)
}
// InstallPlugin unpacks and installs a plugin but does not enable or activate it.
func (a *App) InstallPlugin(pluginFile io.ReadSeeker, replace bool) (*model.Manifest, *model.AppError) {
installationStrategy := installPluginLocallyOnlyIfNew
if replace {
installationStrategy = installPluginLocallyAlways
}
return a.installPlugin(pluginFile, nil, installationStrategy)
}
func (a *App) installPlugin(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
return a.ch.installPlugin(pluginFile, signature, installationStrategy)
}
func (ch *Channels) installPlugin(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
manifest, appErr := ch.installPluginLocally(pluginFile, signature, installationStrategy)
if appErr != nil {
return nil, appErr
}
if manifest == nil {
return nil, nil
}
if signature != nil {
signature.Seek(0, 0)
if _, appErr = ch.srv.writeFile(signature, getSignatureStorePath(manifest.Id)); appErr != nil {
return nil, model.NewAppError("saveSignature", "app.plugin.store_signature.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
}
}
// Store bundle in the file store to allow access from other servers.
pluginFile.Seek(0, 0)
if _, appErr := ch.srv.writeFile(pluginFile, getBundleStorePath(manifest.Id)); appErr != nil {
return nil, model.NewAppError("uploadPlugin", "app.plugin.store_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
}
ch.notifyClusterPluginEvent(
model.ClusterEventInstallPlugin,
model.PluginEventData{
Id: manifest.Id,
},
)
if err := ch.notifyPluginEnabled(manifest); err != nil {
mlog.Warn("Failed notify plugin enabled", mlog.Err(err))
}
if err := ch.notifyPluginStatusesChanged(); err != nil {
mlog.Warn("Failed to notify plugin status changed", mlog.Err(err))
}
return manifest, nil
}
// InstallMarketplacePlugin installs a plugin listed in the marketplace server. It will get the plugin bundle
// from the prepackaged folder, if available, or remotely if EnableRemoteMarketplace is true.
func (ch *Channels) InstallMarketplacePlugin(request *model.InstallMarketplacePluginRequest) (*model.Manifest, *model.AppError) {
var pluginFile, signatureFile io.ReadSeeker
prepackagedPlugin, appErr := ch.getPrepackagedPlugin(request.Id, request.Version)
if appErr != nil && appErr.Id != "app.plugin.marketplace_plugins.not_found.app_error" {
return nil, appErr
}
if prepackagedPlugin != nil {
fileReader, err := os.Open(prepackagedPlugin.Path)
if err != nil {
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.install_marketplace_plugin.app_error", nil, fmt.Sprintf("failed to open prepackaged plugin %s: %s", prepackagedPlugin.Path, err.Error()), http.StatusInternalServerError)
}
defer fileReader.Close()
pluginFile = fileReader
signatureFile = bytes.NewReader(prepackagedPlugin.Signature)
}
if *ch.cfgSvc.Config().PluginSettings.EnableRemoteMarketplace {
var plugin *model.BaseMarketplacePlugin
plugin, appErr = ch.getRemoteMarketplacePlugin(request.Id, request.Version)
if appErr != nil {
return nil, appErr
}
var prepackagedVersion semver.Version
if prepackagedPlugin != nil {
var err error
prepackagedVersion, err = semver.Parse(prepackagedPlugin.Manifest.Version)
if err != nil {
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
}
marketplaceVersion, err := semver.Parse(plugin.Manifest.Version)
if err != nil {
return nil, model.NewAppError("InstallMarketplacePlugin", "app.prepackged-plugin.invalid_version.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if prepackagedVersion.LT(marketplaceVersion) { // Always true if no prepackaged plugin was found
downloadedPluginBytes, err := ch.srv.downloadFromURL(plugin.DownloadURL)
if err != nil {
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.install_marketplace_plugin.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
signature, err := plugin.DecodeSignature()
if err != nil {
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.signature_decode.app_error", nil, "", http.StatusNotImplemented).Wrap(err)
}
pluginFile = bytes.NewReader(downloadedPluginBytes)
signatureFile = signature
}
}
if pluginFile == nil {
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.marketplace_plugins.not_found.app_error", nil, "", http.StatusInternalServerError)
}
if signatureFile == nil {
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.marketplace_plugins.signature_not_found.app_error", nil, "", http.StatusInternalServerError)
}
manifest, appErr := ch.installPluginWithSignature(pluginFile, signatureFile)
if appErr != nil {
return nil, appErr
}
return manifest, nil
}
type pluginInstallationStrategy int
const (
// installPluginLocallyOnlyIfNew installs the given plugin locally only if no plugin with the same id has been unpacked.
installPluginLocallyOnlyIfNew pluginInstallationStrategy = iota
// installPluginLocallyOnlyIfNewOrUpgrade installs the given plugin locally only if no plugin with the same id has been unpacked, or if such a plugin is older.
installPluginLocallyOnlyIfNewOrUpgrade
// installPluginLocallyAlways unconditionally installs the given plugin locally only, clobbering any existing plugin with the same id.
installPluginLocallyAlways
)
func (ch *Channels) installPluginLocally(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return nil, model.NewAppError("installPluginLocally", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
// verify signature
if signature != nil {
if err := ch.verifyPlugin(pluginFile, signature); err != nil {
return nil, err
}
}
tmpDir, err := os.MkdirTemp("", "plugintmp")
if err != nil {
return nil, model.NewAppError("installPluginLocally", "app.plugin.filesystem.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
defer os.RemoveAll(tmpDir)
manifest, pluginDir, appErr := extractPlugin(pluginFile, tmpDir)
if appErr != nil {
return nil, appErr
}
manifest, appErr = ch.installExtractedPlugin(manifest, pluginDir, installationStrategy)
if appErr != nil {
return nil, appErr
}
return manifest, nil
}
func extractPlugin(pluginFile io.ReadSeeker, extractDir string) (*model.Manifest, string, *model.AppError) {
pluginFile.Seek(0, 0)
if err := extractTarGz(pluginFile, extractDir); err != nil {
return nil, "", model.NewAppError("extractPlugin", "app.plugin.extract.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
dir, err := os.ReadDir(extractDir)
if err != nil {
return nil, "", model.NewAppError("extractPlugin", "app.plugin.filesystem.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(dir) == 1 && dir[0].IsDir() {
extractDir = filepath.Join(extractDir, dir[0].Name())
}
manifest, _, err := model.FindManifest(extractDir)
if err != nil {
return nil, "", model.NewAppError("extractPlugin", "app.plugin.manifest.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if !model.IsValidPluginId(manifest.Id) {
return nil, "", model.NewAppError("installPluginLocally", "app.plugin.invalid_id.app_error", map[string]any{"Min": model.MinIdLength, "Max": model.MaxIdLength, "Regex": model.ValidIdRegex}, "", http.StatusBadRequest)
}
return manifest, extractDir, nil
}
func (ch *Channels) installExtractedPlugin(manifest *model.Manifest, fromPluginDir string, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
bundles, err := pluginsEnvironment.Available()
if err != nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Check plugin id is not blocked
if plugin.PluginIDIsBlocked(manifest.Id) {
mlog.Debug("Skipping installation of plugin since plugin is on blocklist", mlog.String("plugin_id", manifest.Id))
return nil, nil
}
// Check for plugins installed with the same ID.
var existingManifest *model.Manifest
for _, bundle := range bundles {
if bundle.Manifest != nil && bundle.Manifest.Id == manifest.Id {
existingManifest = bundle.Manifest
break
}
}
if existingManifest != nil {
// Return an error if already installed and strategy disallows installation.
if installationStrategy == installPluginLocallyOnlyIfNew {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest)
}
// Skip installation if already installed and newer.
if installationStrategy == installPluginLocallyOnlyIfNewOrUpgrade {
var version, existingVersion semver.Version
version, err = semver.Parse(manifest.Version)
if err != nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest)
}
existingVersion, err = semver.Parse(existingManifest.Version)
if err != nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest)
}
if version.LTE(existingVersion) {
mlog.Debug("Skipping local installation of plugin since existing version is newer", mlog.String("plugin_id", manifest.Id))
return nil, nil
}
}
// Otherwise remove the existing installation prior to install below.
mlog.Debug("Removing existing installation of plugin before local install", mlog.String("plugin_id", existingManifest.Id), mlog.String("version", existingManifest.Version))
if err := ch.removePluginLocally(existingManifest.Id); err != nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install_id_failed_remove.app_error", nil, "", http.StatusBadRequest)
}
}
pluginPath := filepath.Join(*ch.cfgSvc.Config().PluginSettings.Directory, manifest.Id)
err = utils.CopyDir(fromPluginDir, pluginPath)
if err != nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.mvdir.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Flag plugin locally as managed by the filestore.
f, err := os.Create(filepath.Join(pluginPath, managedPluginFileName))
if err != nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.flag_managed.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
f.Close()
if manifest.HasWebapp() {
updatedManifest, err := pluginsEnvironment.UnpackWebappBundle(manifest.Id)
if err != nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.webapp_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
manifest = updatedManifest
}
// Activate the plugin if enabled.
pluginState := ch.cfgSvc.Config().PluginSettings.PluginStates[manifest.Id]
if pluginState != nil && pluginState.Enable {
if hasOverride, enabled := ch.getPluginStateOverride(manifest.Id); hasOverride && !enabled {
return manifest, nil
}
updatedManifest, _, err := pluginsEnvironment.Activate(manifest.Id)
if err != nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.restart.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
} else if updatedManifest == nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.restart.app_error", nil, "failed to activate plugin: plugin already active", http.StatusInternalServerError)
}
manifest = updatedManifest
}
mlog.Debug("Installing plugin", mlog.String("plugin_id", manifest.Id), mlog.String("version", manifest.Version))
return manifest, nil
}
func (ch *Channels) RemovePlugin(id string) *model.AppError {
// Disable plugin before removal to make sure this
// plugin remains disabled on re-install.
if err := ch.disablePlugin(id); err != nil {
return err
}
if err := ch.removePluginLocally(id); err != nil {
return err
}
// Remove bundle from the file store.
storePluginFileName := getBundleStorePath(id)
bundleExist, err := ch.srv.fileExists(storePluginFileName)
if err != nil {
return model.NewAppError("removePlugin", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if !bundleExist {
return nil
}
if err = ch.srv.removeFile(storePluginFileName); err != nil {
return model.NewAppError("removePlugin", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err = ch.removeSignature(id); err != nil {
mlog.Warn("Can't remove signature", mlog.Err(err))
}
ch.notifyClusterPluginEvent(
model.ClusterEventRemovePlugin,
model.PluginEventData{
Id: id,
},
)
if err := ch.notifyPluginStatusesChanged(); err != nil {
mlog.Warn("Failed to notify plugin status changed", mlog.Err(err))
}
return nil
}
func (ch *Channels) removePluginLocally(id string) *model.AppError {
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return model.NewAppError("removePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
plugins, err := pluginsEnvironment.Available()
if err != nil {
return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
var manifest *model.Manifest
var pluginPath string
for _, p := range plugins {
if p.Manifest != nil && p.Manifest.Id == id {
manifest = p.Manifest
pluginPath = filepath.Dir(p.ManifestPath)
break
}
}
if manifest == nil {
return model.NewAppError("removePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusNotFound)
}
pluginsEnvironment.Deactivate(id)
pluginsEnvironment.RemovePlugin(id)
ch.unregisterPluginCommands(id)
if err := os.RemoveAll(pluginPath); err != nil {
return model.NewAppError("removePlugin", "app.plugin.remove.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (ch *Channels) removeSignature(pluginID string) *model.AppError {
filePath := getSignatureStorePath(pluginID)
exists, err := ch.srv.fileExists(filePath)
if err != nil {
return model.NewAppError("removeSignature", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if !exists {
mlog.Debug("no plugin signature to remove", mlog.String("plugin_id", pluginID))
return nil
}
if err = ch.srv.removeFile(filePath); err != nil {
return model.NewAppError("removeSignature", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func getBundleStorePath(id string) string {
return filepath.Join(fileStorePluginFolder, fmt.Sprintf("%s.tar.gz", id))
}
func getSignatureStorePath(id string) string {
return filepath.Join(fileStorePluginFolder, fmt.Sprintf("%s.tar.gz.sig", id))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"crypto/sha256"
"encoding/base64"
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func getKeyHash(key string) string {
hash := sha256.New()
hash.Write([]byte(key))
return base64.StdEncoding.EncodeToString(hash.Sum(nil))
}
func (a *App) SetPluginKey(pluginID string, key string, value []byte) *model.AppError {
return a.SetPluginKeyWithExpiry(pluginID, key, value, 0)
}
func (a *App) SetPluginKeyWithExpiry(pluginID string, key string, value []byte, expireInSeconds int64) *model.AppError {
options := model.PluginKVSetOptions{
ExpireInSeconds: expireInSeconds,
}
_, err := a.SetPluginKeyWithOptions(pluginID, key, value, options)
return err
}
func (a *App) CompareAndSetPluginKey(pluginID string, key string, oldValue, newValue []byte) (bool, *model.AppError) {
options := model.PluginKVSetOptions{
Atomic: true,
OldValue: oldValue,
}
return a.SetPluginKeyWithOptions(pluginID, key, newValue, options)
}
func (a *App) SetPluginKeyWithOptions(pluginID string, key string, value []byte, options model.PluginKVSetOptions) (bool, *model.AppError) {
return a.Srv().Platform().SetPluginKeyWithOptions(pluginID, key, value, options)
}
func (a *App) CompareAndDeletePluginKey(pluginID string, key string, oldValue []byte) (bool, *model.AppError) {
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: key,
}
deleted, err := a.Srv().Store().Plugin().CompareAndDelete(kv, oldValue)
if err != nil {
mlog.Error("Failed to compare and delete plugin key value", mlog.String("plugin_id", pluginID), mlog.String("key", key), mlog.Err(err))
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return deleted, appErr
default:
return false, model.NewAppError("CompareAndDeletePluginKey", "app.plugin_store.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// Clean up a previous entry using the hashed key, if it exists.
if err := a.Srv().Store().Plugin().Delete(pluginID, getKeyHash(key)); err != nil {
mlog.Warn("Failed to clean up previously hashed plugin key value", mlog.String("plugin_id", pluginID), mlog.String("key", key), mlog.Err(err))
}
return deleted, nil
}
// TODO: platform: remove
func (s *Server) getPluginKey(pluginID string, key string) ([]byte, *model.AppError) {
if kv, err := s.Store().Plugin().Get(pluginID, key); err == nil {
return kv.Value, nil
} else if nfErr := new(store.ErrNotFound); !errors.As(err, &nfErr) {
mlog.Error("Failed to query plugin key value", mlog.String("plugin_id", pluginID), mlog.String("key", key), mlog.Err(err))
return nil, model.NewAppError("GetPluginKey", "app.plugin_store.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Lookup using the hashed version of the key for keys written prior to v5.6.
if kv, err := s.Store().Plugin().Get(pluginID, getKeyHash(key)); err == nil {
return kv.Value, nil
} else if nfErr := new(store.ErrNotFound); !errors.As(err, &nfErr) {
mlog.Error("Failed to query plugin key value using hashed key", mlog.String("plugin_id", pluginID), mlog.String("key", key), mlog.Err(err))
return nil, model.NewAppError("GetPluginKey", "app.plugin_store.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil, nil
}
func (a *App) GetPluginKey(pluginID string, key string) ([]byte, *model.AppError) {
return a.Srv().getPluginKey(pluginID, key)
}
func (a *App) DeletePluginKey(pluginID string, key string) *model.AppError {
return a.Srv().Platform().DeletePluginKey(pluginID, key)
}
func (a *App) DeleteAllKeysForPlugin(pluginID string) *model.AppError {
if err := a.Srv().Store().Plugin().DeleteAllForPlugin(pluginID); err != nil {
mlog.Error("Failed to delete all plugin key values", mlog.String("plugin_id", pluginID), mlog.Err(err))
return model.NewAppError("DeleteAllKeysForPlugin", "app.plugin_store.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) DeleteAllExpiredPluginKeys() *model.AppError {
if a.Srv() == nil {
return nil
}
if err := a.Srv().Store().Plugin().DeleteAllExpired(); err != nil {
mlog.Error("Failed to delete all expired plugin key values", mlog.Err(err))
return model.NewAppError("DeleteAllExpiredPluginKeys", "app.plugin_store.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) ListPluginKeys(pluginID string, page, perPage int) ([]string, *model.AppError) {
return a.Srv().Platform().ListPluginKeys(pluginID, page, perPage)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"io"
"net/http"
"path"
"path/filepath"
"strings"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (ch *Channels) ServePluginRequest(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
if handler, ok := ch.routerSvc.getHandler(params["plugin_id"]); ok {
ch.servePluginRequest(w, r, func(*plugin.Context, http.ResponseWriter, *http.Request) {
handler.ServeHTTP(w, r)
})
return
}
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
err := model.NewAppError("ServePluginRequest", "app.plugin.disabled.app_error", nil, "Enable plugins to serve plugin requests", http.StatusNotImplemented)
mlog.Error(err.Error())
w.WriteHeader(err.StatusCode)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(err.ToJSON()))
return
}
hooks, err := pluginsEnvironment.HooksForPlugin(params["plugin_id"])
if err != nil {
mlog.Debug("Access to route for non-existent plugin",
mlog.String("missing_plugin_id", params["plugin_id"]),
mlog.String("url", r.URL.String()),
mlog.Err(err))
http.NotFound(w, r)
return
}
ch.servePluginRequest(w, r, hooks.ServeHTTP)
}
func (a *App) ServeInterPluginRequest(w http.ResponseWriter, r *http.Request, sourcePluginId, destinationPluginId string) {
pluginsEnvironment := a.ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
err := model.NewAppError("ServeInterPluginRequest", "app.plugin.disabled.app_error", nil, "Plugin environment not found.", http.StatusNotImplemented)
a.Log().Error(err.Error())
w.WriteHeader(err.StatusCode)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(err.ToJSON()))
return
}
hooks, err := pluginsEnvironment.HooksForPlugin(destinationPluginId)
if err != nil {
a.Log().Error("Access to route for non-existent plugin in inter plugin request",
mlog.String("source_plugin_id", sourcePluginId),
mlog.String("destination_plugin_id", destinationPluginId),
mlog.String("url", r.URL.String()),
mlog.Err(err),
)
http.NotFound(w, r)
return
}
context := &plugin.Context{
RequestId: model.NewId(),
UserAgent: r.UserAgent(),
}
r.Header.Set("Mattermost-Plugin-ID", sourcePluginId)
hooks.ServeHTTP(context, w, r)
}
// ServePluginPublicRequest serves public plugin files
// at the URL http(s)://$SITE_URL/plugins/$PLUGIN_ID/public/{anything}
func (ch *Channels) ServePluginPublicRequest(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/") {
http.NotFound(w, r)
return
}
// Should be in the form of /(subpath/)?/plugins/{plugin_id}/public/* by the time we get here
vars := mux.Vars(r)
pluginID := vars["plugin_id"]
pluginsEnv := ch.GetPluginsEnvironment()
// Check if someone has nullified the pluginsEnv in the meantime
if pluginsEnv == nil {
http.NotFound(w, r)
return
}
publicFilesPath, err := pluginsEnv.PublicFilesPath(pluginID)
if err != nil {
http.NotFound(w, r)
return
}
subpath, err := utils.GetSubpathFromConfig(ch.cfgSvc.Config())
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
publicFilePath := path.Clean(r.URL.Path)
prefix := path.Join(subpath, "plugins", pluginID, "public")
if !strings.HasPrefix(publicFilePath, prefix) {
http.NotFound(w, r)
return
}
publicFile := filepath.Join(publicFilesPath, strings.TrimPrefix(publicFilePath, prefix))
http.ServeFile(w, r, publicFile)
}
func (ch *Channels) servePluginRequest(w http.ResponseWriter, r *http.Request, handler func(*plugin.Context, http.ResponseWriter, *http.Request)) {
token := ""
context := &plugin.Context{
RequestId: model.NewId(),
IPAddress: utils.GetIPAddress(r, ch.cfgSvc.Config().ServiceSettings.TrustedProxyIPHeader),
AcceptLanguage: r.Header.Get("Accept-Language"),
UserAgent: r.UserAgent(),
}
cookieAuth := false
authHeader := r.Header.Get(model.HeaderAuth)
if strings.HasPrefix(strings.ToUpper(authHeader), model.HeaderBearer+" ") {
token = authHeader[len(model.HeaderBearer)+1:]
} else if strings.HasPrefix(strings.ToLower(authHeader), model.HeaderToken+" ") {
token = authHeader[len(model.HeaderToken)+1:]
} else if cookie, _ := r.Cookie(model.SessionCookieToken); cookie != nil {
token = cookie.Value
cookieAuth = true
} else {
token = r.URL.Query().Get("access_token")
}
// Mattermost-Plugin-ID can only be set by inter-plugin requests
r.Header.Del("Mattermost-Plugin-ID")
r.Header.Del("Mattermost-User-Id")
if token != "" {
session, err := New(ServerConnector(ch)).GetSession(token)
defer ch.srv.platform.ReturnSessionToPool(session)
csrfCheckPassed := false
if session != nil && err == nil && cookieAuth && r.Method != "GET" {
sentToken := ""
if r.Header.Get(model.HeaderCsrfToken) == "" {
bodyBytes, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
r.ParseForm()
sentToken = r.FormValue("csrf")
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
} else {
sentToken = r.Header.Get(model.HeaderCsrfToken)
}
expectedToken := session.GetCSRF()
if sentToken == expectedToken {
csrfCheckPassed = true
}
// ToDo(DSchalla) 2019/01/04: Remove after deprecation period and only allow CSRF Header (MM-13657)
if r.Header.Get(model.HeaderRequestedWith) == model.HeaderRequestedWithXML && !csrfCheckPassed {
csrfErrorMessage := "CSRF Check failed for request - Please migrate your plugin to either send a CSRF Header or Form Field, XMLHttpRequest is deprecated"
sid := ""
userID := ""
if session.Id != "" {
sid = session.Id
userID = session.UserId
}
fields := []mlog.Field{
mlog.String("path", r.URL.Path),
mlog.String("ip", r.RemoteAddr),
mlog.String("session_id", sid),
mlog.String("user_id", userID),
}
if *ch.cfgSvc.Config().ServiceSettings.ExperimentalStrictCSRFEnforcement {
mlog.Warn(csrfErrorMessage, fields...)
} else {
mlog.Debug(csrfErrorMessage, fields...)
csrfCheckPassed = true
}
}
} else {
csrfCheckPassed = true
}
if (session != nil && session.Id != "") && err == nil && csrfCheckPassed {
r.Header.Set("Mattermost-User-Id", session.UserId)
context.SessionId = session.Id
}
}
cookies := r.Cookies()
r.Header.Del("Cookie")
for _, c := range cookies {
if c.Name != model.SessionCookieToken {
r.AddCookie(c)
}
}
r.Header.Del(model.HeaderAuth)
r.Header.Del("Referer")
params := mux.Vars(r)
subpath, _ := utils.GetSubpathFromConfig(ch.cfgSvc.Config())
newQuery := r.URL.Query()
newQuery.Del("access_token")
r.URL.RawQuery = newQuery.Encode()
r.URL.Path = strings.TrimPrefix(r.URL.Path, path.Join(subpath, "plugins", params["plugin_id"]))
handler(context, w, r)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"io"
"net/http"
"path/filepath"
"github.com/pkg/errors"
"golang.org/x/crypto/openpgp" //nolint:staticcheck
"golang.org/x/crypto/openpgp/armor" //nolint:staticcheck
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// GetPublicKey will return the actual public key saved in the `name` file.
func (a *App) GetPublicKey(name string) ([]byte, *model.AppError) {
return a.Srv().getPublicKey(name)
}
func (s *Server) getPublicKey(name string) ([]byte, *model.AppError) {
data, err := s.platform.GetConfigFile(name)
if err != nil {
return nil, model.NewAppError("GetPublicKey", "app.plugin.get_public_key.get_file.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return data, nil
}
// AddPublicKey will add plugin public key to the config. Overwrites the previous file
func (a *App) AddPublicKey(name string, key io.Reader) *model.AppError {
if isSamlFile(&a.Config().SamlSettings, name) {
return model.NewAppError("AddPublicKey", "app.plugin.modify_saml.app_error", nil, "", http.StatusInternalServerError)
}
data, err := io.ReadAll(key)
if err != nil {
return model.NewAppError("AddPublicKey", "app.plugin.write_file.read.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
err = a.Srv().platform.SetConfigFile(name, data)
if err != nil {
return model.NewAppError("AddPublicKey", "app.plugin.write_file.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.UpdateConfig(func(cfg *model.Config) {
if !utils.StringInSlice(name, cfg.PluginSettings.SignaturePublicKeyFiles) {
cfg.PluginSettings.SignaturePublicKeyFiles = append(cfg.PluginSettings.SignaturePublicKeyFiles, name)
}
})
return nil
}
// DeletePublicKey will delete plugin public key from the config.
func (a *App) DeletePublicKey(name string) *model.AppError {
if isSamlFile(&a.Config().SamlSettings, name) {
return model.NewAppError("AddPublicKey", "app.plugin.modify_saml.app_error", nil, "", http.StatusInternalServerError)
}
filename := filepath.Base(name)
if err := a.Srv().platform.RemoveConfigFile(filename); err != nil {
return model.NewAppError("DeletePublicKey", "app.plugin.delete_public_key.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.UpdateConfig(func(cfg *model.Config) {
cfg.PluginSettings.SignaturePublicKeyFiles = utils.RemoveStringFromSlice(filename, cfg.PluginSettings.SignaturePublicKeyFiles)
})
return nil
}
// VerifyPlugin checks that the given signature corresponds to the given plugin and matches a trusted certificate.
func (a *App) VerifyPlugin(plugin, signature io.ReadSeeker) *model.AppError {
return a.ch.verifyPlugin(plugin, signature)
}
func (ch *Channels) verifyPlugin(plugin, signature io.ReadSeeker) *model.AppError {
if err := verifySignature(bytes.NewReader(mattermostPluginPublicKey), plugin, signature); err == nil {
return nil
}
publicKeys := ch.cfgSvc.Config().PluginSettings.SignaturePublicKeyFiles
for _, pk := range publicKeys {
pkBytes, appErr := ch.srv.getPublicKey(pk)
if appErr != nil {
mlog.Warn("Unable to get public key for ", mlog.String("filename", pk))
continue
}
publicKey := bytes.NewReader(pkBytes)
plugin.Seek(0, 0)
signature.Seek(0, 0)
if err := verifySignature(publicKey, plugin, signature); err == nil {
return nil
}
}
return model.NewAppError("VerifyPlugin", "api.plugin.verify_plugin.app_error", nil, "", http.StatusInternalServerError)
}
func verifySignature(publicKey, message, signature io.Reader) error {
pk, err := decodeIfArmored(publicKey)
if err != nil {
return errors.Wrap(err, "can't decode public key")
}
s, err := decodeIfArmored(signature)
if err != nil {
return errors.Wrap(err, "can't decode signature")
}
return verifyBinarySignature(pk, message, s)
}
func verifyBinarySignature(publicKey, signedFile, signature io.Reader) error {
keyring, err := openpgp.ReadKeyRing(publicKey)
if err != nil {
return errors.Wrap(err, "can't read public key")
}
if _, err = openpgp.CheckDetachedSignature(keyring, signedFile, signature); err != nil {
return errors.Wrap(err, "error while checking the signature")
}
return nil
}
func decodeIfArmored(reader io.Reader) (io.Reader, error) {
readBytes, err := io.ReadAll(reader)
if err != nil {
return nil, errors.Wrap(err, "can't read the file")
}
block, err := armor.Decode(bytes.NewReader(readBytes))
if err != nil {
return bytes.NewReader(readBytes), nil
}
return block.Body, nil
}
// isSamlFile checks if filename is a SAML file.
func isSamlFile(saml *model.SamlSettings, filename string) bool {
return filename == *saml.PublicCertificateFile || filename == *saml.PrivateKeyFile || filename == *saml.IdpCertificateFile
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
)
// GetPluginStatus returns the status for a plugin installed on this server.
func (ch *Channels) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) {
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return nil, model.NewAppError("GetPluginStatus", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
pluginStatuses, err := pluginsEnvironment.Statuses()
if err != nil {
return nil, model.NewAppError("GetPluginStatus", "app.plugin.get_statuses.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, status := range pluginStatuses {
if status.PluginId == id {
// Add our cluster ID
if ch.srv.platform.Cluster() != nil {
status.ClusterId = ch.srv.platform.Cluster().GetClusterId()
}
return status, nil
}
}
return nil, model.NewAppError("GetPluginStatus", "app.plugin.not_installed.app_error", nil, "", http.StatusNotFound)
}
// GetPluginStatus returns the status for a plugin installed on this server.
func (a *App) GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) {
return a.ch.GetPluginStatus(id)
}
// GetPluginStatuses returns the status for plugins installed on this server.
func (ch *Channels) GetPluginStatuses() (model.PluginStatuses, *model.AppError) {
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return nil, model.NewAppError("GetPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
pluginStatuses, err := pluginsEnvironment.Statuses()
if err != nil {
return nil, model.NewAppError("GetPluginStatuses", "app.plugin.get_statuses.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Add our cluster ID
for _, status := range pluginStatuses {
if ch.srv.platform.Cluster() != nil {
status.ClusterId = ch.srv.platform.Cluster().GetClusterId()
} else {
status.ClusterId = ""
}
}
return pluginStatuses, nil
}
// GetPluginStatuses returns the status for plugins installed on this server.
func (a *App) GetPluginStatuses() (model.PluginStatuses, *model.AppError) {
return a.ch.GetPluginStatuses()
}
// GetClusterPluginStatuses returns the status for plugins installed anywhere in the cluster.
func (a *App) GetClusterPluginStatuses() (model.PluginStatuses, *model.AppError) {
return a.ch.getClusterPluginStatuses()
}
func (ch *Channels) getClusterPluginStatuses() (model.PluginStatuses, *model.AppError) {
pluginStatuses, err := ch.GetPluginStatuses()
if err != nil {
return nil, err
}
if ch.srv.platform.Cluster() != nil && *ch.cfgSvc.Config().ClusterSettings.Enable {
clusterPluginStatuses, err := ch.srv.platform.Cluster().GetPluginStatuses()
if err != nil {
return nil, model.NewAppError("GetClusterPluginStatuses", "app.plugin.get_cluster_plugin_statuses.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
pluginStatuses = append(pluginStatuses, clusterPluginStatuses...)
}
return pluginStatuses, nil
}
func (ch *Channels) notifyPluginStatusesChanged() error {
pluginStatuses, err := ch.getClusterPluginStatuses()
if err != nil {
return err
}
// Notify any system admins.
message := model.NewWebSocketEvent(model.WebsocketEventPluginStatusesChanged, "", "", "", nil, "")
message.Add("plugin_statuses", pluginStatuses)
message.GetBroadcast().ContainsSensitiveData = true
ch.srv.platform.Publish(message)
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/platform/services/cache"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
PendingPostIDsCacheSize = 25000
PendingPostIDsCacheTTL = 30 * time.Second
PageDefault = 0
)
var atMentionPattern = regexp.MustCompile(`\B@`)
// Ensure post service wrapper implements `product.PostService`
var _ product.PostService = (*postServiceWrapper)(nil)
// postServiceWrapper provides an implementation of `product.PostService` for use by products.
type postServiceWrapper struct {
app AppIface
}
func (s *postServiceWrapper) CreatePost(ctx *request.Context, post *model.Post) (*model.Post, *model.AppError) {
return s.app.CreatePostMissingChannel(ctx, post, true, true)
}
func (s *postServiceWrapper) GetPostsByIds(postIDs []string) ([]*model.Post, int64, *model.AppError) {
return s.app.GetPostsByIds(postIDs)
}
func (s *postServiceWrapper) SendEphemeralPost(ctx *request.Context, userID string, post *model.Post) *model.Post {
return s.app.SendEphemeralPost(ctx, userID, post)
}
func (s *postServiceWrapper) GetPost(postID string) (*model.Post, *model.AppError) {
return s.app.GetSinglePost(postID, false)
}
func (s *postServiceWrapper) DeletePost(ctx *request.Context, postID, productID string) (*model.Post, *model.AppError) {
return s.app.DeletePost(ctx, postID, productID)
}
func (s *postServiceWrapper) UpdatePost(ctx *request.Context, post *model.Post, safeUpdate bool) (*model.Post, *model.AppError) {
return s.app.UpdatePost(ctx, post, false)
}
func (a *App) CreatePostAsUser(c request.CTX, post *model.Post, currentSessionId string, setOnline bool) (*model.Post, *model.AppError) {
// Check that channel has not been deleted
channel, errCh := a.Srv().Store().Channel().Get(post.ChannelId, true)
if errCh != nil {
err := model.NewAppError("CreatePostAsUser", "api.context.invalid_param.app_error", map[string]any{"Name": "post.channel_id"}, "", http.StatusBadRequest).Wrap(errCh)
return nil, err
}
if strings.HasPrefix(post.Type, model.PostSystemMessagePrefix) {
err := model.NewAppError("CreatePostAsUser", "api.context.invalid_param.app_error", map[string]any{"Name": "post.type"}, "", http.StatusBadRequest)
return nil, err
}
if channel.DeleteAt != 0 {
err := model.NewAppError("createPost", "api.post.create_post.can_not_post_to_deleted.error", nil, "", http.StatusBadRequest)
return nil, err
}
rp, err := a.CreatePost(c, post, channel, true, setOnline)
if err != nil {
if err.Id == "api.post.create_post.root_id.app_error" ||
err.Id == "api.post.create_post.channel_root_id.app_error" {
err.StatusCode = http.StatusBadRequest
}
return nil, err
}
// Update the Channel LastViewAt only if:
// the post does NOT have from_webhook prop set (e.g. Zapier app), and
// the post does NOT have from_bot set (e.g. from discovering the user is a bot within CreatePost), and
// the post is NOT a reply post with CRT enabled
_, fromWebhook := post.GetProps()["from_webhook"]
_, fromBot := post.GetProps()["from_bot"]
isCRTReply := post.RootId != "" && a.IsCRTEnabledForUser(c, post.UserId)
if !fromWebhook && !fromBot && !isCRTReply {
if _, err := a.MarkChannelsAsViewed(c, []string{post.ChannelId}, post.UserId, currentSessionId, true); err != nil {
c.Logger().Warn(
"Encountered error updating last viewed",
mlog.String("channel_id", post.ChannelId),
mlog.String("user_id", post.UserId),
mlog.Err(err),
)
}
}
return rp, nil
}
func (a *App) CreatePostMissingChannel(c request.CTX, post *model.Post, triggerWebhooks bool, setOnline bool) (*model.Post, *model.AppError) {
channel, err := a.Srv().Store().Channel().Get(post.ChannelId, true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("CreatePostMissingChannel", "app.channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("CreatePostMissingChannel", "app.channel.get.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return a.CreatePost(c, post, channel, triggerWebhooks, setOnline)
}
// deduplicateCreatePost attempts to make posting idempotent within a caching window.
func (a *App) deduplicateCreatePost(post *model.Post) (foundPost *model.Post, err *model.AppError) {
// We rely on the client sending the pending post id across "duplicate" requests. If there
// isn't one, we can't deduplicate, so allow creation normally.
if post.PendingPostId == "" {
return nil, nil
}
const unknownPostId = ""
// Query the cache atomically for the given pending post id, saving a record if
// it hasn't previously been seen.
var postID string
nErr := a.Srv().seenPendingPostIdsCache.Get(post.PendingPostId, &postID)
if nErr == cache.ErrKeyNotFound {
a.Srv().seenPendingPostIdsCache.SetWithExpiry(post.PendingPostId, unknownPostId, PendingPostIDsCacheTTL)
return nil, nil
}
if nErr != nil {
return nil, model.NewAppError("errorGetPostId", "api.post.error_get_post_id.pending", nil, "", http.StatusInternalServerError)
}
// If another thread saved the cache record, but hasn't yet updated it with the actual post
// id (because it's still saving), notify the client with an error. Ideally, we'd wait
// for the other thread, but coordinating that adds complexity to the happy path.
if postID == unknownPostId {
return nil, model.NewAppError("deduplicateCreatePost", "api.post.deduplicate_create_post.pending", nil, "", http.StatusInternalServerError)
}
// If the other thread finished creating the post, return the created post back to the
// client, making the API call feel idempotent.
actualPost, err := a.GetSinglePost(postID, false)
if err != nil {
return nil, model.NewAppError("deduplicateCreatePost", "api.post.deduplicate_create_post.failed_to_get", nil, "", http.StatusInternalServerError).Wrap(err)
}
mlog.Debug("Deduplicated create post", mlog.String("post_id", actualPost.Id), mlog.String("pending_post_id", post.PendingPostId))
return actualPost, nil
}
func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel, triggerWebhooks, setOnline bool) (savedPost *model.Post, err *model.AppError) {
foundPost, err := a.deduplicateCreatePost(post)
if err != nil {
return nil, err
}
if foundPost != nil {
return foundPost, nil
}
// If we get this far, we've recorded the client-provided pending post id to the cache.
// Remove it if we fail below, allowing a proper retry by the client.
defer func() {
if post.PendingPostId == "" {
return
}
if err != nil {
a.Srv().seenPendingPostIdsCache.Remove(post.PendingPostId)
return
}
a.Srv().seenPendingPostIdsCache.SetWithExpiry(post.PendingPostId, savedPost.Id, PendingPostIDsCacheTTL)
}()
post.SanitizeProps()
var pchan chan store.StoreResult
if post.RootId != "" {
pchan = make(chan store.StoreResult, 1)
go func() {
r, pErr := a.Srv().Store().Post().Get(sqlstore.WithMaster(context.Background()), post.RootId, model.GetPostsOptions{}, "", a.Config().GetSanitizeOptions())
pchan <- store.StoreResult{Data: r, NErr: pErr}
close(pchan)
}()
}
user, nErr := a.Srv().Store().User().Get(context.Background(), post.UserId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("CreatePost", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("CreatePost", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if user.IsBot {
post.AddProp("from_bot", "true")
}
if c.Session().IsOAuth {
post.AddProp("from_oauth_app", "true")
}
var ephemeralPost *model.Post
if post.Type == "" && !a.HasPermissionToChannel(c, user.Id, channel.Id, model.PermissionUseChannelMentions) {
mention := post.DisableMentionHighlights()
if mention != "" {
T := i18n.GetUserTranslations(user.Locale)
ephemeralPost = &model.Post{
UserId: user.Id,
RootId: post.RootId,
ChannelId: channel.Id,
Message: T("model.post.channel_notifications_disabled_in_channel.message", model.StringInterface{"ChannelName": channel.Name, "Mention": mention}),
Props: model.StringInterface{model.PostPropsMentionHighlightDisabled: true},
}
}
}
// Verify the parent/child relationships are correct
var parentPostList *model.PostList
if pchan != nil {
result := <-pchan
if result.NErr != nil {
return nil, model.NewAppError("createPost", "api.post.create_post.root_id.app_error", nil, "", http.StatusBadRequest)
}
parentPostList = result.Data.(*model.PostList)
if len(parentPostList.Posts) == 0 || !parentPostList.IsChannelId(post.ChannelId) {
return nil, model.NewAppError("createPost", "api.post.create_post.channel_root_id.app_error", nil, "", http.StatusInternalServerError)
}
rootPost := parentPostList.Posts[post.RootId]
if rootPost.RootId != "" {
return nil, model.NewAppError("createPost", "api.post.create_post.root_id.app_error", nil, "", http.StatusBadRequest)
}
}
post.Hashtags, _ = model.ParseHashtags(post.Message)
if err = a.FillInPostProps(c, post, channel); err != nil {
return nil, err
}
// Temporary fix so old plugins don't clobber new fields in SlackAttachment struct, see MM-13088
if attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment); ok {
jsonAttachments, err := json.Marshal(attachments)
if err == nil {
attachmentsInterface := []any{}
err = json.Unmarshal(jsonAttachments, &attachmentsInterface)
post.AddProp("attachments", attachmentsInterface)
}
if err != nil {
c.Logger().Warn("Could not convert post attachments to map interface.", mlog.Err(err))
}
}
if !a.isPostPriorityEnabled() && post.GetPriority() != nil {
post.Metadata.Priority = nil
}
var metadata *model.PostMetadata
if post.Metadata != nil {
metadata = post.Metadata.Copy()
}
var rejectionError *model.AppError
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
replacementPost, rejectionReason := hooks.MessageWillBePosted(pluginContext, post.ForPlugin())
if rejectionReason != "" {
id := "Post rejected by plugin. " + rejectionReason
if rejectionReason == plugin.DismissPostError {
id = plugin.DismissPostError
}
rejectionError = model.NewAppError("createPost", id, nil, "", http.StatusBadRequest)
return false
}
if replacementPost != nil {
post = replacementPost
if post.Metadata != nil && metadata != nil {
post.Metadata.Priority = metadata.Priority
} else {
post.Metadata = metadata
}
}
return true
}, plugin.MessageWillBePostedID)
if rejectionError != nil {
return nil, rejectionError
}
// Pre-fill the CreateAt field for link previews to get the correct timestamp.
if post.CreateAt == 0 {
post.CreateAt = model.GetMillis()
}
post = a.getEmbedsAndImages(c, post, true)
previewPost := post.GetPreviewPost()
if previewPost != nil {
post.AddProp(model.PostPropsPreviewedPost, previewPost.PostID)
}
rpost, nErr := a.Srv().Store().Post().Save(post)
if nErr != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(nErr, &appErr):
return nil, appErr
case errors.As(nErr, &invErr):
return nil, model.NewAppError("CreatePost", "app.post.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
default:
return nil, model.NewAppError("CreatePost", "app.post.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
// Update the mapping from pending post id to the actual post id, for any clients that
// might be duplicating requests.
a.Srv().seenPendingPostIdsCache.SetWithExpiry(post.PendingPostId, rpost.Id, PendingPostIDsCacheTTL)
// We make a copy of the post for the plugin hook to avoid a race condition,
// and to remove the non-GOB-encodable Metadata from it.
pluginPost := rpost.ForPlugin()
a.Srv().Go(func() {
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.MessageHasBeenPosted(pluginContext, pluginPost)
return true
}, plugin.MessageHasBeenPostedID)
})
if a.Metrics() != nil {
a.Metrics().IncrementPostCreate()
}
if len(post.FileIds) > 0 {
if err = a.attachFilesToPost(post); err != nil {
c.Logger().Warn("Encountered error attaching files to post", mlog.String("post_id", post.Id), mlog.Any("file_ids", post.FileIds), mlog.Err(err))
}
if a.Metrics() != nil {
a.Metrics().IncrementPostFileAttachment(len(post.FileIds))
}
}
// Normally, we would let the API layer call PreparePostForClient, but we do it here since it also needs
// to be done when we send the post over the websocket in handlePostEvents
// PS: we don't want to include PostPriority from the db to avoid the replica lag,
// so we just return the one that was passed with post
rpost = a.PreparePostForClient(c, rpost, true, false, false)
// Make sure poster is following the thread
if *a.Config().ServiceSettings.ThreadAutoFollow && rpost.RootId != "" {
_, err := a.Srv().Store().Thread().MaintainMembership(user.Id, rpost.RootId, store.ThreadMembershipOpts{
Following: true,
UpdateFollowing: true,
})
if err != nil {
c.Logger().Warn("Failed to update thread membership", mlog.Err(err))
}
}
if err := a.handlePostEvents(c, rpost, user, channel, triggerWebhooks, parentPostList, setOnline); err != nil {
c.Logger().Warn("Failed to handle post events", mlog.Err(err))
}
// Send any ephemeral posts after the post is created to ensure it shows up after the latest post created
if ephemeralPost != nil {
a.SendEphemeralPost(c, post.UserId, ephemeralPost)
}
rpost, err = a.SanitizePostMetadataForUser(c, rpost, c.Session().UserId)
if err != nil {
return nil, err
}
return rpost, nil
}
func (a *App) addPostPreviewProp(post *model.Post) (*model.Post, error) {
previewPost := post.GetPreviewPost()
if previewPost != nil {
updatedPost := post.Clone()
updatedPost.AddProp(model.PostPropsPreviewedPost, previewPost.PostID)
updatedPost, err := a.Srv().Store().Post().Update(updatedPost, post)
return updatedPost, err
}
return post, nil
}
func (a *App) attachFilesToPost(post *model.Post) *model.AppError {
var attachedIds []string
for _, fileID := range post.FileIds {
err := a.Srv().Store().FileInfo().AttachToPost(fileID, post.Id, post.ChannelId, post.UserId)
if err != nil {
mlog.Warn("Failed to attach file to post", mlog.String("file_id", fileID), mlog.String("post_id", post.Id), mlog.Err(err))
continue
}
attachedIds = append(attachedIds, fileID)
}
if len(post.FileIds) != len(attachedIds) {
// We couldn't attach all files to the post, so ensure that post.FileIds reflects what was actually attached
post.FileIds = attachedIds
if _, err := a.Srv().Store().Post().Overwrite(post); err != nil {
return model.NewAppError("attachFilesToPost", "app.post.overwrite.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return nil
}
// FillInPostProps should be invoked before saving posts to fill in properties such as
// channel_mentions.
//
// If channel is nil, FillInPostProps will look up the channel corresponding to the post.
func (a *App) FillInPostProps(c request.CTX, post *model.Post, channel *model.Channel) *model.AppError {
channelMentions := post.ChannelMentions()
channelMentionsProp := make(map[string]any)
if len(channelMentions) > 0 {
if channel == nil {
postChannel, err := a.Srv().Store().Channel().GetForPost(post.Id)
if err != nil {
return model.NewAppError("FillInPostProps", "api.context.invalid_param.app_error", map[string]any{"Name": "post.channel_id"}, "", http.StatusBadRequest).Wrap(err)
}
channel = postChannel
}
mentionedChannels, err := a.GetChannelsByNames(c, channelMentions, channel.TeamId)
if err != nil {
return err
}
for _, mentioned := range mentionedChannels {
if mentioned.Type == model.ChannelTypeOpen {
team, err := a.Srv().Store().Team().Get(mentioned.TeamId)
if err != nil {
mlog.Warn("Failed to get team of the channel mention", mlog.String("team_id", channel.TeamId), mlog.String("channel_id", channel.Id), mlog.Err(err))
continue
}
channelMentionsProp[mentioned.Name] = map[string]any{
"display_name": mentioned.DisplayName,
"team_name": team.Name,
}
}
}
}
if len(channelMentionsProp) > 0 {
post.AddProp("channel_mentions", channelMentionsProp)
} else if post.GetProps() != nil {
post.DelProp("channel_mentions")
}
matched := atMentionPattern.MatchString(post.Message)
if a.Srv().License() != nil && *a.Srv().License().Features.LDAPGroups && matched && !a.HasPermissionToChannel(c, post.UserId, post.ChannelId, model.PermissionUseGroupMentions) {
post.AddProp(model.PostPropsGroupHighlightDisabled, true)
}
return nil
}
func (a *App) handlePostEvents(c request.CTX, post *model.Post, user *model.User, channel *model.Channel, triggerWebhooks bool, parentPostList *model.PostList, setOnline bool) error {
var team *model.Team
if channel.TeamId != "" {
t, err := a.Srv().Store().Team().Get(channel.TeamId)
if err != nil {
return err
}
team = t
} else {
// Blank team for DMs
team = &model.Team{}
}
a.Srv().Platform().InvalidateCacheForChannel(channel)
a.invalidateCacheForChannelPosts(channel.Id)
if _, err := a.SendNotifications(c, post, team, channel, user, parentPostList, setOnline); err != nil {
return err
}
if post.Type != model.PostTypeAutoResponder { // don't respond to an auto-responder
a.Srv().Go(func() {
_, err := a.SendAutoResponseIfNecessary(c, channel, user, post)
if err != nil {
mlog.Error("Failed to send auto response", mlog.String("user_id", user.Id), mlog.String("post_id", post.Id), mlog.Err(err))
}
})
}
if triggerWebhooks {
a.Srv().Go(func() {
if err := a.handleWebhookEvents(c, post, team, channel, user); err != nil {
mlog.Error(err.Error())
}
})
}
return nil
}
func (a *App) SendEphemeralPost(c request.CTX, userID string, post *model.Post) *model.Post {
post.Type = model.PostTypeEphemeral
// fill in fields which haven't been specified which have sensible defaults
if post.Id == "" {
post.Id = model.NewId()
}
if post.CreateAt == 0 {
post.CreateAt = model.GetMillis()
}
if post.GetProps() == nil {
post.SetProps(make(model.StringInterface))
}
post.GenerateActionIds()
message := model.NewWebSocketEvent(model.WebsocketEventEphemeralMessage, "", post.ChannelId, userID, nil, "")
post = a.PreparePostForClientWithEmbedsAndImages(c, post, true, false, true)
post = model.AddPostActionCookies(post, a.PostActionCookieSecret())
postJSON, jsonErr := post.ToJSON()
if jsonErr != nil {
mlog.Warn("Failed to encode post to JSON", mlog.Err(jsonErr))
}
message.Add("post", postJSON)
a.Publish(message)
return post
}
func (a *App) UpdateEphemeralPost(c request.CTX, userID string, post *model.Post) *model.Post {
post.Type = model.PostTypeEphemeral
post.UpdateAt = model.GetMillis()
if post.GetProps() == nil {
post.SetProps(make(model.StringInterface))
}
post.GenerateActionIds()
message := model.NewWebSocketEvent(model.WebsocketEventPostEdited, "", post.ChannelId, userID, nil, "")
post = a.PreparePostForClientWithEmbedsAndImages(c, post, true, false, true)
post = model.AddPostActionCookies(post, a.PostActionCookieSecret())
postJSON, jsonErr := post.ToJSON()
if jsonErr != nil {
mlog.Warn("Failed to encode post to JSON", mlog.Err(jsonErr))
}
message.Add("post", postJSON)
a.Publish(message)
return post
}
func (a *App) DeleteEphemeralPost(userID, postID string) {
post := &model.Post{
Id: postID,
UserId: userID,
Type: model.PostTypeEphemeral,
DeleteAt: model.GetMillis(),
UpdateAt: model.GetMillis(),
}
message := model.NewWebSocketEvent(model.WebsocketEventPostDeleted, "", "", userID, nil, "")
postJSON, jsonErr := post.ToJSON()
if jsonErr != nil {
mlog.Warn("Failed to encode post to JSON", mlog.Err(jsonErr))
}
message.Add("post", postJSON)
a.Publish(message)
}
func (a *App) UpdatePost(c *request.Context, post *model.Post, safeUpdate bool) (*model.Post, *model.AppError) {
post.SanitizeProps()
postLists, nErr := a.Srv().Store().Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", a.Config().GetSanitizeOptions())
if nErr != nil {
var nfErr *store.ErrNotFound
var invErr *store.ErrInvalidInput
switch {
case errors.As(nErr, &invErr):
return nil, model.NewAppError("UpdatePost", "app.post.get.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("UpdatePost", "app.post.get.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("UpdatePost", "app.post.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
oldPost := postLists.Posts[post.Id]
var err *model.AppError
if oldPost == nil {
err = model.NewAppError("UpdatePost", "api.post.update_post.find.app_error", nil, "id="+post.Id, http.StatusBadRequest)
return nil, err
}
if oldPost.DeleteAt != 0 {
err = model.NewAppError("UpdatePost", "api.post.update_post.permissions_details.app_error", map[string]any{"PostId": post.Id}, "", http.StatusBadRequest)
return nil, err
}
if oldPost.IsSystemMessage() {
err = model.NewAppError("UpdatePost", "api.post.update_post.system_message.app_error", nil, "id="+post.Id, http.StatusBadRequest)
return nil, err
}
channel, err := a.GetChannel(c, oldPost.ChannelId)
if err != nil {
return nil, err
}
if channel.DeleteAt != 0 {
return nil, model.NewAppError("UpdatePost", "api.post.update_post.can_not_update_post_in_deleted.error", nil, "", http.StatusBadRequest)
}
newPost := oldPost.Clone()
if newPost.Message != post.Message {
newPost.Message = post.Message
newPost.EditAt = model.GetMillis()
newPost.Hashtags, _ = model.ParseHashtags(post.Message)
}
if !safeUpdate {
newPost.IsPinned = post.IsPinned
newPost.HasReactions = post.HasReactions
newPost.FileIds = post.FileIds
newPost.SetProps(post.GetProps())
}
// Avoid deep-equal checks if EditAt was already modified through message change
if newPost.EditAt == oldPost.EditAt && (!oldPost.FileIds.Equals(newPost.FileIds) || !oldPost.AttachmentsEqual(newPost)) {
newPost.EditAt = model.GetMillis()
}
if err = a.FillInPostProps(c, post, nil); err != nil {
return nil, err
}
if post.IsRemote() {
oldPost.RemoteId = model.NewString(*post.RemoteId)
}
var rejectionReason string
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
newPost, rejectionReason = hooks.MessageWillBeUpdated(pluginContext, newPost.ForPlugin(), oldPost.ForPlugin())
return post != nil
}, plugin.MessageWillBeUpdatedID)
if newPost == nil {
return nil, model.NewAppError("UpdatePost", "Post rejected by plugin. "+rejectionReason, nil, "", http.StatusBadRequest)
}
// Restore the post metadata that was stripped by the plugin. Set it to
// the last known good.
newPost.Metadata = oldPost.Metadata
rpost, nErr := a.Srv().Store().Post().Update(newPost, oldPost)
if nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("UpdatePost", "app.post.update.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
pluginOldPost := oldPost.ForPlugin()
pluginNewPost := newPost.ForPlugin()
a.Srv().Go(func() {
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.MessageHasBeenUpdated(pluginContext, pluginNewPost, pluginOldPost)
return true
}, plugin.MessageHasBeenUpdatedID)
})
rpost = a.PreparePostForClientWithEmbedsAndImages(c, rpost, false, true, true)
// Ensure IsFollowing is nil since this updated post will be broadcast to all users
// and we don't want to have to populate it for every single user and broadcast to each
// individually.
rpost.IsFollowing = nil
rpost, nErr = a.addPostPreviewProp(rpost)
if nErr != nil {
return nil, model.NewAppError("UpdatePost", "app.post.update.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
message := model.NewWebSocketEvent(model.WebsocketEventPostEdited, "", rpost.ChannelId, "", nil, "")
postJSON, jsonErr := rpost.ToJSON()
if jsonErr != nil {
return nil, model.NewAppError("UpdatePost", "app.post.marshal.app_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("post", postJSON)
published, err := a.publishWebsocketEventForPermalinkPost(c, rpost, message)
if err != nil {
return nil, err
}
if !published {
a.Publish(message)
}
a.invalidateCacheForChannelPosts(rpost.ChannelId)
return rpost, nil
}
func (a *App) publishWebsocketEventForPermalinkPost(c request.CTX, post *model.Post, message *model.WebSocketEvent) (published bool, err *model.AppError) {
var previewedPostID string
if val, ok := post.GetProp(model.PostPropsPreviewedPost).(string); ok {
previewedPostID = val
} else {
return false, nil
}
if !model.IsValidId(previewedPostID) {
mlog.Warn("invalid post prop value", mlog.String("prop_key", model.PostPropsPreviewedPost), mlog.String("prop_value", previewedPostID))
return false, nil
}
previewedPost, err := a.GetSinglePost(previewedPostID, false)
if err != nil {
if err.StatusCode == http.StatusNotFound {
mlog.Warn("permalinked post not found", mlog.String("referenced_post_id", previewedPostID))
return false, nil
}
return false, err
}
channelMembers, err := a.GetChannelMembersPage(c, post.ChannelId, 0, 10000000)
if err != nil {
return false, err
}
permalinkPreviewedChannel, err := a.GetChannel(c, previewedPost.ChannelId)
if err != nil {
if err.StatusCode == http.StatusNotFound {
mlog.Warn("channel containing permalinked post not found", mlog.String("referenced_channel_id", previewedPost.ChannelId))
return false, nil
}
return false, err
}
permalinkPreviewedPost := post.GetPreviewPost()
for _, cm := range channelMembers {
if permalinkPreviewedPost != nil {
post.Metadata.Embeds[0].Data = permalinkPreviewedPost
}
postForUser := a.sanitizePostMetadataForUserAndChannel(c, post, permalinkPreviewedPost, permalinkPreviewedChannel, cm.UserId)
// Using DeepCopy here to avoid a race condition
// between publishing the event and setting the "post" data value below.
messageCopy := message.DeepCopy()
broadcastCopy := messageCopy.GetBroadcast()
broadcastCopy.UserId = cm.UserId
messageCopy.SetBroadcast(broadcastCopy)
postJSON, jsonErr := postForUser.ToJSON()
if jsonErr != nil {
mlog.Warn("Failed to encode post to JSON", mlog.Err(jsonErr))
}
messageCopy.Add("post", postJSON)
a.Publish(messageCopy)
}
return true, nil
}
func (a *App) PatchPost(c *request.Context, postID string, patch *model.PostPatch) (*model.Post, *model.AppError) {
post, err := a.GetSinglePost(postID, false)
if err != nil {
return nil, err
}
channel, err := a.GetChannel(c, post.ChannelId)
if err != nil {
return nil, err
}
if channel.DeleteAt != 0 {
err = model.NewAppError("PatchPost", "api.post.patch_post.can_not_update_post_in_deleted.error", nil, "", http.StatusBadRequest)
return nil, err
}
if !a.HasPermissionToChannel(c, post.UserId, post.ChannelId, model.PermissionUseChannelMentions) {
patch.DisableMentionHighlights()
}
post.Patch(patch)
updatedPost, err := a.UpdatePost(c, post, false)
if err != nil {
return nil, err
}
return updatedPost, nil
}
func (a *App) GetPostsPage(options model.GetPostsOptions) (*model.PostList, *model.AppError) {
postList, err := a.Srv().Store().Post().GetPosts(options, false, a.Config().GetSanitizeOptions())
if err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("GetPostsPage", "app.post.get_posts.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("GetPostsPage", "app.post.get_root_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// The postList is sorted as only rootPosts Order is included
if appErr := a.filterInaccessiblePosts(postList, filterPostOptions{assumeSortedCreatedAt: true}); appErr != nil {
return nil, appErr
}
return postList, nil
}
func (a *App) GetPosts(channelID string, offset int, limit int) (*model.PostList, *model.AppError) {
postList, err := a.Srv().Store().Post().GetPosts(model.GetPostsOptions{ChannelId: channelID, Page: offset, PerPage: limit}, true, a.Config().GetSanitizeOptions())
if err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("GetPosts", "app.post.get_posts.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("GetPosts", "app.post.get_root_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if appErr := a.filterInaccessiblePosts(postList, filterPostOptions{}); appErr != nil {
return nil, appErr
}
return postList, nil
}
func (a *App) GetPostsEtag(channelID string, collapsedThreads bool) string {
return a.Srv().Store().Post().GetEtag(channelID, true, collapsedThreads)
}
func (a *App) GetPostsSince(options model.GetPostsSinceOptions) (*model.PostList, *model.AppError) {
postList, err := a.Srv().Store().Post().GetPostsSince(options, true, a.Config().GetSanitizeOptions())
if err != nil {
return nil, model.NewAppError("GetPostsSince", "app.post.get_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if appErr := a.filterInaccessiblePosts(postList, filterPostOptions{assumeSortedCreatedAt: true}); appErr != nil {
return nil, appErr
}
return postList, nil
}
func (a *App) GetSinglePost(postID string, includeDeleted bool) (*model.Post, *model.AppError) {
post, err := a.Srv().Store().Post().GetSingle(postID, includeDeleted)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetSinglePost", "app.post.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetSinglePost", "app.post.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
firstInaccessiblePostTime, appErr := a.isInaccessiblePost(post)
if appErr != nil {
return nil, appErr
}
if firstInaccessiblePostTime != 0 {
return nil, model.NewAppError("GetSinglePost", "app.post.cloud.get.app_error", nil, "", http.StatusForbidden)
}
return post, nil
}
func (a *App) GetPostThread(postID string, opts model.GetPostsOptions, userID string) (*model.PostList, *model.AppError) {
posts, err := a.Srv().Store().Post().Get(context.Background(), postID, opts, userID, a.Config().GetSanitizeOptions())
if err != nil {
var nfErr *store.ErrNotFound
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("GetPostThread", "app.post.get.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetPostThread", "app.post.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetPostThread", "app.post.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// Get inserts the requested post first in the list, then adds the sorted threadPosts.
// So, the whole postList.Order is not sorted.
// The fully sorted list comes only when the CollapsedThreads is true and the Directions is not empty.
filterOptions := filterPostOptions{}
if opts.CollapsedThreads && opts.Direction != "" {
filterOptions.assumeSortedCreatedAt = true
}
if appErr := a.filterInaccessiblePosts(posts, filterOptions); appErr != nil {
return nil, appErr
}
return posts, nil
}
func (a *App) GetFlaggedPosts(userID string, offset int, limit int) (*model.PostList, *model.AppError) {
postList, err := a.Srv().Store().Post().GetFlaggedPosts(userID, offset, limit)
if err != nil {
return nil, model.NewAppError("GetFlaggedPosts", "app.post.get_flagged_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if appErr := a.filterInaccessiblePosts(postList, filterPostOptions{assumeSortedCreatedAt: true}); appErr != nil {
return nil, appErr
}
return postList, nil
}
func (a *App) GetFlaggedPostsForTeam(userID, teamID string, offset int, limit int) (*model.PostList, *model.AppError) {
postList, err := a.Srv().Store().Post().GetFlaggedPostsForTeam(userID, teamID, offset, limit)
if err != nil {
return nil, model.NewAppError("GetFlaggedPostsForTeam", "app.post.get_flagged_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if appErr := a.filterInaccessiblePosts(postList, filterPostOptions{assumeSortedCreatedAt: true}); appErr != nil {
return nil, appErr
}
return postList, nil
}
func (a *App) GetFlaggedPostsForChannel(userID, channelID string, offset int, limit int) (*model.PostList, *model.AppError) {
postList, err := a.Srv().Store().Post().GetFlaggedPostsForChannel(userID, channelID, offset, limit)
if err != nil {
return nil, model.NewAppError("GetFlaggedPostsForChannel", "app.post.get_flagged_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if appErr := a.filterInaccessiblePosts(postList, filterPostOptions{assumeSortedCreatedAt: true}); appErr != nil {
return nil, appErr
}
return postList, nil
}
func (a *App) GetPermalinkPost(c request.CTX, postID string, userID string) (*model.PostList, *model.AppError) {
list, nErr := a.Srv().Store().Post().Get(context.Background(), postID, model.GetPostsOptions{}, userID, a.Config().GetSanitizeOptions())
if nErr != nil {
var nfErr *store.ErrNotFound
var invErr *store.ErrInvalidInput
switch {
case errors.As(nErr, &invErr):
return nil, model.NewAppError("GetPermalinkPost", "app.post.get.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("GetPermalinkPost", "app.post.get.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("GetPermalinkPost", "app.post.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if len(list.Order) != 1 {
return nil, model.NewAppError("getPermalinkTmp", "api.post_get_post_by_id.get.app_error", nil, "", http.StatusNotFound)
}
post := list.Posts[list.Order[0]]
channel, err := a.GetChannel(c, post.ChannelId)
if err != nil {
return nil, err
}
if err = a.JoinChannel(c, channel, userID); err != nil {
return nil, err
}
if appErr := a.filterInaccessiblePosts(list, filterPostOptions{assumeSortedCreatedAt: true}); appErr != nil {
return nil, appErr
}
return list, nil
}
func (a *App) GetPostsBeforePost(options model.GetPostsOptions) (*model.PostList, *model.AppError) {
postList, err := a.Srv().Store().Post().GetPostsBefore(options, a.Config().GetSanitizeOptions())
if err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("GetPostsBeforePost", "app.post.get_posts_around.get.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("GetPostsBeforePost", "app.post.get_posts_around.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// GetPostsBefore orders by channel id and deleted at,
// before sorting based on created at.
// but the deleted at is only ever where deleted at = 0,
// and channel id may or may not be empty (all channels) or defined (single channel),
// so we can still optimize if the search is for a single channel
filterOptions := filterPostOptions{}
if options.ChannelId != "" {
filterOptions.assumeSortedCreatedAt = true
}
if appErr := a.filterInaccessiblePosts(postList, filterOptions); appErr != nil {
return nil, appErr
}
return postList, nil
}
func (a *App) GetPostsAfterPost(options model.GetPostsOptions) (*model.PostList, *model.AppError) {
postList, err := a.Srv().Store().Post().GetPostsAfter(options, a.Config().GetSanitizeOptions())
if err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("GetPostsAfterPost", "app.post.get_posts_around.get.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("GetPostsAfterPost", "app.post.get_posts_around.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// GetPostsAfter orders by channel id and deleted at,
// before sorting based on created at.
// but the deleted at is only ever where deleted at = 0,
// and channel id may or may not be empty (all channels) or defined (single channel),
// so we can still optimize if the search is for a single channel
filterOptions := filterPostOptions{}
if options.ChannelId != "" {
filterOptions.assumeSortedCreatedAt = true
}
if appErr := a.filterInaccessiblePosts(postList, filterOptions); appErr != nil {
return nil, appErr
}
return postList, nil
}
func (a *App) GetPostsAroundPost(before bool, options model.GetPostsOptions) (*model.PostList, *model.AppError) {
var postList *model.PostList
var err error
sanitize := a.Config().GetSanitizeOptions()
if before {
postList, err = a.Srv().Store().Post().GetPostsBefore(options, sanitize)
} else {
postList, err = a.Srv().Store().Post().GetPostsAfter(options, sanitize)
}
if err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("GetPostsAroundPost", "app.post.get_posts_around.get.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("GetPostsAroundPost", "app.post.get_posts_around.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// GetPostsBefore and GetPostsAfter order by channel id and deleted at,
// before sorting based on created at.
// but the deleted at is only ever where deleted at = 0,
// and channel id may or may not be empty (all channels) or defined (single channel),
// so we can still optimize if the search is for a single channel
filterOptions := filterPostOptions{}
if options.ChannelId != "" {
filterOptions.assumeSortedCreatedAt = true
}
if appErr := a.filterInaccessiblePosts(postList, filterOptions); appErr != nil {
return nil, appErr
}
return postList, nil
}
func (a *App) GetPostAfterTime(channelID string, time int64, collapsedThreads bool) (*model.Post, *model.AppError) {
post, err := a.Srv().Store().Post().GetPostAfterTime(channelID, time, collapsedThreads)
if err != nil {
return nil, model.NewAppError("GetPostAfterTime", "app.post.get_post_after_time.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return post, nil
}
func (a *App) GetPostIdAfterTime(channelID string, time int64, collapsedThreads bool) (string, *model.AppError) {
postID, err := a.Srv().Store().Post().GetPostIdAfterTime(channelID, time, collapsedThreads)
if err != nil {
return "", model.NewAppError("GetPostIdAfterTime", "app.post.get_post_id_around.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return postID, nil
}
func (a *App) GetPostIdBeforeTime(channelID string, time int64, collapsedThreads bool) (string, *model.AppError) {
postID, err := a.Srv().Store().Post().GetPostIdBeforeTime(channelID, time, collapsedThreads)
if err != nil {
return "", model.NewAppError("GetPostIdBeforeTime", "app.post.get_post_id_around.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return postID, nil
}
func (a *App) GetNextPostIdFromPostList(postList *model.PostList, collapsedThreads bool) string {
if len(postList.Order) > 0 {
firstPostId := postList.Order[0]
firstPost := postList.Posts[firstPostId]
nextPostId, err := a.GetPostIdAfterTime(firstPost.ChannelId, firstPost.CreateAt, collapsedThreads)
if err != nil {
mlog.Warn("GetNextPostIdFromPostList: failed in getting next post", mlog.Err(err))
}
return nextPostId
}
return ""
}
func (a *App) GetPrevPostIdFromPostList(postList *model.PostList, collapsedThreads bool) string {
if len(postList.Order) > 0 {
lastPostId := postList.Order[len(postList.Order)-1]
lastPost := postList.Posts[lastPostId]
previousPostId, err := a.GetPostIdBeforeTime(lastPost.ChannelId, lastPost.CreateAt, collapsedThreads)
if err != nil {
mlog.Warn("GetPrevPostIdFromPostList: failed in getting previous post", mlog.Err(err))
}
return previousPostId
}
return ""
}
// AddCursorIdsForPostList adds NextPostId and PrevPostId as cursor to the PostList.
// The conditional blocks ensure that it sets those cursor IDs immediately as afterPost, beforePost or empty,
// and only query to database whenever necessary.
func (a *App) AddCursorIdsForPostList(originalList *model.PostList, afterPost, beforePost string, since int64, page, perPage int, collapsedThreads bool) {
prevPostIdSet := false
prevPostId := ""
nextPostIdSet := false
nextPostId := ""
if since > 0 { // "since" query to return empty NextPostId and PrevPostId
nextPostIdSet = true
prevPostIdSet = true
} else if afterPost != "" {
if page == 0 {
prevPostId = afterPost
prevPostIdSet = true
}
if len(originalList.Order) < perPage {
nextPostIdSet = true
}
} else if beforePost != "" {
if page == 0 {
nextPostId = beforePost
nextPostIdSet = true
}
if len(originalList.Order) < perPage {
prevPostIdSet = true
}
}
if !nextPostIdSet {
nextPostId = a.GetNextPostIdFromPostList(originalList, collapsedThreads)
}
if !prevPostIdSet {
prevPostId = a.GetPrevPostIdFromPostList(originalList, collapsedThreads)
}
originalList.NextPostId = nextPostId
originalList.PrevPostId = prevPostId
}
func (a *App) GetPostsForChannelAroundLastUnread(c request.CTX, channelID, userID string, limitBefore, limitAfter int, skipFetchThreads bool, collapsedThreads, collapsedThreadsExtended bool) (*model.PostList, *model.AppError) {
var member *model.ChannelMember
var err *model.AppError
if member, err = a.GetChannelMember(c, channelID, userID); err != nil {
return nil, err
} else if member.LastViewedAt == 0 {
return model.NewPostList(), nil
}
lastUnreadPostId, err := a.GetPostIdAfterTime(channelID, member.LastViewedAt, collapsedThreads)
if err != nil {
return nil, err
} else if lastUnreadPostId == "" {
return model.NewPostList(), nil
}
opts := model.GetPostsOptions{
SkipFetchThreads: skipFetchThreads,
CollapsedThreads: collapsedThreads,
CollapsedThreadsExtended: collapsedThreadsExtended,
}
postList, err := a.GetPostThread(lastUnreadPostId, opts, userID)
if err != nil {
return nil, err
}
// Reset order to only include the last unread post: if the thread appears in the centre
// channel organically, those replies will be added below.
postList.Order = []string{}
// Add lastUnreadPostId in order, only if it hasn't been filtered as per the cloud plan's limit
if _, ok := postList.Posts[lastUnreadPostId]; ok {
postList.Order = []string{lastUnreadPostId}
// BeforePosts will only be accessible if the lastUnreadPostId is itself accessible
if postListBefore, err := a.GetPostsBeforePost(model.GetPostsOptions{ChannelId: channelID, PostId: lastUnreadPostId, Page: PageDefault, PerPage: limitBefore, SkipFetchThreads: skipFetchThreads, CollapsedThreads: collapsedThreads, CollapsedThreadsExtended: collapsedThreadsExtended, UserId: userID}); err != nil {
return nil, err
} else if postListBefore != nil {
postList.Extend(postListBefore)
}
}
if postListAfter, err := a.GetPostsAfterPost(model.GetPostsOptions{ChannelId: channelID, PostId: lastUnreadPostId, Page: PageDefault, PerPage: limitAfter - 1, SkipFetchThreads: skipFetchThreads, CollapsedThreads: collapsedThreads, CollapsedThreadsExtended: collapsedThreadsExtended, UserId: userID}); err != nil {
return nil, err
} else if postListAfter != nil {
postList.Extend(postListAfter)
}
postList.SortByCreateAt()
return postList, nil
}
func (a *App) DeletePost(c request.CTX, postID, deleteByID string) (*model.Post, *model.AppError) {
post, err := a.Srv().Store().Post().GetSingle(postID, false)
if err != nil {
return nil, model.NewAppError("DeletePost", "app.post.get.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
channel, appErr := a.GetChannel(c, post.ChannelId)
if appErr != nil {
return nil, appErr
}
if channel.DeleteAt != 0 {
appErr := model.NewAppError("DeletePost", "api.post.delete_post.can_not_delete_post_in_deleted.error", nil, "", http.StatusBadRequest)
return nil, appErr
}
err = a.Srv().Store().Post().Delete(postID, model.GetMillis(), deleteByID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("DeletePost", "app.post.delete.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("DeletePost", "app.post.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
postJSON, err := json.Marshal(post)
if err != nil {
return nil, model.NewAppError("DeletePost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
userMessage := model.NewWebSocketEvent(model.WebsocketEventPostDeleted, "", post.ChannelId, "", nil, "")
userMessage.Add("post", string(postJSON))
userMessage.GetBroadcast().ContainsSanitizedData = true
a.Publish(userMessage)
adminMessage := model.NewWebSocketEvent(model.WebsocketEventPostDeleted, "", post.ChannelId, "", nil, "")
adminMessage.Add("post", string(postJSON))
adminMessage.Add("delete_by", deleteByID)
adminMessage.GetBroadcast().ContainsSensitiveData = true
a.Publish(adminMessage)
if len(post.FileIds) > 0 {
a.Srv().Go(func() {
a.deletePostFiles(post.Id)
})
a.Srv().Store().FileInfo().InvalidateFileInfosForPostCache(postID, true)
a.Srv().Store().FileInfo().InvalidateFileInfosForPostCache(postID, false)
}
a.Srv().Go(func() {
a.deleteFlaggedPosts(post.Id)
})
a.invalidateCacheForChannelPosts(post.ChannelId)
return post, nil
}
func (a *App) deleteFlaggedPosts(postID string) {
if err := a.Srv().Store().Preference().DeleteCategoryAndName(model.PreferenceCategoryFlaggedPost, postID); err != nil {
a.Log().Warn("Unable to delete flagged post preference when deleting post.", mlog.Err(err))
return
}
}
func (a *App) deletePostFiles(postID string) {
if _, err := a.Srv().Store().FileInfo().DeleteForPost(postID); err != nil {
a.Log().Warn("Encountered error when deleting files for post", mlog.String("post_id", postID), mlog.Err(err))
}
}
func (a *App) parseAndFetchChannelIdByNameFromInFilter(c *request.Context, channelName, userID, teamID string, includeDeleted bool) (*model.Channel, error) {
if strings.HasPrefix(channelName, "@") && strings.Contains(channelName, ",") {
var userIDs []string
users, err := a.GetUsersByUsernames(strings.Split(channelName[1:], ","), false, nil)
if err != nil {
return nil, err
}
for _, user := range users {
userIDs = append(userIDs, user.Id)
}
channel, err := a.GetGroupChannel(c, userIDs)
if err != nil {
return nil, err
}
return channel, nil
}
if strings.HasPrefix(channelName, "@") && !strings.Contains(channelName, ",") {
user, err := a.GetUserByUsername(channelName[1:])
if err != nil {
return nil, err
}
channel, err := a.GetOrCreateDirectChannel(c, userID, user.Id)
if err != nil {
return nil, err
}
return channel, nil
}
channel, err := a.GetChannelByName(c, channelName, teamID, includeDeleted)
if err != nil {
return nil, err
}
return channel, nil
}
func (a *App) searchPostsInTeam(teamID string, userID string, paramsList []*model.SearchParams, modifierFun func(*model.SearchParams)) (*model.PostList, *model.AppError) {
var wg sync.WaitGroup
pchan := make(chan store.StoreResult, len(paramsList))
for _, params := range paramsList {
// Don't allow users to search for everything.
if params.Terms == "*" {
continue
}
modifierFun(params)
wg.Add(1)
go func(params *model.SearchParams) {
defer wg.Done()
postList, err := a.Srv().Store().Post().Search(teamID, userID, params)
pchan <- store.StoreResult{Data: postList, NErr: err}
}(params)
}
wg.Wait()
close(pchan)
posts := model.NewPostList()
for result := range pchan {
if result.NErr != nil {
return nil, model.NewAppError("searchPostsInTeam", "app.post.search.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
data := result.Data.(*model.PostList)
posts.Extend(data)
}
posts.SortByCreateAt()
a.filterInaccessiblePosts(posts, filterPostOptions{assumeSortedCreatedAt: true})
return posts, nil
}
func (a *App) convertChannelNamesToChannelIds(c *request.Context, channels []string, userID string, teamID string, includeDeletedChannels bool) []string {
for idx, channelName := range channels {
channel, err := a.parseAndFetchChannelIdByNameFromInFilter(c, channelName, userID, teamID, includeDeletedChannels)
if err != nil {
a.Log().Warn("error getting channel id by name from in filter", mlog.Err(err))
continue
}
channels[idx] = channel.Id
}
return channels
}
func (a *App) convertUserNameToUserIds(usernames []string) []string {
for idx, username := range usernames {
user, err := a.GetUserByUsername(username)
if err != nil {
a.Log().Warn("error getting user by username", mlog.String("user_name", username), mlog.Err(err))
continue
}
usernames[idx] = user.Id
}
return usernames
}
// GetLastAccessiblePostTime returns CreateAt time(from cache) of the last accessible post as per the cloud limit
func (a *App) GetLastAccessiblePostTime() (int64, *model.AppError) {
license := a.Srv().License()
if license == nil || !license.IsCloud() {
return 0, nil
}
system, err := a.Srv().Store().System().GetByName(model.SystemLastAccessiblePostTime)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
// All posts are accessible
return 0, nil
default:
return 0, model.NewAppError("GetLastAccessiblePostTime", "app.system.get_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
lastAccessiblePostTime, err := strconv.ParseInt(system.Value, 10, 64)
if err != nil {
return 0, model.NewAppError("GetLastAccessiblePostTime", "common.parse_error_int64", map[string]interface{}{"Value": system.Value}, "", http.StatusInternalServerError).Wrap(err)
}
return lastAccessiblePostTime, nil
}
// ComputeLastAccessiblePostTime updates cache with CreateAt time of the last accessible post as per the cloud plan's limit.
// Use GetLastAccessiblePostTime() to access the result.
func (a *App) ComputeLastAccessiblePostTime() error {
limit, appErr := a.getCloudMessagesHistoryLimit()
if appErr != nil {
return appErr
}
if limit == 0 {
// All posts are accessible - we must check if a previous value was set so we can clear it
systemValue, err := a.Srv().Store().System().GetByName(model.SystemLastAccessiblePostTime)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
// There was no previous value, nothing to do
return nil
default:
return model.NewAppError("ComputeLastAccessiblePostTime", "app.system.get_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if systemValue != nil {
// Previous value was set, so we must clear it
if _, err = a.Srv().Store().System().PermanentDeleteByName(model.SystemLastAccessiblePostTime); err != nil {
return model.NewAppError("ComputeLastAccessiblePostTime", "app.system.permanent_delete_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// Cloud limit is not applicable
return nil
}
createdAt, err := a.Srv().GetStore().Post().GetNthRecentPostTime(limit)
if err != nil {
var nfErr *store.ErrNotFound
if !errors.As(err, &nfErr) {
return model.NewAppError("ComputeLastAccessiblePostTime", "app.last_accessible_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// Update Cache
err = a.Srv().Store().System().SaveOrUpdate(&model.System{
Name: model.SystemLastAccessiblePostTime,
Value: strconv.FormatInt(createdAt, 10),
})
if err != nil {
return model.NewAppError("ComputeLastAccessiblePostTime", "app.system.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) getCloudMessagesHistoryLimit() (int64, *model.AppError) {
license := a.Srv().License()
if license == nil || !license.IsCloud() {
return 0, nil
}
limits, err := a.Cloud().GetCloudLimits("")
if err != nil {
return 0, model.NewAppError("getCloudMessagesHistoryLimit", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if limits == nil || limits.Messages == nil || limits.Messages.History == nil {
// Cloud limit is not applicable
return 0, nil
}
return int64(*limits.Messages.History), nil
}
func (a *App) SearchPostsInTeam(teamID string, paramsList []*model.SearchParams) (*model.PostList, *model.AppError) {
if !*a.Config().ServiceSettings.EnablePostSearch {
return nil, model.NewAppError("SearchPostsInTeam", "store.sql_post.search.disabled", nil, fmt.Sprintf("teamId=%v", teamID), http.StatusNotImplemented)
}
return a.searchPostsInTeam(teamID, "", paramsList, func(params *model.SearchParams) {
params.SearchWithoutUserId = true
})
}
func (a *App) SearchPostsForUser(c *request.Context, terms string, userID string, teamID string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int, modifier string) (*model.PostSearchResults, *model.AppError) {
var postSearchResults *model.PostSearchResults
paramsList := model.ParseSearchParams(strings.TrimSpace(terms), timeZoneOffset)
includeDeleted := includeDeletedChannels && *a.Config().TeamSettings.ExperimentalViewArchivedChannels
if !*a.Config().ServiceSettings.EnablePostSearch {
return nil, model.NewAppError("SearchPostsForUser", "store.sql_post.search.disabled", nil, fmt.Sprintf("teamId=%v userId=%v", teamID, userID), http.StatusNotImplemented)
}
finalParamsList := []*model.SearchParams{}
for _, params := range paramsList {
params.Modifier = modifier
params.OrTerms = isOrSearch
params.IncludeDeletedChannels = includeDeleted
// Don't allow users to search for "*"
if params.Terms != "*" {
// TODO: we have to send channel ids
// from the front-end. Otherwise it's not possible to distinguish
// from just the channel name at a cross-team level.
// Convert channel names to channel IDs
params.InChannels = a.convertChannelNamesToChannelIds(c, params.InChannels, userID, teamID, includeDeletedChannels)
params.ExcludedChannels = a.convertChannelNamesToChannelIds(c, params.ExcludedChannels, userID, teamID, includeDeletedChannels)
// Convert usernames to user IDs
params.FromUsers = a.convertUserNameToUserIds(params.FromUsers)
params.ExcludedUsers = a.convertUserNameToUserIds(params.ExcludedUsers)
finalParamsList = append(finalParamsList, params)
}
}
// If the processed search params are empty, return empty search results.
if len(finalParamsList) == 0 {
return model.MakePostSearchResults(model.NewPostList(), nil), nil
}
postSearchResults, err := a.Srv().Store().Post().SearchPostsForUser(finalParamsList, userID, teamID, page, perPage)
if err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("SearchPostsForUser", "app.post.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if appErr := a.filterInaccessiblePosts(postSearchResults.PostList, filterPostOptions{assumeSortedCreatedAt: true}); appErr != nil {
return nil, appErr
}
return postSearchResults, nil
}
func (a *App) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, *model.AppError) {
searchParams, err := a.Srv().Store().Post().GetRecentSearchesForUser(userID)
if err != nil {
return nil, model.NewAppError("GetRecentSearchesForUser", "app.recent_searches.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return searchParams, nil
}
func (a *App) GetFileInfosForPostWithMigration(postID string, includeDeleted bool) ([]*model.FileInfo, *model.AppError) {
pchan := make(chan store.StoreResult, 1)
go func() {
post, err := a.Srv().Store().Post().GetSingle(postID, includeDeleted)
pchan <- store.StoreResult{Data: post, NErr: err}
close(pchan)
}()
infos, firstInaccessibleFileTime, err := a.GetFileInfosForPost(postID, false, includeDeleted)
if err != nil {
return nil, err
}
if len(infos) == 0 && firstInaccessibleFileTime == 0 {
// No FileInfos were returned so check if they need to be created for this post
result := <-pchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, model.NewAppError("GetFileInfosForPostWithMigration", "app.post.get.app_error", nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, model.NewAppError("GetFileInfosForPostWithMigration", "app.post.get.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
post := result.Data.(*model.Post)
if len(post.Filenames) > 0 {
a.Srv().Store().FileInfo().InvalidateFileInfosForPostCache(postID, false)
a.Srv().Store().FileInfo().InvalidateFileInfosForPostCache(postID, true)
// The post has Filenames that need to be replaced with FileInfos
infos = a.MigrateFilenamesToFileInfos(post)
}
}
return infos, nil
}
// GetFileInfosForPost also returns firstInaccessibleFileTime based on cloud plan's limit.
func (a *App) GetFileInfosForPost(postID string, fromMaster bool, includeDeleted bool) ([]*model.FileInfo, int64, *model.AppError) {
fileInfos, err := a.Srv().Store().FileInfo().GetForPost(postID, fromMaster, includeDeleted, true)
if err != nil {
return nil, 0, model.NewAppError("GetFileInfosForPost", "app.file_info.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
firstInaccessibleFileTime, appErr := a.removeInaccessibleContentFromFilesSlice(fileInfos)
if appErr != nil {
return nil, 0, appErr
}
a.generateMiniPreviewForInfos(fileInfos)
return fileInfos, firstInaccessibleFileTime, nil
}
func (a *App) getFileInfosForPostIgnoreCloudLimit(postID string, fromMaster bool, includeDeleted bool) ([]*model.FileInfo, *model.AppError) {
fileInfos, err := a.Srv().Store().FileInfo().GetForPost(postID, fromMaster, includeDeleted, true)
if err != nil {
return nil, model.NewAppError("getFileInfosForPostIgnoreCloudLimit", "app.file_info.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.generateMiniPreviewForInfos(fileInfos)
return fileInfos, nil
}
func (a *App) PostWithProxyAddedToImageURLs(post *model.Post) *model.Post {
if f := a.ImageProxyAdder(); f != nil {
return post.WithRewrittenImageURLs(f)
}
return post
}
func (a *App) PostWithProxyRemovedFromImageURLs(post *model.Post) *model.Post {
if f := a.ImageProxyRemover(); f != nil {
return post.WithRewrittenImageURLs(f)
}
return post
}
func (a *App) PostPatchWithProxyRemovedFromImageURLs(patch *model.PostPatch) *model.PostPatch {
if f := a.ImageProxyRemover(); f != nil {
return patch.WithRewrittenImageURLs(f)
}
return patch
}
func (a *App) ImageProxyAdder() func(string) string {
if !*a.Config().ImageProxySettings.Enable {
return nil
}
return func(url string) string {
return a.ImageProxy().GetProxiedImageURL(url)
}
}
func (a *App) ImageProxyRemover() (f func(string) string) {
if !*a.Config().ImageProxySettings.Enable {
return nil
}
return func(url string) string {
return a.ImageProxy().GetUnproxiedImageURL(url)
}
}
func (a *App) MaxPostSize() int {
return a.Srv().Platform().MaxPostSize()
}
// countThreadMentions returns the number of times the user is mentioned in a specified thread after the timestamp.
func (a *App) countThreadMentions(c request.CTX, user *model.User, post *model.Post, teamID string, timestamp int64) (int64, *model.AppError) {
channel, err := a.GetChannel(c, post.ChannelId)
if err != nil {
return 0, err
}
keywords := addMentionKeywordsForUser(
map[string][]string{},
user,
map[string]string{},
&model.Status{Status: model.StatusOnline}, // Assume the user is online since they would've triggered this
true, // Assume channel mentions are always allowed for simplicity
)
posts, nErr := a.Srv().Store().Post().GetPostsByThread(post.Id, timestamp)
if nErr != nil {
return 0, model.NewAppError("countThreadMentions", "app.channel.count_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
count := 0
if channel.Type == model.ChannelTypeDirect {
// In a DM channel, every post made by the other user is a mention
otherId := channel.GetOtherUserIdForDM(user.Id)
for _, p := range posts {
if p.UserId == otherId {
count++
}
}
return int64(count), nil
}
var team *model.Team
if teamID != "" {
team, err = a.GetTeam(teamID)
if err != nil {
return 0, err
}
}
groups, nErr := a.getGroupsAllowedForReferenceInChannel(channel, team)
if nErr != nil {
return 0, model.NewAppError("countThreadMentions", "app.channel.count_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
for _, p := range posts {
if p.CreateAt >= timestamp {
mentions := getExplicitMentions(p, keywords, groups)
if _, ok := mentions.Mentions[user.Id]; ok {
count += 1
}
}
}
return int64(count), nil
}
// countMentionsFromPost returns the number of posts in the post's channel that mention the user after and including the
// given post.
func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model.Post) (int, int, int, *model.AppError) {
channel, err := a.GetChannel(c, post.ChannelId)
if err != nil {
return 0, 0, 0, err
}
if channel.Type == model.ChannelTypeDirect {
// In a DM channel, every post made by the other user is a mention
count, countRoot, nErr := a.Srv().Store().Channel().CountPostsAfter(post.ChannelId, post.CreateAt-1, channel.GetOtherUserIdForDM(user.Id))
if nErr != nil {
return 0, 0, 0, model.NewAppError("countMentionsFromPost", "app.channel.count_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
var urgentCount int
if a.isPostPriorityEnabled() {
urgentCount, nErr = a.Srv().Store().Channel().CountUrgentPostsAfter(post.ChannelId, post.CreateAt-1, channel.GetOtherUserIdForDM(user.Id))
if nErr != nil {
return 0, 0, 0, model.NewAppError("countMentionsFromPost", "app.channel.count_urgent_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return count, countRoot, urgentCount, nil
}
channelMember, err := a.GetChannelMember(c, channel.Id, user.Id)
if err != nil {
return 0, 0, 0, err
}
keywords := addMentionKeywordsForUser(
map[string][]string{},
user,
channelMember.NotifyProps,
&model.Status{Status: model.StatusOnline}, // Assume the user is online since they would've triggered this
true, // Assume channel mentions are always allowed for simplicity
)
commentMentions := user.NotifyProps[model.CommentsNotifyProp]
checkForCommentMentions := commentMentions == model.CommentsNotifyRoot || commentMentions == model.CommentsNotifyAny
// A mapping of thread root IDs to whether or not a post in that thread mentions the user
mentionedByThread := make(map[string]bool)
thread, err := a.GetPostThread(post.Id, model.GetPostsOptions{}, user.Id)
if err != nil {
return 0, 0, 0, err
}
count := 0
countRoot := 0
urgentCount := 0
if isPostMention(user, post, keywords, thread.Posts, mentionedByThread, checkForCommentMentions) {
count += 1
if post.RootId == "" {
countRoot += 1
if a.isPostPriorityEnabled() {
priority, err := a.GetPriorityForPost(post.Id)
if err != nil {
return 0, 0, 0, err
}
if priority != nil && *priority.Priority == model.PostPriorityUrgent {
urgentCount += 1
}
}
}
}
page := 0
perPage := 200
for {
postList, err := a.GetPostsAfterPost(model.GetPostsOptions{
ChannelId: post.ChannelId,
PostId: post.Id,
Page: page,
PerPage: perPage,
})
if err != nil {
return 0, 0, 0, err
}
mentionPostIds := make([]string, 0)
for _, postID := range postList.Order {
if isPostMention(user, postList.Posts[postID], keywords, postList.Posts, mentionedByThread, checkForCommentMentions) {
count += 1
if postList.Posts[postID].RootId == "" {
mentionPostIds = append(mentionPostIds, postID)
countRoot += 1
}
}
}
if a.isPostPriorityEnabled() {
priorityList, nErr := a.Srv().Store().PostPriority().GetForPosts(mentionPostIds)
if nErr != nil {
return 0, 0, 0, model.NewAppError("countMentionsFromPost", "app.channel.get_priority_for_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
for _, priority := range priorityList {
if *priority.Priority == model.PostPriorityUrgent {
urgentCount += 1
}
}
}
if len(postList.Order) < perPage {
break
}
page += 1
}
return count, countRoot, urgentCount, nil
}
func isCommentMention(user *model.User, post *model.Post, otherPosts map[string]*model.Post, mentionedByThread map[string]bool) bool {
if post.RootId == "" {
// Not a comment
return false
}
if mentioned, ok := mentionedByThread[post.RootId]; ok {
// We've already figured out if the user was mentioned by this thread
return mentioned
}
// Whether or not the user was mentioned because they started the thread
mentioned := otherPosts[post.RootId].UserId == user.Id
// Or because they commented on it before this post
if !mentioned && user.NotifyProps[model.CommentsNotifyProp] == model.CommentsNotifyAny {
for _, otherPost := range otherPosts {
if otherPost.Id == post.Id {
continue
}
if otherPost.RootId != post.RootId {
continue
}
if otherPost.UserId == user.Id && otherPost.CreateAt < post.CreateAt {
// Found a comment made by the user from before this post
mentioned = true
break
}
}
}
mentionedByThread[post.RootId] = mentioned
return mentioned
}
func isPostMention(user *model.User, post *model.Post, keywords map[string][]string, otherPosts map[string]*model.Post, mentionedByThread map[string]bool, checkForCommentMentions bool) bool {
// Prevent the user from mentioning themselves
if post.UserId == user.Id && post.GetProp("from_webhook") != "true" {
return false
}
// Check for keyword mentions
mentions := getExplicitMentions(post, keywords, make(map[string]*model.Group))
if _, ok := mentions.Mentions[user.Id]; ok {
return true
}
// Check for mentions caused by being added to the channel
if post.Type == model.PostTypeAddToChannel {
if addedUserId, ok := post.GetProp(model.PostPropsAddedUserId).(string); ok && addedUserId == user.Id {
return true
}
}
// Check for comment mentions
if checkForCommentMentions && isCommentMention(user, post, otherPosts, mentionedByThread) {
return true
}
return false
}
func (a *App) GetThreadMembershipsForUser(userID, teamID string) ([]*model.ThreadMembership, error) {
return a.Srv().Store().Thread().GetMembershipsForUser(userID, teamID)
}
func (a *App) GetPostIfAuthorized(c request.CTX, postID string, session *model.Session, includeDeleted bool) (*model.Post, *model.AppError) {
post, err := a.GetSinglePost(postID, includeDeleted)
if err != nil {
return nil, err
}
channel, err := a.GetChannel(c, post.ChannelId)
if err != nil {
return nil, err
}
if !a.SessionHasPermissionToChannel(c, *session, channel.Id, model.PermissionReadChannel) {
if channel.Type == model.ChannelTypeOpen {
if !a.SessionHasPermissionToTeam(*session, channel.TeamId, model.PermissionReadPublicChannel) {
return nil, a.MakePermissionError(session, []*model.Permission{model.PermissionReadPublicChannel})
}
} else {
return nil, a.MakePermissionError(session, []*model.Permission{model.PermissionReadChannel})
}
}
return post, nil
}
// GetPostsByIds response bool value indicates, if the post is inaccessible due to cloud plan's limit.
func (a *App) GetPostsByIds(postIDs []string) ([]*model.Post, int64, *model.AppError) {
posts, err := a.Srv().Store().Post().GetPostsByIds(postIDs)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, 0, model.NewAppError("GetPostsByIds", "app.post.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, 0, model.NewAppError("GetPostsByIds", "app.post.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
posts, firstInaccessiblePostTime, appErr := a.getFilteredAccessiblePosts(posts, filterPostOptions{assumeSortedCreatedAt: true})
if appErr != nil {
return nil, 0, appErr
}
return posts, firstInaccessiblePostTime, nil
}
func (a *App) GetEditHistoryForPost(postID string) ([]*model.Post, *model.AppError) {
posts, err := a.Srv().Store().Post().GetEditHistoryForPost(postID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetEditHistoryForPost", "app.post.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetEditHistoryForPost", "app.post.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return posts, nil
}
func (a *App) GetTopThreadsForTeamSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "app.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topThreads, err := a.Srv().Store().Thread().GetTopThreadsForTeamSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "app.post.get_top_threads_for_team_since.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
topThreadsWithEmbedAndImage, err := includeEmbedsAndImages(a, c, topThreads, userID)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "app.post.get_top_threads_for_team_since.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return topThreadsWithEmbedAndImage, nil
}
func (a *App) GetTopThreadsForUserSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "app.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topThreads, err := a.Srv().Store().Thread().GetTopThreadsForUserSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "app.post.get_top_threads_for_team_since.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
topThreadsWithEmbedAndImage, err := includeEmbedsAndImages(a, c, topThreads, userID)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForUserSince", "app.post.get_top_threads_for_user_since.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return topThreadsWithEmbedAndImage, nil
}
func (a *App) GetTopDMsForUserSince(userID string, opts *model.InsightsOpts) (*model.TopDMList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopDMsForUserSince", "app.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topDMs, err := a.Srv().Store().Post().GetTopDMsForUserSince(userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopDMsForUserSince", "app.post.get_top_dms_for_user_since.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return topDMs, nil
}
func (a *App) SetPostReminder(postID, userID string, targetTime int64) *model.AppError {
// Store the reminder in the DB
reminder := &model.PostReminder{
PostId: postID,
UserId: userID,
TargetTime: targetTime,
}
err := a.Srv().Store().Post().SetPostReminder(reminder)
if err != nil {
return model.NewAppError("SetPostReminder", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
metadata, err := a.Srv().Store().Post().GetPostReminderMetadata(postID)
if err != nil {
return model.NewAppError("SetPostReminder", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
parsedTime := time.Unix(targetTime, 0).UTC().Format(time.RFC822)
siteURL := *a.Config().ServiceSettings.SiteURL
var permalink string
if metadata.TeamName == "" {
permalink = fmt.Sprintf("%s/pl/%s", siteURL, postID)
} else {
permalink = fmt.Sprintf("%s/%s/pl/%s", siteURL, metadata.TeamName, postID)
}
// Send an ack message.
ephemeralPost := &model.Post{
Type: model.PostTypeEphemeral,
Id: model.NewId(),
CreateAt: model.GetMillis(),
UserId: userID,
RootId: postID,
ChannelId: metadata.ChannelId,
// It's okay to keep this non-translated. This is just a fallback.
// The webapp will parse the timestamp and show that in user's local timezone.
Message: fmt.Sprintf("You will be reminded about %s by @%s at %s", permalink, metadata.Username, parsedTime),
Props: model.StringInterface{
"target_time": targetTime,
"team_name": metadata.TeamName,
"post_id": postID,
"username": metadata.Username,
"type": model.PostTypeReminder,
},
}
message := model.NewWebSocketEvent(model.WebsocketEventEphemeralMessage, "", ephemeralPost.ChannelId, userID, nil, "")
ephemeralPost = a.PreparePostForClientWithEmbedsAndImages(request.EmptyContext(a.Log()), ephemeralPost, true, false, true)
ephemeralPost = model.AddPostActionCookies(ephemeralPost, a.PostActionCookieSecret())
postJSON, jsonErr := ephemeralPost.ToJSON()
if jsonErr != nil {
mlog.Warn("Failed to encode post to JSON", mlog.Err(jsonErr))
}
message.Add("post", postJSON)
a.Publish(message)
return nil
}
func (a *App) CheckPostReminders() {
systemBot, appErr := a.GetSystemBot()
if appErr != nil {
mlog.Error("Failed to get system bot", mlog.Err(appErr))
return
}
// This will return the reminders and also delete them from the DB.
// In case, any of the next steps fail, those reminders would be lost.
// Alternatively, if we delete those reminders _after_ it has been sent,
// then in case of any temporary failure, they would get sent in the next batch.
// MM-45595.
reminders, err := a.Srv().Store().Post().GetPostReminders(time.Now().UTC().Unix())
if err != nil {
mlog.Error("Failed to get post reminders", mlog.Err(err))
return
}
// We group multiple reminders for a single user.
groupedReminders := make(map[string][]string)
for _, r := range reminders {
if groupedReminders[r.UserId] == nil {
groupedReminders[r.UserId] = []string{r.PostId}
} else {
groupedReminders[r.UserId] = append(groupedReminders[r.UserId], r.PostId)
}
}
siteURL := *a.Config().ServiceSettings.SiteURL
for userID, postIDs := range groupedReminders {
ch, appErr := a.GetOrCreateDirectChannel(request.EmptyContext(a.Log()), userID, systemBot.UserId)
if appErr != nil {
mlog.Error("Failed to get direct channel", mlog.Err(appErr))
return
}
for _, postID := range postIDs {
metadata, err := a.Srv().Store().Post().GetPostReminderMetadata(postID)
if err != nil {
mlog.Error("Failed to get post reminder metadata", mlog.Err(err), mlog.String("post_id", postID))
continue
}
T := i18n.GetUserTranslations(metadata.UserLocale)
dm := &model.Post{
ChannelId: ch.Id,
Message: T("app.post_reminder_dm", model.StringInterface{
"SiteURL": siteURL,
"TeamName": metadata.TeamName,
"PostId": postID,
"Username": metadata.Username,
}),
Type: model.PostTypeReminder,
UserId: systemBot.UserId,
Props: model.StringInterface{
"team_name": metadata.TeamName,
"post_id": postID,
"username": metadata.Username,
},
}
if _, err := a.CreatePost(request.EmptyContext(a.Log()), dm, ch, false, true); err != nil {
mlog.Error("Failed to post reminder message", mlog.Err(err))
}
}
}
}
func (a *App) GetPostInfo(c request.CTX, postID string) (*model.PostInfo, *model.AppError) {
userID := c.Session().UserId
post, appErr := a.GetSinglePost(postID, false)
if appErr != nil {
return nil, appErr
}
channel, appErr := a.GetChannel(c, post.ChannelId)
if appErr != nil {
return nil, appErr
}
notFoundError := model.NewAppError("GetPostInfo", "app.post.get.app_error", nil, "", http.StatusNotFound)
var team *model.Team
hasPermissionToAccessTeam := false
if channel.TeamId != "" {
team, appErr = a.GetTeam(channel.TeamId)
if appErr != nil {
return nil, appErr
}
if team.Type == model.TeamOpen {
hasPermissionToAccessTeam = a.HasPermissionToTeam(userID, team.Id, model.PermissionJoinPublicTeams)
} else if team.Type == model.TeamInvite {
hasPermissionToAccessTeam = a.HasPermissionToTeam(userID, team.Id, model.PermissionJoinPrivateTeams)
}
} else {
// This happens in case of DMs and GMs.
hasPermissionToAccessTeam = true
}
if !hasPermissionToAccessTeam {
return nil, notFoundError
}
hasPermissionToAccessChannel := false
if channel.Type == model.ChannelTypeOpen {
hasPermissionToAccessChannel = true
} else if channel.Type == model.ChannelTypePrivate {
hasPermissionToAccessChannel = a.HasPermissionToChannel(c, userID, channel.Id, model.PermissionManagePrivateChannelMembers)
} else if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
hasPermissionToAccessChannel = a.HasPermissionToChannel(c, userID, channel.Id, model.PermissionReadChannel)
}
if !hasPermissionToAccessChannel {
return nil, notFoundError
}
_, channelMemberErr := a.GetChannelMember(c, channel.Id, userID)
info := model.PostInfo{
ChannelId: channel.Id,
ChannelType: channel.Type,
ChannelDisplayName: channel.DisplayName,
HasJoinedChannel: channelMemberErr == nil,
}
if team != nil {
_, teamMemberErr := a.GetTeamMember(team.Id, userID)
info.TeamId = team.Id
info.TeamType = team.Type
info.TeamDisplayName = team.DisplayName
info.HasJoinedTeam = teamMemberErr == nil
}
return &info, nil
}
func includeEmbedsAndImages(a *App, c request.CTX, topThreadList *model.TopThreadList, userID string) (*model.TopThreadList, error) {
for _, topThread := range topThreadList.Items {
topThread.Post = a.PreparePostForClientWithEmbedsAndImages(c, topThread.Post, false, false, true)
sanitizedPost, err := a.SanitizePostMetadataForUser(c, topThread.Post, userID)
if err != nil {
return nil, err
}
topThread.Post = sanitizedPost
}
return topThreadList, nil
}
func (a *App) isPostPriorityEnabled() bool {
return a.Config().FeatureFlags.PostPriority && *a.Config().ServiceSettings.PostPriority
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) SaveAcknowledgementForPost(c *request.Context, postID, userID string) (*model.PostAcknowledgement, *model.AppError) {
post, err := a.GetSinglePost(postID, false)
if err != nil {
return nil, err
}
channel, err := a.GetChannel(c, post.ChannelId)
if err != nil {
return nil, err
}
if channel.DeleteAt > 0 {
return nil, model.NewAppError("SaveAcknowledgementForPost", "api.acknowledgement.save.archived_channel.app_error", nil, "", http.StatusForbidden)
}
acknowledgedAt := model.GetMillis()
acknowledgement, nErr := a.Srv().Store().PostAcknowledgement().Save(postID, userID, acknowledgedAt)
if nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("SaveAcknowledgementForPost", "app.acknowledgement.save.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
// The post is always modified since the UpdateAt always changes
a.invalidateCacheForChannelPosts(channel.Id)
a.Srv().Go(func() {
a.sendAcknowledgementEvent(model.WebsocketEventAcknowledgementAdded, acknowledgement, post)
})
return acknowledgement, nil
}
func (a *App) DeleteAcknowledgementForPost(c *request.Context, postID, userID string) *model.AppError {
post, err := a.GetSinglePost(postID, false)
if err != nil {
return err
}
channel, err := a.GetChannel(c, post.ChannelId)
if err != nil {
return err
}
if channel.DeleteAt > 0 {
return model.NewAppError("DeleteAcknowledgementForPost", "api.acknowledgement.delete.archived_channel.app_error", nil, "", http.StatusForbidden)
}
oldAck, nErr := a.Srv().Store().PostAcknowledgement().Get(postID, userID)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return model.NewAppError("GetPostAcknowledgement", "app.acknowledgement.get.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return model.NewAppError("GetPostAcknowledgement", "app.acknowledgement.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if model.GetMillis()-oldAck.AcknowledgedAt > 5*60*1000 {
return model.NewAppError("DeleteAcknowledgementForPost", "api.acknowledgement.delete.deadline.app_error", nil, "", http.StatusForbidden)
}
nErr = a.Srv().Store().PostAcknowledgement().Delete(oldAck)
if nErr != nil {
return model.NewAppError("DeleteAcknowledgementForPost", "app.acknowledgement.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
// The post is always modified since the UpdateAt always changes
a.invalidateCacheForChannelPosts(channel.Id)
a.Srv().Go(func() {
a.sendAcknowledgementEvent(model.WebsocketEventAcknowledgementRemoved, oldAck, post)
})
return nil
}
func (a *App) GetAcknowledgementsForPost(postID string) ([]*model.PostAcknowledgement, *model.AppError) {
acknowledgements, nErr := a.Srv().Store().PostAcknowledgement().GetForPost(postID)
if nErr != nil {
return nil, model.NewAppError("GetAcknowledgementsForPost", "app.acknowledgement.getforpost.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return acknowledgements, nil
}
func (a *App) GetAcknowledgementsForPostList(postList *model.PostList) (map[string][]*model.PostAcknowledgement, *model.AppError) {
acknowledgements, err := a.Srv().Store().PostAcknowledgement().GetForPosts(postList.Order)
if err != nil {
return nil, model.NewAppError("GetPostAcknowledgementsForPostList", "app.acknowledgement.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
acknowledgementsMap := make(map[string][]*model.PostAcknowledgement)
for _, ack := range acknowledgements {
acknowledgementsMap[ack.PostId] = append(acknowledgementsMap[ack.PostId], ack)
}
return acknowledgementsMap, nil
}
func (a *App) sendAcknowledgementEvent(event string, acknowledgement *model.PostAcknowledgement, post *model.Post) {
// send out that a acknowledgement has been added/removed
message := model.NewWebSocketEvent(event, "", post.ChannelId, "", nil, "")
acknowledgementJSON, err := json.Marshal(acknowledgement)
if err != nil {
a.Log().Warn("Failed to encode acknowledgement to JSON", mlog.Err(err))
}
message.Add("acknowledgement", string(acknowledgementJSON))
a.Publish(message)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"sort"
"github.com/mattermost/mattermost-server/v6/model"
)
type filterPostOptions struct {
assumeSortedCreatedAt bool
}
type accessibleBounds struct {
start int
end int
}
func (b accessibleBounds) allAccessible(lenPosts int) bool {
return b.start == allAccessibleBounds(lenPosts).start && b.end == allAccessibleBounds(lenPosts).end
}
func (b accessibleBounds) noAccessible() bool {
return b.start == noAccessibleBounds.start && b.end == noAccessibleBounds.end
}
// assumes checking was already performed that at least one post is inaccessible
func (b accessibleBounds) getInaccessibleRange(listLength int) (int, int) {
var start, end int
if b.start == 0 {
start = b.end + 1
end = listLength - 1
} else {
start = 0
end = b.start - 1
}
return start, end
}
var noAccessibleBounds = accessibleBounds{start: -1, end: -1}
var allAccessibleBounds = func(lenPosts int) accessibleBounds { return accessibleBounds{start: 0, end: lenPosts - 1} }
// getTimeSortedPostAccessibleBounds returns what the boundaries are for accessible posts.
// It assumes that CreateAt time for posts is monotonically increasing or decreasing.
// It could be either because posts can be returned in ascending or descending time order.
// Special values (which can be checked with methods `allAccessible` and `allInaccessible`)
// denote if all or none of the posts are accessible.
func getTimeSortedPostAccessibleBounds(earliestAccessibleTime int64, lenPosts int, getCreateAt func(int) int64) accessibleBounds {
if lenPosts == 0 {
return allAccessibleBounds(lenPosts)
}
if lenPosts == 1 {
if getCreateAt(0) >= earliestAccessibleTime {
return allAccessibleBounds(lenPosts)
}
return noAccessibleBounds
}
ascending := getCreateAt(0) < getCreateAt(lenPosts-1)
idx := sort.Search(lenPosts, func(i int) bool {
if ascending {
// Ascending order automatically picks the left most post(at idx),
// in case multiple posts at idx, idx+1, idx+2... have the same time.
return getCreateAt(i) >= earliestAccessibleTime
}
// Special case(subtracting 1) for descending order to include the right most post(at idx+k),
// in case multiple posts at idx, idx+1, idx+2...idx+k have the same time.
return getCreateAt(i) <= earliestAccessibleTime-1
})
if ascending {
if idx == lenPosts {
return noAccessibleBounds
}
return accessibleBounds{start: idx, end: lenPosts - 1}
}
if idx == 0 {
return noAccessibleBounds
}
return accessibleBounds{start: 0, end: idx - 1}
}
// linearFilterPostList make no assumptions about ordering, go through posts one by one
// this is the slower fallback that is still safe if we can not
// assume posts are ordered by CreatedAt
func linearFilterPostList(postList *model.PostList, earliestAccessibleTime int64) {
// filter Posts
posts := postList.Posts
order := postList.Order
n := 0
for i, postId := range order {
if createAt := posts[postId].CreateAt; createAt >= earliestAccessibleTime {
order[n] = order[i]
n++
} else {
if createAt > postList.FirstInaccessiblePostTime {
postList.FirstInaccessiblePostTime = createAt
}
delete(posts, postId)
}
}
postList.Order = order[:n]
// it can happen that some post list results don't have all posts in the Order field.
// for example GetPosts in the CollapsedThreads = false path, parents are not added
// to Order
for postId := range posts {
if createAt := posts[postId].CreateAt; createAt < earliestAccessibleTime {
if createAt > postList.FirstInaccessiblePostTime {
postList.FirstInaccessiblePostTime = createAt
}
delete(posts, postId)
}
}
}
// linearFilterPostsSlice make no assumptions about ordering, go through posts one by one
// this is the slower fallback that is still safe if we can not
// assume posts are ordered by CreatedAt
func linearFilterPostsSlice(posts []*model.Post, earliestAccessibleTime int64) ([]*model.Post, int64) {
var firstInaccessiblePostTime int64 = 0
n := 0
for i := range posts {
if createAt := posts[i].CreateAt; createAt >= earliestAccessibleTime {
posts[n] = posts[i]
n++
} else {
if createAt > firstInaccessiblePostTime {
firstInaccessiblePostTime = createAt
}
}
}
return posts[:n], firstInaccessiblePostTime
}
// filterInaccessiblePosts filters out the posts, past the cloud limit
func (a *App) filterInaccessiblePosts(postList *model.PostList, options filterPostOptions) *model.AppError {
if postList == nil || postList.Posts == nil || len(postList.Posts) == 0 {
return nil
}
lastAccessiblePostTime, appErr := a.GetLastAccessiblePostTime()
if appErr != nil {
return model.NewAppError("filterInaccessiblePosts", "app.last_accessible_post.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
}
if lastAccessiblePostTime == 0 {
// No need to filter, all posts are accessible
return nil
}
if len(postList.Posts) == len(postList.Order) && options.assumeSortedCreatedAt {
lenPosts := len(postList.Posts)
getCreateAt := func(i int) int64 { return postList.Posts[postList.Order[i]].CreateAt }
bounds := getTimeSortedPostAccessibleBounds(lastAccessiblePostTime, lenPosts, getCreateAt)
if bounds.allAccessible(lenPosts) {
return nil
}
if bounds.noAccessible() {
if lenPosts > 0 {
firstPostCreatedAt := postList.Posts[postList.Order[0]].CreateAt
lastPostCreatedAt := postList.Posts[postList.Order[len(postList.Order)-1]].CreateAt
postList.FirstInaccessiblePostTime = max(firstPostCreatedAt, lastPostCreatedAt)
}
postList.Posts = map[string]*model.Post{}
postList.Order = []string{}
return nil
}
startInaccessibleIndex, endInaccessibleIndex := bounds.getInaccessibleRange(len(postList.Order))
startInaccessibleCreatedAt := postList.Posts[postList.Order[startInaccessibleIndex]].CreateAt
endInaccessibleCreatedAt := postList.Posts[postList.Order[endInaccessibleIndex]].CreateAt
postList.FirstInaccessiblePostTime = max(startInaccessibleCreatedAt, endInaccessibleCreatedAt)
posts := postList.Posts
order := postList.Order
accessibleCount := bounds.end - bounds.start + 1
inaccessibleCount := lenPosts - accessibleCount
// Linearly cover shorter route to traverse posts map
if inaccessibleCount < accessibleCount {
for i := 0; i < bounds.start; i++ {
delete(posts, order[i])
}
for i := bounds.end + 1; i < lenPosts; i++ {
delete(posts, order[i])
}
} else {
accessiblePosts := make(map[string]*model.Post, accessibleCount)
for i := bounds.start; i <= bounds.end; i++ {
accessiblePosts[order[i]] = posts[order[i]]
}
postList.Posts = accessiblePosts
}
postList.Order = postList.Order[bounds.start : bounds.end+1]
} else {
linearFilterPostList(postList, lastAccessiblePostTime)
}
return nil
}
// isInaccessiblePost indicates if the post is past the cloud plan's limit.
func (a *App) isInaccessiblePost(post *model.Post) (int64, *model.AppError) {
if post == nil {
return 0, nil
}
pl := &model.PostList{
Order: []string{post.Id},
Posts: map[string]*model.Post{post.Id: post},
}
return pl.FirstInaccessiblePostTime, a.filterInaccessiblePosts(pl, filterPostOptions{assumeSortedCreatedAt: true})
}
// getFilteredAccessiblePosts returns accessible posts filtered as per the cloud plan's limit and also indicates if there were any inaccessible posts
func (a *App) getFilteredAccessiblePosts(posts []*model.Post, options filterPostOptions) ([]*model.Post, int64, *model.AppError) {
if len(posts) == 0 {
return posts, 0, nil
}
filteredPosts := []*model.Post{}
lastAccessiblePostTime, appErr := a.GetLastAccessiblePostTime()
if appErr != nil {
return filteredPosts, 0, model.NewAppError("getFilteredAccessiblePosts", "app.last_accessible_post.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
} else if lastAccessiblePostTime == 0 {
// No need to filter, all posts are accessible
return posts, 0, nil
}
if options.assumeSortedCreatedAt {
lenPosts := len(posts)
getCreateAt := func(i int) int64 { return posts[i].CreateAt }
bounds := getTimeSortedPostAccessibleBounds(lastAccessiblePostTime, lenPosts, getCreateAt)
if bounds.allAccessible(lenPosts) {
return posts, 0, nil
}
if bounds.noAccessible() {
var firstInaccessiblePostTime int64 = 0
if lenPosts > 0 {
firstPostCreatedAt := posts[0].CreateAt
lastPostCreatedAt := posts[len(posts)-1].CreateAt
firstInaccessiblePostTime = max(firstPostCreatedAt, lastPostCreatedAt)
}
return filteredPosts, firstInaccessiblePostTime, nil
}
startInaccessibleIndex, endInaccessibleIndex := bounds.getInaccessibleRange(len(posts))
firstPostCreatedAt := posts[startInaccessibleIndex].CreateAt
lastPostCreatedAt := posts[endInaccessibleIndex].CreateAt
firstInaccessiblePostTime := max(firstPostCreatedAt, lastPostCreatedAt)
filteredPosts = posts[bounds.start : bounds.end+1]
return filteredPosts, firstInaccessiblePostTime, nil
}
filteredPosts, firstInaccessiblePostTime := linearFilterPostsSlice(posts, lastAccessiblePostTime)
return filteredPosts, firstInaccessiblePostTime, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"fmt"
"image"
"io"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
"github.com/dyatlov/go-opengraph/opengraph"
"golang.org/x/net/idna"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/imgutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/markdown"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type linkMetadataCache struct {
OpenGraph *opengraph.OpenGraph
PostImage *model.PostImage
Permalink *model.Permalink
}
const MaxMetadataImageSize = MaxOpenGraphResponseSize
func (s *Server) initPostMetadata() {
// Dump any cached links if the proxy settings have changed so image URLs can be updated
s.platform.AddConfigListener(func(before, after *model.Config) {
if (before.ImageProxySettings.Enable != after.ImageProxySettings.Enable) ||
(before.ImageProxySettings.ImageProxyType != after.ImageProxySettings.ImageProxyType) ||
(before.ImageProxySettings.RemoteImageProxyURL != after.ImageProxySettings.RemoteImageProxyURL) ||
(before.ImageProxySettings.RemoteImageProxyOptions != after.ImageProxySettings.RemoteImageProxyOptions) {
platform.PurgeLinkCache()
}
})
}
func (a *App) PreparePostListForClient(c request.CTX, originalList *model.PostList) *model.PostList {
list := &model.PostList{
Posts: make(map[string]*model.Post, len(originalList.Posts)),
Order: originalList.Order,
NextPostId: originalList.NextPostId,
PrevPostId: originalList.PrevPostId,
HasNext: originalList.HasNext,
FirstInaccessiblePostTime: originalList.FirstInaccessiblePostTime,
}
for id, originalPost := range originalList.Posts {
post := a.PreparePostForClientWithEmbedsAndImages(c, originalPost, false, false, false)
list.Posts[id] = post
}
if a.isPostPriorityEnabled() {
priority, _ := a.GetPriorityForPostList(list)
acknowledgements, _ := a.GetAcknowledgementsForPostList(list)
for _, id := range list.Order {
if _, ok := priority[id]; ok {
list.Posts[id].Metadata.Priority = priority[id]
}
if _, ok := acknowledgements[id]; ok {
list.Posts[id].Metadata.Acknowledgements = acknowledgements[id]
}
}
}
return list
}
// OverrideIconURLIfEmoji changes the post icon override URL prop, if it has an emoji icon,
// so that it points to the URL (relative) of the emoji - static if emoji is default, /api if custom.
func (a *App) OverrideIconURLIfEmoji(c request.CTX, post *model.Post) {
prop, ok := post.GetProps()[model.PostPropsOverrideIconEmoji]
if !ok || prop == nil {
return
}
emojiName, ok := prop.(string)
if !ok {
return
}
if !*a.Config().ServiceSettings.EnablePostIconOverride || emojiName == "" {
return
}
emojiName = strings.ReplaceAll(emojiName, ":", "")
if emojiURL, err := a.GetEmojiStaticURL(c, emojiName); err == nil {
post.AddProp(model.PostPropsOverrideIconURL, emojiURL)
} else {
mlog.Warn("Failed to retrieve URL for overridden profile icon (emoji)", mlog.String("emojiName", emojiName), mlog.Err(err))
}
}
func (a *App) PreparePostForClient(c request.CTX, originalPost *model.Post, isNewPost, isEditPost, includePriority bool) *model.Post {
post := originalPost.Clone()
// Proxy image links before constructing metadata so that requests go through the proxy
post = a.PostWithProxyAddedToImageURLs(post)
a.OverrideIconURLIfEmoji(c, post)
if post.Metadata == nil {
post.Metadata = &model.PostMetadata{}
}
if post.DeleteAt > 0 {
// For deleted posts we don't fill out metadata nor do we return the post content
post.Message = ""
post.Metadata = &model.PostMetadata{}
return post
}
// Emojis and reaction counts
if emojis, reactions, err := a.getEmojisAndReactionsForPost(c, post); err != nil {
mlog.Warn("Failed to get emojis and reactions for a post", mlog.String("post_id", post.Id), mlog.Err(err))
} else {
post.Metadata.Emojis = emojis
post.Metadata.Reactions = reactions
}
// Files
if fileInfos, _, err := a.getFileMetadataForPost(post, isNewPost || isEditPost); err != nil {
mlog.Warn("Failed to get files for a post", mlog.String("post_id", post.Id), mlog.Err(err))
} else {
post.Metadata.Files = fileInfos
}
if includePriority && a.isPostPriorityEnabled() && post.RootId == "" {
// Post's Priority if any
if priority, err := a.GetPriorityForPost(post.Id); err != nil {
mlog.Warn("Failed to get post priority for a post", mlog.String("post_id", post.Id), mlog.Err(err))
} else {
post.Metadata.Priority = priority
}
// Post's acknowledgements if any
if acknowledgements, err := a.GetAcknowledgementsForPost(post.Id); err != nil {
mlog.Warn("Failed to get post acknowledgements for a post", mlog.String("post_id", post.Id), mlog.Err(err))
} else {
post.Metadata.Acknowledgements = acknowledgements
}
}
return post
}
func (a *App) PreparePostForClientWithEmbedsAndImages(c request.CTX, originalPost *model.Post, isNewPost, isEditPost, includePriority bool) *model.Post {
post := a.PreparePostForClient(c, originalPost, isNewPost, isEditPost, includePriority)
post = a.getEmbedsAndImages(c, post, isNewPost)
return post
}
func (a *App) getEmbedsAndImages(c request.CTX, post *model.Post, isNewPost bool) *model.Post {
if post.Metadata == nil {
post.Metadata = &model.PostMetadata{}
}
// Embeds and image dimensions
firstLink, images := a.getFirstLinkAndImages(post.Message)
if post.Metadata.Embeds == nil {
post.Metadata.Embeds = []*model.PostEmbed{}
}
if embed, err := a.getEmbedForPost(c, post, firstLink, isNewPost); err != nil {
appErr, ok := err.(*model.AppError)
isNotFound := ok && appErr.StatusCode == http.StatusNotFound
// Ignore NotFound errors.
if !isNotFound {
mlog.Debug("Failed to get embedded content for a post", mlog.String("post_id", post.Id), mlog.Err(err))
}
} else if embed != nil {
post.Metadata.Embeds = append(post.Metadata.Embeds, embed)
}
post.Metadata.Images = a.getImagesForPost(c, post, images, isNewPost)
return post
}
func (a *App) sanitizePostMetadataForUserAndChannel(c request.CTX, post *model.Post, previewedPost *model.PreviewPost, previewedChannel *model.Channel, userID string) *model.Post {
if post.Metadata == nil || len(post.Metadata.Embeds) == 0 || previewedPost == nil {
return post
}
if previewedChannel != nil && !a.HasPermissionToReadChannel(c, userID, previewedChannel) {
post.Metadata.Embeds[0].Data = nil
}
return post
}
func (a *App) SanitizePostMetadataForUser(c request.CTX, post *model.Post, userID string) (*model.Post, *model.AppError) {
if post.Metadata == nil || len(post.Metadata.Embeds) == 0 {
return post, nil
}
previewPost := post.GetPreviewPost()
if previewPost == nil {
return post, nil
}
previewedChannel, err := a.GetChannel(c, previewPost.Post.ChannelId)
if err != nil {
return nil, err
}
if previewedChannel != nil && !a.HasPermissionToReadChannel(c, userID, previewedChannel) {
for _, embed := range post.Metadata.Embeds {
embed.Data = nil
}
}
return post, nil
}
func (a *App) SanitizePostListMetadataForUser(c request.CTX, postList *model.PostList, userID string) (*model.PostList, *model.AppError) {
clonedPostList := postList.Clone()
for postID, post := range clonedPostList.Posts {
sanitizedPost, err := a.SanitizePostMetadataForUser(c, post, userID)
if err != nil {
return nil, err
}
clonedPostList.Posts[postID] = sanitizedPost
}
return clonedPostList, nil
}
func (a *App) getFileMetadataForPost(post *model.Post, fromMaster bool) ([]*model.FileInfo, int64, *model.AppError) {
if len(post.FileIds) == 0 {
return nil, 0, nil
}
return a.GetFileInfosForPost(post.Id, fromMaster, false)
}
func (a *App) getEmojisAndReactionsForPost(c request.CTX, post *model.Post) ([]*model.Emoji, []*model.Reaction, *model.AppError) {
var reactions []*model.Reaction
if post.HasReactions {
var err *model.AppError
reactions, err = a.GetReactionsForPost(post.Id)
if err != nil {
return nil, nil, err
}
}
emojis, err := a.getCustomEmojisForPost(c, post, reactions)
if err != nil {
return nil, nil, err
}
return emojis, reactions, nil
}
func (a *App) getEmbedForPost(c request.CTX, post *model.Post, firstLink string, isNewPost bool) (*model.PostEmbed, error) {
if _, ok := post.GetProps()["attachments"]; ok {
return &model.PostEmbed{
Type: model.PostEmbedMessageAttachment,
}, nil
}
if _, ok := post.GetProps()["boards"]; ok {
return &model.PostEmbed{
Type: model.PostEmbedBoards,
Data: post.GetProps()["boards"],
}, nil
}
if firstLink == "" {
return nil, nil
}
// Permalink previews are not toggled via the ServiceSettings.EnableLinkPreviews config setting.
if !*a.Config().ServiceSettings.EnableLinkPreviews && !looksLikeAPermalink(firstLink, *a.Config().ServiceSettings.SiteURL) {
return nil, nil
}
og, image, permalink, err := a.getLinkMetadata(c, firstLink, post.CreateAt, isNewPost, post.GetPreviewedPostProp())
if err != nil {
return nil, err
}
if !*a.Config().ServiceSettings.EnablePermalinkPreviews || !a.Config().FeatureFlags.PermalinkPreviews {
permalink = nil
}
if og != nil {
return &model.PostEmbed{
Type: model.PostEmbedOpengraph,
URL: firstLink,
Data: og,
}, nil
}
if image != nil {
// Note that we're not passing the image info here since it'll be part of the PostMetadata.Images field
return &model.PostEmbed{
Type: model.PostEmbedImage,
URL: firstLink,
}, nil
}
if permalink != nil {
return &model.PostEmbed{Type: model.PostEmbedPermalink, Data: permalink.PreviewPost}, nil
}
return &model.PostEmbed{
Type: model.PostEmbedLink,
URL: firstLink,
}, nil
}
func (a *App) getImagesForPost(c request.CTX, post *model.Post, imageURLs []string, isNewPost bool) map[string]*model.PostImage {
images := map[string]*model.PostImage{}
for _, embed := range post.Metadata.Embeds {
switch embed.Type {
case model.PostEmbedImage:
// These dimensions will generally be cached by a previous call to getEmbedForPost
imageURLs = append(imageURLs, embed.URL)
case model.PostEmbedMessageAttachment:
imageURLs = append(imageURLs, a.getImagesInMessageAttachments(post)...)
case model.PostEmbedOpengraph:
openGraph, ok := embed.Data.(*opengraph.OpenGraph)
if !ok {
mlog.Warn("Could not read the image data: the data could not be casted to OpenGraph",
mlog.String("post_id", post.Id), mlog.String("data type", fmt.Sprintf("%t", embed.Data)))
continue
}
for _, image := range openGraph.Images {
var imageURL string
if image.SecureURL != "" {
imageURL = image.SecureURL
} else if image.URL != "" {
imageURL = image.URL
}
if imageURL == "" {
continue
}
imageURLs = append(imageURLs, imageURL)
}
}
}
// Removing duplicates isn't strictly since images is a map, but it feels safer to do it beforehand
if len(imageURLs) > 1 {
imageURLs = model.RemoveDuplicateStrings(imageURLs)
}
for _, imageURL := range imageURLs {
if _, image, _, err := a.getLinkMetadata(c, imageURL, post.CreateAt, isNewPost, post.GetPreviewedPostProp()); err != nil {
appErr, ok := err.(*model.AppError)
isNotFound := ok && appErr.StatusCode == http.StatusNotFound
// Ignore NotFound errors.
if !isNotFound {
mlog.Debug("Failed to get dimensions of an image in a post",
mlog.String("post_id", post.Id), mlog.String("image_url", imageURL), mlog.Err(err))
}
} else if image != nil {
images[imageURL] = image
}
}
return images
}
func getEmojiNamesForString(s string) []string {
names := model.EmojiPattern.FindAllString(s, -1)
for i, name := range names {
names[i] = strings.Trim(name, ":")
}
return names
}
func getEmojiNamesForPost(post *model.Post, reactions []*model.Reaction) []string {
// Post message
names := getEmojiNamesForString(post.Message)
// Reactions
for _, reaction := range reactions {
names = append(names, reaction.EmojiName)
}
// Post attachments
for _, attachment := range post.Attachments() {
if attachment.Title != "" {
names = append(names, getEmojiNamesForString(attachment.Title)...)
}
if attachment.Text != "" {
names = append(names, getEmojiNamesForString(attachment.Text)...)
}
if attachment.Pretext != "" {
names = append(names, getEmojiNamesForString(attachment.Pretext)...)
}
for _, field := range attachment.Fields {
if field == nil {
continue
}
if value, ok := field.Value.(string); ok {
names = append(names, getEmojiNamesForString(value)...)
}
}
}
// Remove duplicates
names = model.RemoveDuplicateStrings(names)
return names
}
func (a *App) getCustomEmojisForPost(c request.CTX, post *model.Post, reactions []*model.Reaction) ([]*model.Emoji, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCustomEmoji {
// Only custom emoji are returned
return []*model.Emoji{}, nil
}
names := getEmojiNamesForPost(post, reactions)
if len(names) == 0 {
return []*model.Emoji{}, nil
}
return a.GetMultipleEmojiByName(c, names)
}
func (a *App) isLinkAllowedForPreview(link string) bool {
domains := normalizeDomains(*a.Config().ServiceSettings.RestrictLinkPreviews)
for _, d := range domains {
parsed, err := url.Parse(link)
if err != nil {
a.Log().Warn("Unable to parse the link", mlog.String("link", link), mlog.Err(err))
// We disable link preview if link is badly formed
// to remain on the safe side
return false
}
// Conforming to IDNA2008 using the UTS-46 standard.
cleaned, err := idna.Lookup.ToASCII(parsed.Hostname())
if err != nil {
a.Log().Warn("Unable to lookup hostname to ASCII", mlog.String("hostname", parsed.Hostname()), mlog.Err(err))
// Same applies if compatibility processing fails.
return false
}
if strings.Contains(cleaned, d) {
return false
}
}
return true
}
func normalizeDomains(domains string) []string {
// commas and @ signs are optional
// can be in the form of "@corp.mattermost.com, mattermost.com mattermost.org" -> corp.mattermost.com mattermost.com mattermost.org
return strings.Fields(
strings.TrimSpace(
strings.ToLower(
strings.ReplaceAll(
strings.ReplaceAll(domains, "@", " "),
",", " "),
),
),
)
}
// Given a string, returns the first autolinked URL in the string as well as an array of all Markdown
// images of the form . Note that this does not return Markdown links of the
// form [text](url).
func (a *App) getFirstLinkAndImages(str string) (string, []string) {
firstLink := ""
images := []string{}
markdown.Inspect(str, func(blockOrInline any) bool {
switch v := blockOrInline.(type) {
case *markdown.Autolink:
if link := v.Destination(); firstLink == "" && a.isLinkAllowedForPreview(link) {
firstLink = link
}
case *markdown.InlineImage:
if link := v.Destination(); a.isLinkAllowedForPreview(link) {
images = append(images, link)
}
case *markdown.ReferenceImage:
if link := v.ReferenceDefinition.Destination(); a.isLinkAllowedForPreview(link) {
images = append(images, link)
}
}
return true
})
return firstLink, images
}
func (a *App) getImagesInMessageAttachments(post *model.Post) []string {
var images []string
for _, attachment := range post.Attachments() {
_, imagesInText := a.getFirstLinkAndImages(attachment.Text)
images = append(images, imagesInText...)
_, imagesInPretext := a.getFirstLinkAndImages(attachment.Pretext)
images = append(images, imagesInPretext...)
for _, field := range attachment.Fields {
if field == nil {
continue
}
if value, ok := field.Value.(string); ok {
_, imagesInFieldValue := a.getFirstLinkAndImages(value)
images = append(images, imagesInFieldValue...)
}
}
if attachment.AuthorIcon != "" {
images = append(images, attachment.AuthorIcon)
}
if attachment.ImageURL != "" {
images = append(images, attachment.ImageURL)
}
if attachment.ThumbURL != "" {
images = append(images, attachment.ThumbURL)
}
if attachment.FooterIcon != "" {
images = append(images, attachment.FooterIcon)
}
}
return images
}
func looksLikeAPermalink(url, siteURL string) bool {
expression := fmt.Sprintf(`^(%s).*(/pl/)[a-z0-9]{26}$`, siteURL)
matched, err := regexp.MatchString(expression, strings.TrimSpace(url))
if err != nil {
mlog.Warn("error matching regex", mlog.Err(err))
}
return matched
}
func (a *App) containsPermalink(post *model.Post) bool {
link, _ := a.getFirstLinkAndImages(post.Message)
if link == "" {
return false
}
return looksLikeAPermalink(link, a.GetSiteURL())
}
func (a *App) getLinkMetadata(c request.CTX, requestURL string, timestamp int64, isNewPost bool, previewedPostPropVal string) (*opengraph.OpenGraph, *model.PostImage, *model.Permalink, error) {
requestURL = resolveMetadataURL(requestURL, a.GetSiteURL())
timestamp = model.FloorToNearestHour(timestamp)
// Check cache
og, image, permalink, ok := getLinkMetadataFromCache(requestURL, timestamp)
if !*a.Config().ServiceSettings.EnablePermalinkPreviews || !a.Config().FeatureFlags.PermalinkPreviews {
permalink = nil
}
if ok && previewedPostPropVal == "" {
return og, image, permalink, nil
}
// Check the database if this isn't a new post. If it is a new post and the data is cached, it should be in memory.
if !isNewPost {
og, image, ok = a.getLinkMetadataFromDatabase(requestURL, timestamp)
if ok && previewedPostPropVal == "" {
cacheLinkMetadata(requestURL, timestamp, og, image, nil)
return og, image, nil, nil
}
}
var err error
if looksLikeAPermalink(requestURL, a.GetSiteURL()) && *a.Config().ServiceSettings.EnablePermalinkPreviews && a.Config().FeatureFlags.PermalinkPreviews {
referencedPostID := requestURL[len(requestURL)-26:]
referencedPost, appErr := a.GetSinglePost(referencedPostID, false)
// TODO: Look into saving a value in the LinkMetadata.Data field to prevent perpetually re-querying for the deleted post.
if appErr != nil {
return nil, nil, nil, appErr
}
referencedChannel, appErr := a.GetChannel(c, referencedPost.ChannelId)
if appErr != nil {
return nil, nil, nil, appErr
}
var referencedTeam *model.Team
if referencedChannel.Type == model.ChannelTypeDirect || referencedChannel.Type == model.ChannelTypeGroup {
referencedTeam = &model.Team{}
} else {
referencedTeam, appErr = a.GetTeam(referencedChannel.TeamId)
if appErr != nil {
return nil, nil, nil, appErr
}
}
// Get metadata for embedded post
if a.containsPermalink(referencedPost) {
// referencedPost contains a permalink: we don't get its metadata
permalink = &model.Permalink{PreviewPost: model.NewPreviewPost(referencedPost, referencedTeam, referencedChannel)}
} else {
// referencedPost does not contain a permalink: we get its metadata
referencedPostWithMetadata := a.PreparePostForClientWithEmbedsAndImages(c, referencedPost, false, false, false)
permalink = &model.Permalink{PreviewPost: model.NewPreviewPost(referencedPostWithMetadata, referencedTeam, referencedChannel)}
}
} else {
var request *http.Request
// Make request for a web page or an image
request, err = http.NewRequest("GET", requestURL, nil)
if err != nil {
return nil, nil, nil, err
}
var body io.ReadCloser
var contentType string
if (request.URL.Scheme+"://"+request.URL.Host) == a.GetSiteURL() && request.URL.Path == "/api/v4/image" {
// /api/v4/image requires authentication, so bypass the API by hitting the proxy directly
body, contentType, err = a.ImageProxy().GetImageDirect(a.ImageProxy().GetUnproxiedImageURL(request.URL.String()))
} else {
request.Header.Add("Accept", "image/*")
request.Header.Add("Accept", "text/html;q=0.8")
request.Header.Add("Accept-Language", *a.Config().LocalizationSettings.DefaultServerLocale)
client := a.HTTPService().MakeClient(false)
client.Timeout = time.Duration(*a.Config().ExperimentalSettings.LinkMetadataTimeoutMilliseconds) * time.Millisecond
var res *http.Response
res, err = client.Do(request)
if res != nil {
body = res.Body
contentType = res.Header.Get("Content-Type")
}
}
if body != nil {
defer func() {
io.Copy(io.Discard, body)
body.Close()
}()
}
if err == nil {
// Parse the data
og, image, err = a.parseLinkMetadata(requestURL, body, contentType)
}
og = model.TruncateOpenGraph(og) // remove unwanted length of texts
a.saveLinkMetadataToDatabase(requestURL, timestamp, og, image)
}
// Write back to cache and database, even if there was an error and the results are nil
cacheLinkMetadata(requestURL, timestamp, og, image, permalink)
return og, image, permalink, err
}
// resolveMetadataURL resolves a given URL relative to the server's site URL.
func resolveMetadataURL(requestURL string, siteURL string) string {
base, err := url.Parse(siteURL)
if err != nil {
return ""
}
resolved, err := base.Parse(requestURL)
if err != nil {
return ""
}
return resolved.String()
}
func getLinkMetadataFromCache(requestURL string, timestamp int64) (*opengraph.OpenGraph, *model.PostImage, *model.Permalink, bool) {
var cached linkMetadataCache
err := platform.LinkCache().Get(strconv.FormatInt(model.GenerateLinkMetadataHash(requestURL, timestamp), 16), &cached)
if err != nil {
return nil, nil, nil, false
}
return cached.OpenGraph, cached.PostImage, cached.Permalink, true
}
func (a *App) getLinkMetadataFromDatabase(requestURL string, timestamp int64) (*opengraph.OpenGraph, *model.PostImage, bool) {
linkMetadata, err := a.Srv().Store().LinkMetadata().Get(requestURL, timestamp)
if err != nil {
return nil, nil, false
}
data := linkMetadata.Data
switch v := data.(type) {
case *opengraph.OpenGraph:
return v, nil, true
case *model.PostImage:
return nil, v, true
default:
return nil, nil, true
}
}
func (a *App) saveLinkMetadataToDatabase(requestURL string, timestamp int64, og *opengraph.OpenGraph, image *model.PostImage) {
metadata := &model.LinkMetadata{
URL: requestURL,
Timestamp: timestamp,
}
if og != nil {
metadata.Type = model.LinkMetadataTypeOpengraph
metadata.Data = og
} else if image != nil {
metadata.Type = model.LinkMetadataTypeImage
metadata.Data = image
} else {
metadata.Type = model.LinkMetadataTypeNone
}
_, err := a.Srv().Store().LinkMetadata().Save(metadata)
if err != nil {
mlog.Warn("Failed to write link metadata", mlog.String("request_url", requestURL), mlog.Err(err))
}
}
func cacheLinkMetadata(requestURL string, timestamp int64, og *opengraph.OpenGraph, image *model.PostImage, permalink *model.Permalink) {
metadata := linkMetadataCache{
OpenGraph: og,
PostImage: image,
Permalink: permalink,
}
platform.LinkCache().SetWithExpiry(strconv.FormatInt(model.GenerateLinkMetadataHash(requestURL, timestamp), 16), metadata, platform.LinkCacheDuration)
}
func (a *App) parseLinkMetadata(requestURL string, body io.Reader, contentType string) (*opengraph.OpenGraph, *model.PostImage, error) {
if contentType == "image/svg+xml" {
image := &model.PostImage{
Format: "svg",
}
return nil, image, nil
} else if strings.HasPrefix(contentType, "image") {
image, err := parseImages(io.LimitReader(body, MaxMetadataImageSize))
return nil, image, err
} else if strings.HasPrefix(contentType, "text/html") {
og := a.parseOpenGraphMetadata(requestURL, body, contentType)
// The OpenGraph library and Go HTML library don't error for malformed input, so check that at least
// one of these required fields exists before returning the OpenGraph data
if og.Title != "" || og.Type != "" || og.URL != "" {
return og, nil, nil
}
return nil, nil, nil
} else {
// Not an image or web page with OpenGraph information
return nil, nil, nil
}
}
func parseImages(body io.Reader) (*model.PostImage, error) {
// Store any data that is read for the config for any further processing
buf := &bytes.Buffer{}
t := io.TeeReader(body, buf)
// Read the image config to get the format and dimensions
config, format, err := image.DecodeConfig(t)
if err != nil {
return nil, err
}
image := &model.PostImage{
Width: config.Width,
Height: config.Height,
Format: format,
}
if format == "gif" {
// Decoding the config may have read some of the image data, so re-read the data that has already been read first
frameCount, err := imgutils.CountGIFFrames(io.MultiReader(buf, body))
if err != nil {
return nil, err
}
image.FrameCount = frameCount
}
// Make image information nil when the format is tiff
if format == "tiff" {
image = nil
}
return image, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"database/sql"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
)
func (a *App) GetPriorityForPost(postId string) (*model.PostPriority, *model.AppError) {
priority, err := a.Srv().Store().PostPriority().GetForPost(postId)
if err != nil && err != sql.ErrNoRows {
return nil, model.NewAppError("GetPriorityForPost", "app.post_prority.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return priority, nil
}
func (a *App) GetPriorityForPostList(list *model.PostList) (map[string]*model.PostPriority, *model.AppError) {
priority, err := a.Srv().Store().PostPriority().GetForPosts(list.Order)
if err != nil {
return nil, model.NewAppError("GetPriorityForPost", "app.post_prority.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
priorityMap := make(map[string]*model.PostPriority)
for _, p := range priority {
priorityMap[p.PostId] = p
}
return priorityMap, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
)
// Ensure preferences service wrapper implements `product.PreferencesService`
var _ product.PreferencesService = (*preferencesServiceWrapper)(nil)
// preferencesServiceWrapper provides an implementation of `product.PreferencesService` for use by products.
type preferencesServiceWrapper struct {
app AppIface
}
func (w *preferencesServiceWrapper) GetPreferencesForUser(userID string) (model.Preferences, *model.AppError) {
return w.app.GetPreferencesForUser(userID)
}
func (w *preferencesServiceWrapper) UpdatePreferencesForUser(userID string, preferences model.Preferences) *model.AppError {
return w.app.UpdatePreferences(userID, preferences)
}
func (w *preferencesServiceWrapper) DeletePreferencesForUser(userID string, preferences model.Preferences) *model.AppError {
return w.app.DeletePreferences(userID, preferences)
}
func (a *App) GetPreferencesForUser(userID string) (model.Preferences, *model.AppError) {
preferences, err := a.Srv().Store().Preference().GetAll(userID)
if err != nil {
return nil, model.NewAppError("GetPreferencesForUser", "app.preference.get_all.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
return preferences, nil
}
func (a *App) GetPreferenceByCategoryForUser(userID string, category string) (model.Preferences, *model.AppError) {
preferences, err := a.Srv().Store().Preference().GetCategory(userID, category)
if err != nil {
return nil, model.NewAppError("GetPreferenceByCategoryForUser", "app.preference.get_category.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if len(preferences) == 0 {
err := model.NewAppError("GetPreferenceByCategoryForUser", "api.preference.preferences_category.get.app_error", nil, "", http.StatusNotFound)
return nil, err
}
return preferences, nil
}
func (a *App) GetPreferenceByCategoryAndNameForUser(userID string, category string, preferenceName string) (*model.Preference, *model.AppError) {
res, err := a.Srv().Store().Preference().Get(userID, category, preferenceName)
if err != nil {
return nil, model.NewAppError("GetPreferenceByCategoryAndNameForUser", "app.preference.get.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
return res, nil
}
func (a *App) UpdatePreferences(userID string, preferences model.Preferences) *model.AppError {
for _, preference := range preferences {
if userID != preference.UserId {
return model.NewAppError("savePreferences", "api.preference.update_preferences.set.app_error", nil,
"userId="+userID+", preference.UserId="+preference.UserId, http.StatusForbidden)
}
}
if err := a.Srv().Store().Preference().Save(preferences); err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return appErr
default:
return model.NewAppError("UpdatePreferences", "app.preference.save.updating.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
}
if err := a.Srv().Store().Channel().UpdateSidebarChannelsByPreferences(preferences); err != nil {
return model.NewAppError("UpdatePreferences", "api.preference.update_preferences.update_sidebar.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
message := model.NewWebSocketEvent(model.WebsocketEventSidebarCategoryUpdated, "", "", userID, nil, "")
// TODO this needs to be updated to include information on which categories changed
a.Publish(message)
message = model.NewWebSocketEvent(model.WebsocketEventPreferencesChanged, "", "", userID, nil, "")
prefsJSON, jsonErr := json.Marshal(preferences)
if jsonErr != nil {
return model.NewAppError("UpdatePreferences", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("preferences", string(prefsJSON))
a.Publish(message)
return nil
}
func (a *App) DeletePreferences(userID string, preferences model.Preferences) *model.AppError {
for _, preference := range preferences {
if userID != preference.UserId {
err := model.NewAppError("DeletePreferences", "api.preference.delete_preferences.delete.app_error", nil,
"userId="+userID+", preference.UserId="+preference.UserId, http.StatusForbidden)
return err
}
}
for _, preference := range preferences {
if err := a.Srv().Store().Preference().Delete(userID, preference.Category, preference.Name); err != nil {
return model.NewAppError("DeletePreferences", "app.preference.delete.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
}
if err := a.Srv().Store().Channel().DeleteSidebarChannelsByPreferences(preferences); err != nil {
return model.NewAppError("DeletePreferences", "api.preference.delete_preferences.update_sidebar.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
message := model.NewWebSocketEvent(model.WebsocketEventSidebarCategoryUpdated, "", "", userID, nil, "")
// TODO this needs to be updated to include information on which categories changed
a.Publish(message)
message = model.NewWebSocketEvent(model.WebsocketEventPreferencesDeleted, "", "", userID, nil, "")
prefsJSON, jsonErr := json.Marshal(preferences)
if jsonErr != nil {
return model.NewAppError("DeletePreferences", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("preferences", string(prefsJSON))
a.Publish(message)
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"fmt"
"os"
"strings"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
)
func (s *Server) initializeProducts(
productMap map[string]product.Manifest,
serviceMap map[product.ServiceKey]any,
) error {
// create a product map to consume
pmap := make(map[string]struct{})
for name := range productMap {
if !s.shouldStart(name) {
continue
}
pmap[name] = struct{}{}
}
// We figure out the initialization order by trial and error fashion hence maxTry
// is the maximum possible trials of initialization attempts. The order is not
// determined elsewhere therefore we do a on the fly sorting here. Which means the
// initialization order will be resolved during the loop.
maxTry := len(pmap) * len(pmap)
for len(pmap) > 0 && maxTry != 0 {
initLoop:
for product := range pmap {
manifest := productMap[product]
// we have dependencies defined. Here we check if the serviceMap
// has all the dependencies registered. If not, we continue to the
// loop to let other products initialize and register their services
// if they have any.
for key := range manifest.Dependencies {
if _, ok := serviceMap[key]; !ok {
maxTry--
continue initLoop
}
}
// some products can register themselves/their services
initializer := manifest.Initializer
prod, err := initializer(serviceMap)
if err != nil {
return fmt.Errorf("error initializing product %q: %w", product, err)
}
s.products[product] = prod
// we remove this product from the map to not try to initialize it again
delete(pmap, product)
}
}
if maxTry == 0 && len(pmap) != 0 {
var products string
for p := range pmap {
products = strings.Join([]string{products, fmt.Sprintf("%q", p)}, " ")
}
return fmt.Errorf("could not initialize product(s) due to circular dependency: %s", products)
}
return nil
}
func (s *Server) shouldStart(product string) bool {
if product == "boards" {
if !s.Config().FeatureFlags.BoardsProduct {
s.Log().Warn("Skipping boards start: not enabled via feature flag")
return false
}
}
if product == "playbooks" {
if os.Getenv("MM_DISABLE_PLAYBOOKS") == "true" {
s.Log().Warn("Skipping playbooks start: disabled via env var")
return false
}
}
return true
}
func (s *Server) HasBoardProduct() (bool, error) {
prod, exists := s.services[product.BoardsKey]
if !exists {
return false, nil
}
if prod == nil {
return false, errors.New("board product is nil")
}
if _, ok := prod.(product.BoardsService); !ok {
return false, errors.New("board product key does not match its definition")
}
return true, nil
}
func (a *App) HasBoardProduct() (bool, error) {
return a.Srv().HasBoardProduct()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"github.com/Masterminds/semver/v3"
"github.com/pkg/errors"
date_constraints "github.com/reflog/dateconstraints"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const MaxRepeatViewings = 3
const MinSecondsBetweenRepeatViewings = 60 * 60
// http request cache
var noticesCache = utils.RequestCache{}
var rcStripRegexp = regexp.MustCompile(`(.*?)(-rc\d+)(.*?)`)
func cleanupVersion(originalVersion string) string {
// clean up BuildNumber to remove release- prefix, -rc suffix and a hash part of the version
version := strings.Replace(originalVersion, "release-", "", 1)
version = rcStripRegexp.ReplaceAllString(version, `$1$3`)
versionParts := strings.Split(version, ".")
var versionPartsOut []string
for _, part := range versionParts {
if _, err := strconv.ParseInt(part, 10, 16); err == nil {
versionPartsOut = append(versionPartsOut, part)
}
}
return strings.Join(versionPartsOut, ".")
}
func noticeMatchesConditions(config *model.Config, preferences store.PreferenceStore, userID string,
client model.NoticeClientType, clientVersion string, postCount int64, userCount int64, isSystemAdmin bool,
isTeamAdmin bool, isCloud bool, sku, dbName, dbVer, searchEngineName, searchEngineVer string,
notice *model.ProductNotice) (bool, error) {
cnd := notice.Conditions
// check client type
if cnd.ClientType != nil {
if !cnd.ClientType.Matches(client) {
return false, nil
}
}
// check if client version is in notice range
clientVersions := cnd.DesktopVersion
if client == model.NoticeClientTypeMobileAndroid || client == model.NoticeClientTypeMobileIos {
clientVersions = cnd.MobileVersion
}
clientVersionParsed, err := semver.NewVersion(clientVersion)
if err != nil {
return false, errors.Wrapf(err, "Cannot parse version range %s", clientVersion)
}
for _, v := range clientVersions {
c, err2 := semver.NewConstraint(v)
if err2 != nil {
return false, errors.Wrapf(err2, "Cannot parse version range %s", v)
}
if !c.Check(clientVersionParsed) {
return false, nil
}
}
// check if notice date range matches current
if cnd.DisplayDate != nil {
y, m, d := time.Now().UTC().Date()
trunc := time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
c, err2 := date_constraints.NewConstraint(*cnd.DisplayDate)
if err2 != nil {
return false, errors.Wrapf(err2, "Cannot parse date range %s", *cnd.DisplayDate)
}
if !c.Check(&trunc) {
return false, nil
}
}
// check if current server version is notice range
if !isCloud && cnd.ServerVersion != nil {
version := cleanupVersion(model.BuildNumber)
serverVersion, err := semver.NewVersion(version)
if err != nil {
mlog.Warn("Build number is not in semver format", mlog.String("build_number", version))
return false, nil
}
for _, v := range cnd.ServerVersion {
c, err := semver.NewConstraint(v)
if err != nil {
return false, errors.Wrapf(err, "Cannot parse version range %s", v)
}
if !c.Check(serverVersion) {
return false, nil
}
}
}
// check if sku matches our license
if cnd.Sku != nil {
if !cnd.Sku.Matches(sku) {
return false, nil
}
}
// check the target audience
if cnd.Audience != nil {
if !cnd.Audience.Matches(isSystemAdmin, isTeamAdmin) {
return false, nil
}
}
// check user count condition against previously calculated total user count
if cnd.NumberOfUsers != nil && userCount > 0 {
if userCount < *cnd.NumberOfUsers {
return false, nil
}
}
// check post count condition against previously calculated total post count
if cnd.NumberOfPosts != nil && postCount > 0 {
if postCount < *cnd.NumberOfPosts {
return false, nil
}
}
if cnd.DeprecatingDependency != nil {
extDepVersion, err := semver.NewVersion(cnd.DeprecatingDependency.MinimumVersion)
if err != nil {
return false, errors.Wrapf(err, "Cannot parse external dependency version %s", cnd.DeprecatingDependency.MinimumVersion)
}
switch cnd.DeprecatingDependency.Name {
case model.DatabaseDriverMysql, model.DatabaseDriverPostgres:
if dbName != cnd.DeprecatingDependency.Name {
return false, nil
}
serverDBMSVersion, err := semver.NewVersion(dbVer)
if err != nil {
return false, errors.Wrapf(err, "Cannot parse DBMS version %s", dbVer)
}
return extDepVersion.GreaterThan(serverDBMSVersion), nil
case model.SearchengineElasticsearch:
if searchEngineName != model.SearchengineElasticsearch {
return false, nil
}
semverESVersion, err := semver.NewVersion(searchEngineVer)
if err != nil {
return false, errors.Wrapf(err, "Cannot parse search engine version %s", searchEngineVer)
}
return extDepVersion.GreaterThan(semverESVersion), nil
default:
return false, nil
}
}
// check if our server config matches the notice
for k, v := range cnd.ServerConfig {
if !validateConfigEntry(config, k, v) {
return false, nil
}
}
// check if user's config matches the notice
for k, v := range cnd.UserConfig {
res, err := validateUserConfigEntry(preferences, userID, k, v)
if err != nil {
return false, err
}
if !res {
return false, nil
}
}
// check the type of installation
if cnd.InstanceType != nil {
if !cnd.InstanceType.Matches(isCloud) {
return false, nil
}
}
return true, nil
}
func validateUserConfigEntry(preferences store.PreferenceStore, userID string, key string, expectedValue any) (bool, error) {
parts := strings.Split(key, ".")
if len(parts) != 2 {
return false, errors.New("Invalid format of user config. Must be in form of Category.SettingName")
}
if _, ok := expectedValue.(string); !ok {
return false, errors.New("Invalid format of user config. Value should be string")
}
pref, err := preferences.Get(userID, parts[0], parts[1])
if err != nil {
return false, nil
}
return pref.Value == expectedValue, nil
}
func validateConfigEntry(conf *model.Config, path string, expectedValue any) bool {
value, found := config.GetValueByPath(strings.Split(path, "."), *conf)
if !found {
return false
}
vt := reflect.ValueOf(value)
if vt.IsNil() {
return expectedValue == nil
}
if vt.Kind() == reflect.Ptr {
vt = vt.Elem()
}
val := vt.Interface()
return val == expectedValue
}
// GetProductNotices is called from the frontend to fetch the product notices that are relevant to the caller
func (a *App) GetProductNotices(c *request.Context, userID, teamID string, client model.NoticeClientType, clientVersion string, locale string) (model.NoticeMessages, *model.AppError) {
isSystemAdmin := a.SessionHasPermissionTo(*c.Session(), model.PermissionManageSystem)
isTeamAdmin := a.SessionHasPermissionToTeam(*c.Session(), teamID, model.PermissionManageTeam)
// check if notices for regular users are disabled
if !*a.Config().AnnouncementSettings.UserNoticesEnabled && !isSystemAdmin {
return []model.NoticeMessage{}, nil
}
// check if notices for admins are disabled
if !*a.Config().AnnouncementSettings.AdminNoticesEnabled && (isTeamAdmin || isSystemAdmin) {
return []model.NoticeMessage{}, nil
}
views, err := a.Srv().Store().ProductNotices().GetViews(userID)
if err != nil {
return nil, model.NewAppError("GetProductNotices", "api.system.update_viewed_notices.failed", nil, "", http.StatusBadRequest).Wrap(err)
}
sku := a.Srv().ClientLicense()["SkuShortName"]
isCloud := a.Srv().License() != nil && *a.Srv().License().Features.Cloud
dbName := *a.Config().SqlSettings.DriverName
var searchEngineName, searchEngineVersion string
if engine := a.Srv().Platform().SearchEngine; engine != nil && engine.ElasticsearchEngine != nil {
searchEngineName = engine.ElasticsearchEngine.GetName()
searchEngineVersion = engine.ElasticsearchEngine.GetFullVersion()
}
filteredNotices := make([]model.NoticeMessage, 0)
for noticeIndex, notice := range a.ch.cachedNotices {
// check if the notice has been viewed already
var view *model.ProductNoticeViewState
for viewIndex, v := range views {
if v.NoticeId == notice.ID {
view = &views[viewIndex]
break
}
}
if view != nil {
repeatable := notice.Repeatable != nil && *notice.Repeatable
if repeatable {
if view.Viewed > MaxRepeatViewings {
continue
}
if (time.Now().UTC().Unix() - view.Timestamp) < MinSecondsBetweenRepeatViewings {
continue
}
} else if view.Viewed > 0 {
continue
}
}
result, err := noticeMatchesConditions(
a.Config(),
a.Srv().Store().Preference(),
userID,
client,
clientVersion,
a.ch.cachedPostCount,
a.ch.cachedUserCount,
isSystemAdmin,
isTeamAdmin,
isCloud,
sku,
dbName,
a.ch.cachedDBMSVersion,
searchEngineName,
searchEngineVersion,
&a.ch.cachedNotices[noticeIndex])
if err != nil {
return nil, model.NewAppError("GetProductNotices", "api.system.update_notices.validating_failed", nil, "", http.StatusBadRequest).Wrap(err)
}
if result {
selectedLocale := "en"
filteredNotices = append(filteredNotices, model.NoticeMessage{
NoticeMessageInternal: notice.LocalizedMessages[selectedLocale],
ID: notice.ID,
TeamAdminOnly: notice.TeamAdminOnly(),
SysAdminOnly: notice.SysAdminOnly(),
})
}
}
return filteredNotices, nil
}
// UpdateViewedProductNotices is called from the frontend to mark a set of notices as 'viewed' by user
func (a *App) UpdateViewedProductNotices(userID string, noticeIds []string) *model.AppError {
if err := a.Srv().Store().ProductNotices().View(userID, noticeIds); err != nil {
return model.NewAppError("UpdateViewedProductNotices", "api.system.update_viewed_notices.failed", nil, "", http.StatusBadRequest).Wrap(err)
}
return nil
}
// UpdateViewedProductNoticesForNewUser is called when new user is created to mark all current notices for this
// user as viewed in order to avoid showing them imminently on first login
func (a *App) UpdateViewedProductNoticesForNewUser(userID string) {
var noticeIds []string
for _, notice := range a.ch.cachedNotices {
noticeIds = append(noticeIds, notice.ID)
}
if err := a.Srv().Store().ProductNotices().View(userID, noticeIds); err != nil {
mlog.Error("Cannot update product notices viewed state for user", mlog.String("userId", userID))
}
}
// UpdateProductNotices is called periodically from a scheduled worker to fetch new notices and update the cache
func (a *App) UpdateProductNotices() *model.AppError {
url := *a.Config().AnnouncementSettings.NoticesURL
skip := *a.Config().AnnouncementSettings.NoticesSkipCache
mlog.Debug("Will fetch notices from", mlog.String("url", url), mlog.Bool("skip_cache", skip))
var err error
a.ch.cachedPostCount, err = a.Srv().Store().Post().AnalyticsPostCount(&model.PostCountOptions{})
if err != nil {
mlog.Warn("Failed to fetch post count", mlog.String("error", err.Error()))
}
a.ch.cachedUserCount, err = a.Srv().Store().User().Count(model.UserCountOptions{IncludeDeleted: true})
if err != nil {
mlog.Warn("Failed to fetch user count", mlog.String("error", err.Error()))
}
a.ch.cachedDBMSVersion, err = a.Srv().Store().GetDbVersion(false)
if err != nil {
mlog.Warn("Failed to get DBMS version", mlog.String("error", err.Error()))
}
a.ch.cachedDBMSVersion = strings.Split(a.ch.cachedDBMSVersion, " ")[0] // get rid of trailing strings attached to the version
data, err := utils.GetURLWithCache(url, ¬icesCache, skip)
if err != nil {
return model.NewAppError("UpdateProductNotices", "api.system.update_notices.fetch_failed", nil, "", http.StatusBadRequest).Wrap(err)
}
a.ch.cachedNotices, err = model.UnmarshalProductNotices(data)
if err != nil {
return model.NewAppError("UpdateProductNotices", "api.system.update_notices.parse_failed", nil, "", http.StatusBadRequest).Wrap(err)
}
if err := a.Srv().Store().ProductNotices().ClearOldNotices(a.ch.cachedNotices); err != nil {
return model.NewAppError("UpdateProductNotices", "api.system.update_notices.clear_failed", nil, "", http.StatusBadRequest).Wrap(err)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"math"
"net/http"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/throttled/throttled"
"github.com/throttled/throttled/store/memstore"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type RateLimiter struct {
throttledRateLimiter *throttled.GCRARateLimiter
useAuth bool
useIP bool
header string
trustedProxyIPHeader []string
}
func NewRateLimiter(settings *model.RateLimitSettings, trustedProxyIPHeader []string) (*RateLimiter, error) {
store, err := memstore.New(*settings.MemoryStoreSize)
if err != nil {
return nil, errors.Wrap(err, i18n.T("api.server.start_server.rate_limiting_memory_store"))
}
quota := throttled.RateQuota{
MaxRate: throttled.PerSec(*settings.PerSec),
MaxBurst: *settings.MaxBurst,
}
throttledRateLimiter, err := throttled.NewGCRARateLimiter(store, quota)
if err != nil {
return nil, errors.Wrap(err, i18n.T("api.server.start_server.rate_limiting_rate_limiter"))
}
return &RateLimiter{
throttledRateLimiter: throttledRateLimiter,
useAuth: *settings.VaryByUser,
useIP: *settings.VaryByRemoteAddr,
header: settings.VaryByHeader,
trustedProxyIPHeader: trustedProxyIPHeader,
}, nil
}
func (rl *RateLimiter) GenerateKey(r *http.Request) string {
key := ""
if rl.useAuth {
token, tokenLocation := ParseAuthTokenFromRequest(r)
if tokenLocation != TokenLocationNotFound {
key += token
} else if rl.useIP { // If we don't find an authentication token and IP based is enabled, fall back to IP
key += utils.GetIPAddress(r, rl.trustedProxyIPHeader)
}
} else if rl.useIP { // Only if Auth based is not enabed do we use a plain IP based
key += utils.GetIPAddress(r, rl.trustedProxyIPHeader)
}
// Note that most of the time the user won't have to set this because the utils.GetIPAddress above tries the
// most common headers anyway.
if rl.header != "" {
key += strings.ToLower(r.Header.Get(rl.header))
}
return key
}
func (rl *RateLimiter) RateLimitWriter(key string, w http.ResponseWriter) bool {
limited, context, err := rl.throttledRateLimiter.RateLimit(key, 1)
if err != nil {
mlog.Error("Internal server error when rate limiting. Rate Limiting broken.", mlog.Err(err))
return false
}
setRateLimitHeaders(w, context)
if limited {
mlog.Debug("Denied due to throttling settings code=429", mlog.String("key", key))
http.Error(w, "limit exceeded", http.StatusTooManyRequests)
}
return limited
}
func (rl *RateLimiter) UserIdRateLimit(userID string, w http.ResponseWriter) bool {
if rl.useAuth {
return rl.RateLimitWriter(userID, w)
}
return false
}
func (rl *RateLimiter) RateLimitHandler(wrappedHandler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := rl.GenerateKey(r)
if !rl.RateLimitWriter(key, w) {
wrappedHandler.ServeHTTP(w, r)
}
})
}
// Copied from https://github.com/throttled/throttled http.go
func setRateLimitHeaders(w http.ResponseWriter, context throttled.RateLimitResult) {
if v := context.Limit; v >= 0 {
w.Header().Add("X-RateLimit-Limit", strconv.Itoa(v))
}
if v := context.Remaining; v >= 0 {
w.Header().Add("X-RateLimit-Remaining", strconv.Itoa(v))
}
if v := context.ResetAfter; v >= 0 {
vi := int(math.Ceil(v.Seconds()))
w.Header().Add("X-RateLimit-Reset", strconv.Itoa(vi))
}
if v := context.RetryAfter; v >= 0 {
vi := int(math.Ceil(v.Seconds()))
w.Header().Add("Retry-After", strconv.Itoa(vi))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) SaveReactionForPost(c *request.Context, reaction *model.Reaction) (*model.Reaction, *model.AppError) {
post, err := a.GetSinglePost(reaction.PostId, false)
if err != nil {
return nil, err
}
channel, err := a.GetChannel(c, post.ChannelId)
if err != nil {
return nil, err
}
if channel.DeleteAt > 0 {
return nil, model.NewAppError("deleteReactionForPost", "api.reaction.save.archived_channel.app_error", nil, "", http.StatusForbidden)
}
reaction, nErr := a.Srv().Store().Reaction().Save(reaction)
if nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("SaveReactionForPost", "app.reaction.save.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
// The post is always modified since the UpdateAt always changes
a.invalidateCacheForChannelPosts(post.ChannelId)
pluginContext := pluginContext(c)
a.Srv().Go(func() {
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.ReactionHasBeenAdded(pluginContext, reaction)
return true
}, plugin.ReactionHasBeenAddedID)
})
a.Srv().Go(func() {
a.sendReactionEvent(model.WebsocketEventReactionAdded, reaction, post)
})
return reaction, nil
}
func (a *App) GetReactionsForPost(postID string) ([]*model.Reaction, *model.AppError) {
reactions, err := a.Srv().Store().Reaction().GetForPost(postID, true)
if err != nil {
return nil, model.NewAppError("GetReactionsForPost", "app.reaction.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return reactions, nil
}
func (a *App) GetBulkReactionsForPosts(postIDs []string) (map[string][]*model.Reaction, *model.AppError) {
reactions := make(map[string][]*model.Reaction)
allReactions, err := a.Srv().Store().Reaction().BulkGetForPosts(postIDs)
if err != nil {
return nil, model.NewAppError("GetBulkReactionsForPosts", "app.reaction.bulk_get_for_post_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, reaction := range allReactions {
reactionsForPost := reactions[reaction.PostId]
reactionsForPost = append(reactionsForPost, reaction)
reactions[reaction.PostId] = reactionsForPost
}
reactions = populateEmptyReactions(postIDs, reactions)
return reactions, nil
}
func populateEmptyReactions(postIDs []string, reactions map[string][]*model.Reaction) map[string][]*model.Reaction {
for _, postID := range postIDs {
if _, present := reactions[postID]; !present {
reactions[postID] = []*model.Reaction{}
}
}
return reactions
}
func (a *App) GetTopReactionsForTeamSince(teamID string, userID string, opts *model.InsightsOpts) (*model.TopReactionList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopReactionsForTeamSince", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topReactionList, err := a.Srv().Store().Reaction().GetTopForTeamSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopReactionsForTeamSince", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return topReactionList, nil
}
func (a *App) GetTopReactionsForUserSince(userID string, teamID string, opts *model.InsightsOpts) (*model.TopReactionList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopReactionsForUserSince", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topReactionList, err := a.Srv().Store().Reaction().GetTopForUserSince(userID, teamID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopReactionsForUserSince", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return topReactionList, nil
}
func (a *App) DeleteReactionForPost(c *request.Context, reaction *model.Reaction) *model.AppError {
post, err := a.GetSinglePost(reaction.PostId, false)
if err != nil {
return err
}
channel, err := a.GetChannel(c, post.ChannelId)
if err != nil {
return err
}
if channel.DeleteAt > 0 {
return model.NewAppError("DeleteReactionForPost", "api.reaction.delete.archived_channel.app_error", nil, "", http.StatusForbidden)
}
if _, err := a.Srv().Store().Reaction().Delete(reaction); err != nil {
return model.NewAppError("DeleteReactionForPost", "app.reaction.delete_all_with_emoji_name.get_reactions.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// The post is always modified since the UpdateAt always changes
a.invalidateCacheForChannelPosts(post.ChannelId)
pluginContext := pluginContext(c)
a.Srv().Go(func() {
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.ReactionHasBeenRemoved(pluginContext, reaction)
return true
}, plugin.ReactionHasBeenRemovedID)
})
a.Srv().Go(func() {
a.sendReactionEvent(model.WebsocketEventReactionRemoved, reaction, post)
})
return nil
}
func (a *App) sendReactionEvent(event string, reaction *model.Reaction, post *model.Post) {
// send out that a reaction has been added/removed
message := model.NewWebSocketEvent(event, "", post.ChannelId, "", nil, "")
reactionJSON, err := json.Marshal(reaction)
if err != nil {
a.Log().Warn("Failed to encode reaction to JSON", mlog.Err(err))
}
message.Add("reaction", string(reactionJSON))
a.Publish(message)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/mattermost/mattermost-server/v6/model"
)
func (a *App) AddRemoteCluster(rc *model.RemoteCluster) (*model.RemoteCluster, *model.AppError) {
rc, err := a.Srv().Store().RemoteCluster().Save(rc)
if err != nil {
if sqlstore.IsUniqueConstraintError(errors.Cause(err), []string{sqlstore.RemoteClusterSiteURLUniqueIndex}) {
return nil, model.NewAppError("AddRemoteCluster", "api.remote_cluster.save_not_unique.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil, model.NewAppError("AddRemoteCluster", "api.remote_cluster.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return rc, nil
}
func (a *App) UpdateRemoteCluster(rc *model.RemoteCluster) (*model.RemoteCluster, *model.AppError) {
rc, err := a.Srv().Store().RemoteCluster().Update(rc)
if err != nil {
if sqlstore.IsUniqueConstraintError(errors.Cause(err), []string{sqlstore.RemoteClusterSiteURLUniqueIndex}) {
return nil, model.NewAppError("UpdateRemoteCluster", "api.remote_cluster.update_not_unique.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil, model.NewAppError("UpdateRemoteCluster", "api.remote_cluster.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return rc, nil
}
func (a *App) DeleteRemoteCluster(remoteClusterId string) (bool, *model.AppError) {
deleted, err := a.Srv().Store().RemoteCluster().Delete(remoteClusterId)
if err != nil {
return false, model.NewAppError("DeleteRemoteCluster", "api.remote_cluster.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return deleted, nil
}
func (a *App) GetRemoteCluster(remoteClusterId string) (*model.RemoteCluster, *model.AppError) {
rc, err := a.Srv().Store().RemoteCluster().Get(remoteClusterId)
if err != nil {
return nil, model.NewAppError("GetRemoteCluster", "api.remote_cluster.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return rc, nil
}
func (a *App) GetAllRemoteClusters(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, *model.AppError) {
list, err := a.Srv().Store().RemoteCluster().GetAll(filter)
if err != nil {
return nil, model.NewAppError("GetAllRemoteClusters", "api.remote_cluster.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return list, nil
}
func (a *App) UpdateRemoteClusterTopics(remoteClusterId string, topics string) (*model.RemoteCluster, *model.AppError) {
rc, err := a.Srv().Store().RemoteCluster().UpdateTopics(remoteClusterId, topics)
if err != nil {
return nil, model.NewAppError("UpdateRemoteClusterTopics", "api.remote_cluster.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return rc, nil
}
func (a *App) SetRemoteClusterLastPingAt(remoteClusterId string) *model.AppError {
err := a.Srv().Store().RemoteCluster().SetLastPingAt(remoteClusterId)
if err != nil {
return model.NewAppError("SetRemoteClusterLastPingAt", "api.remote_cluster.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) GetRemoteClusterService() (remotecluster.RemoteClusterServiceIFace, *model.AppError) {
service := a.Srv().GetRemoteClusterService()
if service == nil {
return nil, model.NewAppError("GetRemoteClusterService", "api.remote_cluster.service_not_enabled.app_error", nil, "", http.StatusNotImplemented)
}
return service, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
)
// MockOptionRemoteClusterService a mock of the remote cluster service
type MockOptionRemoteClusterService func(service *mockRemoteClusterService)
func MockOptionRemoteClusterServiceWithActive(active bool) MockOptionRemoteClusterService {
return func(mrcs *mockRemoteClusterService) {
mrcs.active = active
}
}
func NewMockRemoteClusterService(service remotecluster.RemoteClusterServiceIFace, options ...MockOptionRemoteClusterService) *mockRemoteClusterService {
mrcs := &mockRemoteClusterService{service, true}
for _, option := range options {
option(mrcs)
}
return mrcs
}
type mockRemoteClusterService struct {
remotecluster.RemoteClusterServiceIFace
active bool
}
func (mrcs *mockRemoteClusterService) Shutdown() error {
return nil
}
func (mrcs *mockRemoteClusterService) Start() error {
return nil
}
func (mrcs *mockRemoteClusterService) Active() bool {
return mrcs.active
}
func (mrcs *mockRemoteClusterService) AddTopicListener(topic string, listener remotecluster.TopicListener) string {
return model.NewId()
}
func (mrcs *mockRemoteClusterService) RemoveTopicListener(listenerId string) {
}
func (mrcs *mockRemoteClusterService) AddConnectionStateListener(listener remotecluster.ConnectionStateListener) string {
return model.NewId()
}
func (mrcs *mockRemoteClusterService) RemoveConnectionStateListener(listenerId string) {
}
func (mrcs *mockRemoteClusterService) SendMsg(ctx context.Context, msg model.RemoteClusterMsg, rc *model.RemoteCluster, f remotecluster.SendMsgResultFunc) error {
return nil
}
func (mrcs *mockRemoteClusterService) SendFile(ctx context.Context, us *model.UploadSession, fi *model.FileInfo, rc *model.RemoteCluster, rp remotecluster.ReaderProvider, f remotecluster.SendFileResultFunc) error {
return nil
}
func (mrcs *mockRemoteClusterService) AcceptInvitation(invite *model.RemoteClusterInvite, name string, displayName string, creatorId string, teamId string, siteURL string) (*model.RemoteCluster, error) {
return nil, nil
}
func (mrcs *mockRemoteClusterService) ReceiveIncomingMsg(rc *model.RemoteCluster, msg model.RemoteClusterMsg) remotecluster.Response {
return remotecluster.Response{}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package request
import (
"context"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type Context struct {
t i18n.TranslateFunc
session model.Session
requestId string
ipAddress string
path string
userAgent string
acceptLanguage string
logger mlog.LoggerIFace
err *model.AppError
context context.Context
}
func NewContext(ctx context.Context, requestId, ipAddress, path, userAgent, acceptLanguage string, session model.Session, t i18n.TranslateFunc) *Context {
return &Context{
t: t,
session: session,
requestId: requestId,
ipAddress: ipAddress,
path: path,
userAgent: userAgent,
acceptLanguage: acceptLanguage,
context: ctx,
}
}
func EmptyContext(logger mlog.LoggerIFace) *Context {
return &Context{
t: i18n.T,
logger: logger,
context: context.Background(),
}
}
func (c *Context) T(translationID string, args ...any) string {
return c.t(translationID, args...)
}
func (c *Context) Session() *model.Session {
return &c.session
}
func (c *Context) RequestId() string {
return c.requestId
}
func (c *Context) IPAddress() string {
return c.ipAddress
}
func (c *Context) Path() string {
return c.path
}
func (c *Context) UserAgent() string {
return c.userAgent
}
func (c *Context) AcceptLanguage() string {
return c.acceptLanguage
}
func (c *Context) Context() context.Context {
return c.context
}
func (c *Context) SetSession(s *model.Session) {
c.session = *s
}
func (c *Context) SetT(t i18n.TranslateFunc) {
c.t = t
}
func (c *Context) SetRequestId(s string) {
c.requestId = s
}
func (c *Context) SetIPAddress(s string) {
c.ipAddress = s
}
func (c *Context) SetUserAgent(s string) {
c.userAgent = s
}
func (c *Context) SetAcceptLanguage(s string) {
c.acceptLanguage = s
}
func (c *Context) SetPath(s string) {
c.path = s
}
func (c *Context) SetContext(ctx context.Context) {
c.context = ctx
}
func (c *Context) GetT() i18n.TranslateFunc {
return c.t
}
func (c *Context) SetLogger(logger mlog.LoggerIFace) {
c.logger = logger
}
func (c *Context) Logger() mlog.LoggerIFace {
return c.logger
}
func (c *Context) SetAppError(err *model.AppError) {
c.err = err
}
func (c *Context) AppError() *model.AppError {
return c.err
}
type CTX interface {
T(string, ...interface{}) string
Session() *model.Session
RequestId() string
IPAddress() string
Path() string
UserAgent() string
AcceptLanguage() string
Context() context.Context
SetSession(s *model.Session)
SetT(i18n.TranslateFunc)
SetRequestId(string)
SetIPAddress(string)
SetUserAgent(string)
SetAcceptLanguage(string)
SetPath(string)
SetContext(ctx context.Context)
GetT() i18n.TranslateFunc
SetLogger(mlog.LoggerIFace)
Logger() mlog.LoggerIFace
SetAppError(*model.AppError)
AppError() *model.AppError
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"fmt"
"io"
"net/http"
"strconv"
"strings"
)
type PluginResponseWriter struct {
bytes.Buffer
headers http.Header
statusCode int
}
func (rt *PluginResponseWriter) Header() http.Header {
if rt.headers == nil {
rt.headers = make(http.Header)
}
return rt.headers
}
func (rt *PluginResponseWriter) WriteHeader(statusCode int) {
rt.statusCode = statusCode
}
// From net/http/httptest/recorder.go
func parseContentLength(cl string) int64 {
cl = strings.TrimSpace(cl)
if cl == "" {
return -1
}
n, err := strconv.ParseInt(cl, 10, 64)
if err != nil {
return -1
}
return n
}
func (rt *PluginResponseWriter) GenerateResponse() *http.Response {
res := &http.Response{
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
StatusCode: rt.statusCode,
Header: rt.headers.Clone(),
}
if res.StatusCode == 0 {
res.StatusCode = http.StatusOK
}
res.Status = fmt.Sprintf("%03d %s", res.StatusCode, http.StatusText(res.StatusCode))
if rt.Len() > 0 {
res.Body = io.NopCloser(rt)
} else {
res.Body = http.NoBody
}
res.ContentLength = parseContentLength(rt.headers.Get("Content-Length"))
return res
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"encoding/json"
"errors"
"net/http"
"reflect"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
func (a *App) GetRole(id string) (*model.Role, *model.AppError) {
role, err := a.Srv().Store().Role().Get(id)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetRole", "app.role.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetRole", "app.role.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
appErr := a.Srv().mergeChannelHigherScopedPermissions([]*model.Role{role})
if appErr != nil {
return nil, appErr
}
return role, nil
}
func (a *App) GetAllRoles() ([]*model.Role, *model.AppError) {
roles, err := a.Srv().Store().Role().GetAll()
if err != nil {
return nil, model.NewAppError("GetAllRoles", "app.role.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
appErr := a.Srv().mergeChannelHigherScopedPermissions(roles)
if appErr != nil {
return nil, appErr
}
return roles, nil
}
func (s *Server) GetRoleByName(ctx context.Context, name string) (*model.Role, *model.AppError) {
role, nErr := s.Store().Role().GetByName(ctx, name)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("GetRoleByName", "app.role.get_by_name.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("GetRoleByName", "app.role.get_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
err := s.mergeChannelHigherScopedPermissions([]*model.Role{role})
if err != nil {
return nil, err
}
return role, nil
}
func (a *App) GetRoleByName(ctx context.Context, name string) (*model.Role, *model.AppError) {
return a.Srv().GetRoleByName(ctx, name)
}
func (a *App) GetRolesByNames(names []string) ([]*model.Role, *model.AppError) {
roles, nErr := a.Srv().Store().Role().GetByNames(names)
if nErr != nil {
return nil, model.NewAppError("GetRolesByNames", "app.role.get_by_names.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
err := a.mergeChannelHigherScopedPermissions(roles)
if err != nil {
return nil, err
}
return roles, nil
}
// mergeChannelHigherScopedPermissions updates the permissions based on the role type, whether the permission is
// moderated, and the value of the permission on the higher-scoped scheme.
func (s *Server) mergeChannelHigherScopedPermissions(roles []*model.Role) *model.AppError {
var higherScopeNamesToQuery []string
for _, role := range roles {
if role.SchemeManaged {
higherScopeNamesToQuery = append(higherScopeNamesToQuery, role.Name)
}
}
if len(higherScopeNamesToQuery) == 0 {
return nil
}
higherScopedPermissionsMap, err := s.Store().Role().ChannelHigherScopedPermissions(higherScopeNamesToQuery)
if err != nil {
return model.NewAppError("mergeChannelHigherScopedPermissions", "app.role.get_by_names.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, role := range roles {
if role.SchemeManaged {
if higherScopedPermissions, ok := higherScopedPermissionsMap[role.Name]; ok {
role.MergeChannelHigherScopedPermissions(higherScopedPermissions)
}
}
}
return nil
}
// mergeChannelHigherScopedPermissions updates the permissions based on the role type, whether the permission is
// moderated, and the value of the permission on the higher-scoped scheme.
func (a *App) mergeChannelHigherScopedPermissions(roles []*model.Role) *model.AppError {
return a.Srv().mergeChannelHigherScopedPermissions(roles)
}
func (a *App) PatchRole(role *model.Role, patch *model.RolePatch) (*model.Role, *model.AppError) {
// If patch is a no-op then short-circuit the store.
if patch.Permissions != nil && reflect.DeepEqual(*patch.Permissions, role.Permissions) {
return role, nil
}
role.Patch(patch)
role, err := a.UpdateRole(role)
if err != nil {
return nil, err
}
if appErr := a.sendUpdatedRoleEvent(role); appErr != nil {
return nil, appErr
}
return role, err
}
func (a *App) CreateRole(role *model.Role) (*model.Role, *model.AppError) {
role.Id = ""
role.CreateAt = 0
role.UpdateAt = 0
role.DeleteAt = 0
role.BuiltIn = false
role.SchemeManaged = false
var err error
role, err = a.Srv().Store().Role().Save(role)
if err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateRole", "app.role.save.invalid_role.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("CreateRole", "app.role.save.insert.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return role, nil
}
func (a *App) UpdateRole(role *model.Role) (*model.Role, *model.AppError) {
savedRole, err := a.Srv().Store().Role().Save(role)
if err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("UpdateRole", "app.role.save.invalid_role.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpdateRole", "app.role.save.insert.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
builtInChannelRoles := []string{
model.ChannelGuestRoleId,
model.ChannelUserRoleId,
model.ChannelAdminRoleId,
}
builtInRolesMinusChannelRoles := append(utils.RemoveStringsFromSlice(model.BuiltInSchemeManagedRoleIDs, builtInChannelRoles...), model.NewSystemRoleIDs...)
if utils.StringInSlice(savedRole.Name, builtInRolesMinusChannelRoles) {
return savedRole, nil
}
var roleRetrievalFunc func() ([]*model.Role, *model.AppError)
if utils.StringInSlice(savedRole.Name, builtInChannelRoles) {
roleRetrievalFunc = func() ([]*model.Role, *model.AppError) {
roles, nErr := a.Srv().Store().Role().AllChannelSchemeRoles()
if nErr != nil {
return nil, model.NewAppError("UpdateRole", "app.role.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return roles, nil
}
} else {
roleRetrievalFunc = func() ([]*model.Role, *model.AppError) {
roles, nErr := a.Srv().Store().Role().ChannelRolesUnderTeamRole(savedRole.Name)
if nErr != nil {
return nil, model.NewAppError("UpdateRole", "app.role.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return roles, nil
}
}
impactedRoles, appErr := roleRetrievalFunc()
if appErr != nil {
return nil, appErr
}
impactedRoles = append(impactedRoles, role)
appErr = a.mergeChannelHigherScopedPermissions(impactedRoles)
if appErr != nil {
return nil, appErr
}
for _, ir := range impactedRoles {
if ir.Name != role.Name {
appErr = a.sendUpdatedRoleEvent(ir)
if appErr != nil {
return nil, appErr
}
}
}
return savedRole, nil
}
func (a *App) CheckRolesExist(roleNames []string) *model.AppError {
roles, err := a.GetRolesByNames(roleNames)
if err != nil {
return err
}
for _, name := range roleNames {
nameFound := false
for _, role := range roles {
if name == role.Name {
nameFound = true
break
}
}
if !nameFound {
return model.NewAppError("CheckRolesExist", "app.role.check_roles_exist.role_not_found", nil, "role="+name, http.StatusBadRequest)
}
}
return nil
}
func (a *App) sendUpdatedRoleEvent(role *model.Role) *model.AppError {
message := model.NewWebSocketEvent(model.WebsocketEventRoleUpdated, "", "", "", nil, "")
roleJSON, jsonErr := json.Marshal(role)
if jsonErr != nil {
return model.NewAppError("sendUpdatedRoleEvent", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("role", string(roleJSON))
a.Publish(message)
return nil
}
func RemoveRoles(rolesToRemove []string, roles string) string {
roleList := strings.Fields(roles)
newRoles := make([]string, 0)
for _, role := range roleList {
shouldRemove := false
for _, roleToRemove := range rolesToRemove {
if role == roleToRemove {
shouldRemove = true
break
}
}
if !shouldRemove {
newRoles = append(newRoles, role)
}
}
return strings.Join(newRoles, " ")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"crypto/x509"
"encoding/pem"
"encoding/xml"
"fmt"
"io"
"mime/multipart"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
)
const (
SamlPublicCertificateName = "saml-public.crt"
SamlPrivateKeyName = "saml-private.key"
SamlIdpCertificateName = "saml-idp.crt"
)
func (a *App) GetSamlMetadata() (string, *model.AppError) {
if a.Saml() == nil {
err := model.NewAppError("GetSamlMetadata", "api.admin.saml.not_available.app_error", nil, "", http.StatusNotImplemented)
return "", err
}
result, err := a.Saml().GetMetadata()
if err != nil {
return "", model.NewAppError("GetSamlMetadata", "api.admin.saml.metadata.app_error", nil, "err="+err.Message, err.StatusCode)
}
return result, nil
}
func (a *App) writeSamlFile(filename string, fileData *multipart.FileHeader) *model.AppError {
file, err := fileData.Open()
if err != nil {
return model.NewAppError("AddSamlCertificate", "api.admin.add_certificate.open.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return model.NewAppError("AddSamlCertificate", "api.admin.add_certificate.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
err = a.Srv().platform.SetConfigFile(filename, data)
if err != nil {
return model.NewAppError("AddSamlCertificate", "api.admin.add_certificate.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) AddSamlPublicCertificate(fileData *multipart.FileHeader) *model.AppError {
if err := a.writeSamlFile(SamlPublicCertificateName, fileData); err != nil {
return err
}
cfg := a.Config().Clone()
*cfg.SamlSettings.PublicCertificateFile = SamlPublicCertificateName
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
func (a *App) AddSamlPrivateCertificate(fileData *multipart.FileHeader) *model.AppError {
if err := a.writeSamlFile(SamlPrivateKeyName, fileData); err != nil {
return err
}
cfg := a.Config().Clone()
*cfg.SamlSettings.PrivateKeyFile = SamlPrivateKeyName
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
func (a *App) AddSamlIdpCertificate(fileData *multipart.FileHeader) *model.AppError {
if err := a.writeSamlFile(SamlIdpCertificateName, fileData); err != nil {
return err
}
cfg := a.Config().Clone()
*cfg.SamlSettings.IdpCertificateFile = SamlIdpCertificateName
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
func (a *App) removeSamlFile(filename string) *model.AppError {
if err := a.Srv().platform.RemoveConfigFile(filename); err != nil {
return model.NewAppError("RemoveSamlFile", "api.admin.remove_certificate.delete.app_error", map[string]any{"Filename": filename}, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) RemoveSamlPublicCertificate() *model.AppError {
if err := a.removeSamlFile(*a.Config().SamlSettings.PublicCertificateFile); err != nil {
return err
}
cfg := a.Config().Clone()
*cfg.SamlSettings.PublicCertificateFile = ""
*cfg.SamlSettings.Encrypt = false
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
func (a *App) RemoveSamlPrivateCertificate() *model.AppError {
if err := a.removeSamlFile(*a.Config().SamlSettings.PrivateKeyFile); err != nil {
return err
}
cfg := a.Config().Clone()
*cfg.SamlSettings.PrivateKeyFile = ""
*cfg.SamlSettings.Encrypt = false
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
func (a *App) RemoveSamlIdpCertificate() *model.AppError {
if err := a.removeSamlFile(*a.Config().SamlSettings.IdpCertificateFile); err != nil {
return err
}
cfg := a.Config().Clone()
*cfg.SamlSettings.IdpCertificateFile = ""
*cfg.SamlSettings.Enable = false
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
func (a *App) GetSamlCertificateStatus() *model.SamlCertificateStatus {
status := &model.SamlCertificateStatus{}
status.IdpCertificateFile, _ = a.Srv().platform.HasConfigFile(*a.Config().SamlSettings.IdpCertificateFile)
status.PrivateKeyFile, _ = a.Srv().platform.HasConfigFile(*a.Config().SamlSettings.PrivateKeyFile)
status.PublicCertificateFile, _ = a.Srv().platform.HasConfigFile(*a.Config().SamlSettings.PublicCertificateFile)
return status
}
func (a *App) GetSamlMetadataFromIdp(idpMetadataURL string) (*model.SamlMetadataResponse, *model.AppError) {
if a.Saml() == nil {
err := model.NewAppError("GetSamlMetadataFromIdp", "api.admin.saml.not_available.app_error", nil, "", http.StatusNotImplemented)
return nil, err
}
if !strings.HasPrefix(idpMetadataURL, "http://") && !strings.HasPrefix(idpMetadataURL, "https://") {
idpMetadataURL = "https://" + idpMetadataURL
}
idpMetadataRaw, err := a.FetchSamlMetadataFromIdp(idpMetadataURL)
if err != nil {
return nil, err
}
data, err := a.BuildSamlMetadataObject(idpMetadataRaw)
if err != nil {
return nil, err
}
return data, nil
}
func (a *App) FetchSamlMetadataFromIdp(url string) ([]byte, *model.AppError) {
resp, err := a.HTTPService().MakeClient(false).Get(url)
if err != nil {
return nil, model.NewAppError("FetchSamlMetadataFromIdp", "app.admin.saml.invalid_response_from_idp.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if resp.StatusCode != http.StatusOK {
return nil, model.NewAppError("FetchSamlMetadataFromIdp", "app.admin.saml.invalid_response_from_idp.app_error", nil, fmt.Sprintf("status_code=%d", resp.StatusCode), http.StatusBadRequest)
}
defer resp.Body.Close()
bodyXML, err := io.ReadAll(resp.Body)
if err != nil {
return nil, model.NewAppError("FetchSamlMetadataFromIdp", "app.admin.saml.failure_read_response_body_from_idp.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return bodyXML, nil
}
func (a *App) BuildSamlMetadataObject(idpMetadata []byte) (*model.SamlMetadataResponse, *model.AppError) {
entityDescriptor := model.EntityDescriptor{}
err := xml.Unmarshal(idpMetadata, &entityDescriptor)
if err != nil {
return nil, model.NewAppError("BuildSamlMetadataObject", "app.admin.saml.failure_decode_metadata_xml_from_idp.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
data := &model.SamlMetadataResponse{}
data.IdpDescriptorURL = entityDescriptor.EntityID
if entityDescriptor.IDPSSODescriptors == nil || len(entityDescriptor.IDPSSODescriptors) == 0 {
err := model.NewAppError("BuildSamlMetadataObject", "api.admin.saml.invalid_xml_missing_idpssodescriptors.app_error", nil, "", http.StatusInternalServerError)
return nil, err
}
idpSSODescriptor := entityDescriptor.IDPSSODescriptors[0]
if idpSSODescriptor.SingleSignOnServices == nil || len(idpSSODescriptor.SingleSignOnServices) == 0 {
err := model.NewAppError("BuildSamlMetadataObject", "api.admin.saml.invalid_xml_missing_ssoservices.app_error", nil, "", http.StatusInternalServerError)
return nil, err
}
data.IdpURL = idpSSODescriptor.SingleSignOnServices[0].Location
if idpSSODescriptor.SSODescriptor.RoleDescriptor.KeyDescriptors == nil || len(idpSSODescriptor.SSODescriptor.RoleDescriptor.KeyDescriptors) == 0 {
err := model.NewAppError("BuildSamlMetadataObject", "api.admin.saml.invalid_xml_missing_keydescriptor.app_error", nil, "", http.StatusInternalServerError)
return nil, err
}
keyDescriptor := idpSSODescriptor.SSODescriptor.RoleDescriptor.KeyDescriptors[0]
data.IdpPublicCertificate = keyDescriptor.KeyInfo.X509Data.X509Certificate.Cert
return data, nil
}
func (a *App) SetSamlIdpCertificateFromMetadata(data []byte) *model.AppError {
const certPrefix = "-----BEGIN CERTIFICATE-----\n"
const certSuffix = "\n-----END CERTIFICATE-----"
fixedCertTxt := certPrefix + string(data) + certSuffix
block, _ := pem.Decode([]byte(fixedCertTxt))
if _, e := x509.ParseCertificate(block.Bytes); e != nil {
return model.NewAppError("SetSamlIdpCertificateFromMetadata", "api.admin.saml.failure_parse_idp_certificate.app_error", nil, "", http.StatusInternalServerError).Wrap(e)
}
data = pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: block.Bytes,
})
if err := a.Srv().platform.SetConfigFile(SamlIdpCertificateName, data); err != nil {
return model.NewAppError("SetSamlIdpCertificateFromMetadata", "api.admin.saml.failure_save_idp_certificate_file.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
cfg := a.Config().Clone()
*cfg.SamlSettings.IdpCertificateFile = SamlIdpCertificateName
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}
func (a *App) ResetSamlAuthDataToEmail(includeDeleted bool, dryRun bool, userIDs []string) (numAffected int, appErr *model.AppError) {
if a.Saml() == nil {
appErr = model.NewAppError("ResetAuthDataToEmail", "api.admin.saml.not_available.app_error", nil, "", http.StatusNotImplemented)
return
}
numAffected, err := a.Srv().Store().User().ResetAuthDataToEmailForUsers(model.UserAuthServiceSaml, userIDs, includeDeleted, dryRun)
if err != nil {
appErr = model.NewAppError("ResetAuthDataToEmail", "api.admin.saml.failure_reset_authdata_to_email.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
return
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func (a *App) GetScheme(id string) (*model.Scheme, *model.AppError) {
if appErr := a.IsPhase2MigrationCompleted(); appErr != nil {
return nil, appErr
}
scheme, err := a.Srv().Store().Scheme().Get(id)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetScheme", "app.scheme.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetScheme", "app.scheme.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return scheme, nil
}
func (a *App) GetSchemeByName(name string) (*model.Scheme, *model.AppError) {
if err := a.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
scheme, err := a.Srv().Store().Scheme().GetByName(name)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetSchemeByName", "app.scheme.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetSchemeByName", "app.scheme.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return scheme, nil
}
func (a *App) GetSchemesPage(scope string, page int, perPage int) ([]*model.Scheme, *model.AppError) {
if err := a.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
return a.GetSchemes(scope, page*perPage, perPage)
}
func (s *Server) GetSchemes(scope string, offset int, limit int) ([]*model.Scheme, *model.AppError) {
if err := s.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
scheme, err := s.Store().Scheme().GetAllPage(scope, offset, limit)
if err != nil {
return nil, model.NewAppError("GetSchemes", "app.scheme.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return scheme, nil
}
func (a *App) GetSchemes(scope string, offset int, limit int) ([]*model.Scheme, *model.AppError) {
return a.Srv().GetSchemes(scope, offset, limit)
}
func (a *App) CreateScheme(scheme *model.Scheme) (*model.Scheme, *model.AppError) {
if err := a.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
// Clear any user-provided values for trusted properties.
scheme.DefaultTeamAdminRole = ""
scheme.DefaultTeamUserRole = ""
scheme.DefaultTeamGuestRole = ""
scheme.DefaultChannelAdminRole = ""
scheme.DefaultChannelUserRole = ""
scheme.DefaultChannelGuestRole = ""
scheme.DefaultPlaybookAdminRole = ""
scheme.DefaultPlaybookMemberRole = ""
scheme.DefaultRunAdminRole = ""
scheme.DefaultRunMemberRole = ""
scheme.CreateAt = 0
scheme.UpdateAt = 0
scheme.DeleteAt = 0
scheme, err := a.Srv().Store().Scheme().Save(scheme)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateScheme", "app.scheme.save.invalid_scheme.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("CreateScheme", "app.scheme.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return scheme, nil
}
func (a *App) PatchScheme(scheme *model.Scheme, patch *model.SchemePatch) (*model.Scheme, *model.AppError) {
if err := a.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
scheme.Patch(patch)
scheme, err := a.UpdateScheme(scheme)
if err != nil {
return nil, err
}
return scheme, err
}
func (a *App) UpdateScheme(scheme *model.Scheme) (*model.Scheme, *model.AppError) {
if err := a.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
scheme, err := a.Srv().Store().Scheme().Save(scheme)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("UpdateScheme", "app.scheme.save.invalid_scheme.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpdateScheme", "app.scheme.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return scheme, nil
}
func (a *App) DeleteScheme(schemeId string) (*model.Scheme, *model.AppError) {
if err := a.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
scheme, err := a.Srv().Store().Scheme().Delete(schemeId)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("DeleteScheme", "app.scheme.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("DeleteScheme", "app.scheme.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return scheme, nil
}
func (a *App) GetTeamsForSchemePage(scheme *model.Scheme, page int, perPage int) ([]*model.Team, *model.AppError) {
if err := a.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
return a.GetTeamsForScheme(scheme, page*perPage, perPage)
}
func (a *App) GetTeamsForScheme(scheme *model.Scheme, offset int, limit int) ([]*model.Team, *model.AppError) {
if err := a.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
teams, err := a.Srv().Store().Team().GetTeamsByScheme(scheme.Id, offset, limit)
if err != nil {
return nil, model.NewAppError("GetTeamsForScheme", "app.team.get_by_scheme.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teams, nil
}
func (a *App) GetChannelsForSchemePage(scheme *model.Scheme, page int, perPage int) (model.ChannelList, *model.AppError) {
if err := a.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
return a.GetChannelsForScheme(scheme, page*perPage, perPage)
}
func (a *App) GetChannelsForScheme(scheme *model.Scheme, offset int, limit int) (model.ChannelList, *model.AppError) {
if err := a.IsPhase2MigrationCompleted(); err != nil {
return nil, err
}
channelList, nErr := a.Srv().Store().Channel().GetChannelsByScheme(scheme.Id, offset, limit)
if nErr != nil {
return nil, model.NewAppError("GetChannelsForScheme", "app.channel.get_by_scheme.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return channelList, nil
}
func (s *Server) IsPhase2MigrationCompleted() *model.AppError {
if s.phase2PermissionsMigrationComplete {
return nil
}
if _, err := s.Store().System().GetByName(model.MigrationKeyAdvancedPermissionsPhase2); err != nil {
return model.NewAppError("App.IsPhase2MigrationCompleted", "app.schemes.is_phase_2_migration_completed.not_completed.app_error", nil, "", http.StatusNotImplemented).Wrap(err)
}
s.phase2PermissionsMigrationComplete = true
return nil
}
func (a *App) IsPhase2MigrationCompleted() *model.AppError {
return a.Srv().IsPhase2MigrationCompleted()
}
func (a *App) SchemesIterator(scope string, batchSize int) func() []*model.Scheme {
offset := 0
return func() []*model.Scheme {
schemes, err := a.Srv().Store().Scheme().GetAllPage(scope, offset, batchSize)
if err != nil {
return []*model.Scheme{}
}
offset += batchSize
return schemes
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
)
func (a *App) TestElasticsearch(cfg *model.Config) *model.AppError {
if *cfg.ElasticsearchSettings.Password == model.FakeSetting {
if *cfg.ElasticsearchSettings.ConnectionURL == *a.Config().ElasticsearchSettings.ConnectionURL && *cfg.ElasticsearchSettings.Username == *a.Config().ElasticsearchSettings.Username {
*cfg.ElasticsearchSettings.Password = *a.Config().ElasticsearchSettings.Password
} else {
return model.NewAppError("TestElasticsearch", "ent.elasticsearch.test_config.reenter_password", nil, "", http.StatusBadRequest)
}
}
seI := a.SearchEngine().ElasticsearchEngine
if seI == nil {
err := model.NewAppError("TestElasticsearch", "ent.elasticsearch.test_config.license.error", nil, "", http.StatusNotImplemented)
return err
}
if err := seI.TestConfig(cfg); err != nil {
return err
}
return nil
}
func (a *App) SetSearchEngine(se *searchengine.Broker) {
a.ch.srv.platform.SearchEngine = se
}
func (a *App) PurgeElasticsearchIndexes() *model.AppError {
engine := a.SearchEngine().ElasticsearchEngine
if engine == nil {
err := model.NewAppError("PurgeElasticsearchIndexes", "ent.elasticsearch.test_config.license.error", nil, "", http.StatusNotImplemented)
return err
}
if err := engine.PurgeIndexes(); err != nil {
return err
}
return nil
}
func (a *App) PurgeBleveIndexes() *model.AppError {
engine := a.SearchEngine().BleveEngine
if engine == nil {
err := model.NewAppError("PurgeBleveIndexes", "searchengine.bleve.disabled.error", nil, "", http.StatusNotImplemented)
return err
}
if err := engine.PurgeIndexes(); err != nil {
return err
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"io"
"net/http"
"net/url"
"runtime"
"strconv"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mail"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
PropSecurityURL = "https://securityupdatecheck.mattermost.com"
SecurityUpdatePeriod = 86400000 // 24 hours in milliseconds.
PropSecurityID = "id"
PropSecurityBuild = "b"
PropSecurityEnterpriseReady = "be"
PropSecurityDatabase = "db"
PropSecurityOS = "os"
PropSecurityUserCount = "uc"
PropSecurityTeamCount = "tc"
PropSecurityActiveUserCount = "auc"
PropSecurityUnitTests = "ut"
)
func (s *Server) DoSecurityUpdateCheck() {
if !*s.platform.Config().ServiceSettings.EnableSecurityFixAlert {
return
}
props, err := s.Store().System().Get()
if err != nil {
return
}
lastSecurityTime, _ := strconv.ParseInt(props[model.SystemLastSecurityTime], 10, 0)
currentTime := model.GetMillis()
if (currentTime - lastSecurityTime) > SecurityUpdatePeriod {
mlog.Debug("Checking for security update from Mattermost")
v := url.Values{}
v.Set(PropSecurityID, s.TelemetryId())
v.Set(PropSecurityBuild, model.CurrentVersion+"."+model.BuildNumber)
v.Set(PropSecurityEnterpriseReady, model.BuildEnterpriseReady)
v.Set(PropSecurityDatabase, *s.platform.Config().SqlSettings.DriverName)
v.Set(PropSecurityOS, runtime.GOOS)
if props[model.SystemRanUnitTests] != "" {
v.Set(PropSecurityUnitTests, "1")
} else {
v.Set(PropSecurityUnitTests, "0")
}
systemSecurityLastTime := &model.System{Name: model.SystemLastSecurityTime, Value: strconv.FormatInt(currentTime, 10)}
if lastSecurityTime == 0 {
s.Store().System().Save(systemSecurityLastTime)
} else {
s.Store().System().Update(systemSecurityLastTime)
}
if count, err := s.Store().User().Count(model.UserCountOptions{IncludeDeleted: true}); err == nil {
v.Set(PropSecurityUserCount, strconv.FormatInt(count, 10))
}
if ucr, err := s.Store().Status().GetTotalActiveUsersCount(); err == nil {
v.Set(PropSecurityActiveUserCount, strconv.FormatInt(ucr, 10))
}
if teamCount, err := s.Store().Team().AnalyticsTeamCount(nil); err == nil {
v.Set(PropSecurityTeamCount, strconv.FormatInt(teamCount, 10))
}
res, err := http.Get(PropSecurityURL + "/security?" + v.Encode())
if err != nil {
mlog.Error("Failed to get security update information from Mattermost.")
return
}
defer res.Body.Close()
var bulletins model.SecurityBulletins
if jsonErr := json.NewDecoder(res.Body).Decode(&bulletins); jsonErr != nil {
s.Log().Error("Failed to decode JSON", mlog.Err(jsonErr))
return
}
for _, bulletin := range bulletins {
if bulletin.AppliesToVersion == model.CurrentVersion {
if props["SecurityBulletin_"+bulletin.Id] == "" {
users, userErr := s.Store().User().GetSystemAdminProfiles()
if userErr != nil {
mlog.Error("Failed to get system admins for security update information from Mattermost.")
return
}
resBody, err := http.Get(PropSecurityURL + "/bulletins/" + bulletin.Id)
if err != nil {
mlog.Error("Failed to get security bulletin details")
return
}
body, err := io.ReadAll(resBody.Body)
resBody.Body.Close()
if err != nil || resBody.StatusCode != 200 {
mlog.Error("Failed to read security bulletin details")
return
}
for _, user := range users {
mlog.Info("Sending security bulletin", mlog.String("bulletin_id", bulletin.Id), mlog.String("user_email", user.Email))
license := s.License()
mailConfig := s.MailServiceConfig()
mail.SendMailUsingConfig(user.Email, i18n.T("mattermost.bulletin.subject"), string(body), mailConfig, license != nil && *license.Features.Compliance, "", "", "", "", "SecurityUpdateCheck")
}
bulletinSeen := &model.System{Name: "SecurityBulletin_" + bulletin.Id, Value: bulletin.Id}
s.Store().System().Save(bulletinSeen)
}
}
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/getsentry/sentry-go"
sentryhttp "github.com/getsentry/sentry-go/http"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/rs/cors"
"golang.org/x/crypto/acme/autocert"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin/scheduler"
"github.com/mattermost/mattermost-server/v6/server/channels/app/email"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/app/teams"
"github.com/mattermost/mattermost-server/v6/server/channels/app/users"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/active_users"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/expirynotify"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/export_delete"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/export_process"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/extract_content"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/hosted_purchase_screening"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/import_delete"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/import_process"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/last_accessible_file"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/last_accessible_post"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/migrations"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/notify_admin"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/product_notices"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs/resend_invitation_email"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/services/awsmeter"
"github.com/mattermost/mattermost-server/v6/server/platform/services/cache"
"github.com/mattermost/mattermost-server/v6/server/platform/services/httpservice"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine/bleveengine"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine/bleveengine/indexer"
"github.com/mattermost/mattermost-server/v6/server/platform/services/sharedchannel"
"github.com/mattermost/mattermost-server/v6/server/platform/services/telemetry"
"github.com/mattermost/mattermost-server/v6/server/platform/services/timezones"
"github.com/mattermost/mattermost-server/v6/server/platform/services/tracing"
"github.com/mattermost/mattermost-server/v6/server/platform/services/upgrader"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mail"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/templates"
)
// declaring this as var to allow overriding in tests
var SentryDSN = "placeholder_sentry_dsn"
type Server struct {
// RootRouter is the starting point for all HTTP requests to the server.
RootRouter *mux.Router
// LocalRouter is the starting point for all the local UNIX socket
// requests to the server
LocalRouter *mux.Router
// Router is the starting point for all web, api4 and ws requests to the server. It differs
// from RootRouter only if the SiteURL contains a /subpath.
Router *mux.Router
Server *http.Server
ListenAddr *net.TCPAddr
RateLimiter *RateLimiter
localModeServer *http.Server
didFinishListen chan struct{}
EmailService email.ServiceInterface
httpService httpservice.HTTPService
PushNotificationsHub PushNotificationsHub
pushNotificationClient *http.Client // TODO: move this to it's own package
runEssentialJobs bool
Jobs *jobs.JobServer
licenseWrapper *licenseWrapper
timezones *timezones.Timezones
htmlTemplateWatcher *templates.Container
seenPendingPostIdsCache cache.Cache
openGraphDataCache cache.Cache
clusterLeaderListenerId string
loggerLicenseListenerId string
platform *platform.PlatformService
platformOptions []platform.Option
telemetryService *telemetry.TelemetryService
userService *users.UserService
teamService *teams.TeamService
serviceMux sync.RWMutex
remoteClusterService remotecluster.RemoteClusterServiceIFace
sharedChannelService SharedChannelServiceIFace // TODO: platform: move to platform package
phase2PermissionsMigrationComplete bool
Audit *audit.Audit
joinCluster bool
// startSearchEngine bool
skipPostInit bool
Cloud einterfaces.CloudInterface
tracer *tracing.Tracer
products map[string]product.Product
services map[product.ServiceKey]any
hooksManager *product.HooksManager
}
func (s *Server) Store() store.Store {
if s.platform != nil {
return s.platform.Store
}
return nil
}
func (s *Server) SetStore(st store.Store) {
if s.platform != nil {
s.platform.Store = st
}
}
func NewServer(options ...Option) (*Server, error) {
rootRouter := mux.NewRouter()
localRouter := mux.NewRouter()
s := &Server{
RootRouter: rootRouter,
LocalRouter: localRouter,
timezones: timezones.New(),
products: make(map[string]product.Product),
services: make(map[product.ServiceKey]any),
}
for _, option := range options {
if err := option(s); err != nil {
return nil, errors.Wrap(err, "failed to apply option")
}
}
// Following outlines the specific set of steps
// performed during server bootup. They are sensitive to order
// and has dependency requirements with the previous step.
//
// Step 1: Platform.
if s.platform == nil {
ps, sErr := platform.New(platform.ServiceConfig{}, s.platformOptions...)
if sErr != nil {
return nil, errors.Wrap(sErr, "failed to initialize platform")
}
s.platform = ps
}
subpath, err := utils.GetSubpathFromConfig(s.platform.Config())
if err != nil {
return nil, errors.Wrap(err, "failed to parse SiteURL subpath")
}
s.Router = s.RootRouter.PathPrefix(subpath).Subrouter()
s.httpService = httpservice.MakeHTTPService(s.platform)
// Step 2: Init Enterprise
// Depends on step 1 (s.Platform must be non-nil)
s.initEnterprise()
// Needed to run before loading license.
s.userService, err = users.New(users.ServiceConfig{
UserStore: s.Store().User(),
SessionStore: s.Store().Session(),
OAuthStore: s.Store().OAuth(),
ConfigFn: s.platform.Config,
Metrics: s.GetMetrics(),
Cluster: s.platform.Cluster(),
LicenseFn: s.License,
})
if err != nil {
return nil, errors.Wrapf(err, "unable to create users service")
}
if model.BuildEnterpriseReady == "true" {
// Dependent on user service
s.LoadLicense()
}
s.licenseWrapper = &licenseWrapper{
srv: s,
}
s.teamService, err = teams.New(teams.ServiceConfig{
TeamStore: s.Store().Team(),
ChannelStore: s.Store().Channel(),
GroupStore: s.Store().Group(),
Users: s.userService,
WebHub: s.platform,
ConfigFn: s.platform.Config,
LicenseFn: s.License,
})
if err != nil {
return nil, errors.Wrapf(err, "unable to create teams service")
}
s.hooksManager = product.NewHooksManager(s.GetMetrics())
// ensure app implements `product.UserService`
var _ product.UserService = (*App)(nil)
app := New(ServerConnector(s.Channels()))
serviceMap := map[product.ServiceKey]any{
ServerKey: s,
product.ConfigKey: s.platform,
product.LicenseKey: s.licenseWrapper,
product.FilestoreKey: s.platform.FileBackend(),
product.FileInfoStoreKey: &fileInfoWrapper{srv: s},
product.ClusterKey: s.platform,
product.UserKey: app,
product.LogKey: s.platform.Log(),
product.CloudKey: &cloudWrapper{cloud: s.Cloud},
product.KVStoreKey: s.platform,
product.StoreKey: store.NewStoreServiceAdapter(s.Store()),
product.SystemKey: &systemServiceAdapter{server: s},
product.SessionKey: app,
product.FrontendKey: app,
product.CommandKey: app,
}
// Step 4: Initialize products.
// Depends on s.httpService.
err = s.initializeProducts(product.GetProducts(), serviceMap)
if err != nil {
return nil, errors.Wrap(err, "failed to initialize products")
}
s.services = serviceMap
// After channel is initialized set it to the App object
channelsWrapper, ok := serviceMap[product.ChannelKey].(*channelsWrapper)
if !ok {
return nil, errors.Wrap(err, "channels product is not initialized")
}
app.ch = channelsWrapper.app.ch
// It is important to initialize the hub only after the global logger is set
// to avoid race conditions while logging from inside the hub.
// Step 5: Start hub in platform which the hub depends on s.Channels() (step 4)
s.platform.Start()
// -------------------------------------------------------------------------
// Everything below this is not order sensitive and safe to be moved around.
// If you are adding a new field that is non-channels specific, please add
// below this. Otherwise, please add it to Channels struct in app/channels.go.
// -------------------------------------------------------------------------
if *s.platform.Config().LogSettings.EnableDiagnostics && *s.platform.Config().LogSettings.EnableSentry {
if strings.Contains(SentryDSN, "placeholder") {
mlog.Warn("Sentry reporting is enabled, but SENTRY_DSN is not set. Disabling reporting.")
} else {
if err2 := sentry.Init(sentry.ClientOptions{
Dsn: SentryDSN,
Release: model.BuildHash,
AttachStacktrace: true,
BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
// sanitize data sent to sentry to reduce exposure of PII
if event.Request != nil {
event.Request.Cookies = ""
event.Request.QueryString = ""
event.Request.Headers = nil
event.Request.Data = ""
}
return event
},
EnableTracing: false,
TracesSampler: sentry.TracesSampler(func(ctx sentry.SamplingContext) float64 {
return 0.0
}),
}); err2 != nil {
mlog.Warn("Sentry could not be initiated, probably bad DSN?", mlog.Err(err2))
}
}
}
if *s.platform.Config().ServiceSettings.EnableOpenTracing {
tracer, err2 := tracing.New()
if err2 != nil {
return nil, err2
}
s.tracer = tracer
}
s.pushNotificationClient = s.httpService.MakeClient(true)
if err2 := utils.TranslationsPreInit(); err2 != nil {
return nil, errors.Wrapf(err2, "unable to load Mattermost translation files")
}
model.AppErrorInit(i18n.T)
if s.seenPendingPostIdsCache, err = s.platform.CacheProvider().NewCache(&cache.CacheOptions{
Size: PendingPostIDsCacheSize,
}); err != nil {
return nil, errors.Wrap(err, "Unable to create pending post ids cache")
}
if s.openGraphDataCache, err = s.platform.CacheProvider().NewCache(&cache.CacheOptions{
Size: openGraphMetadataCacheSize,
}); err != nil {
return nil, errors.Wrap(err, "Unable to create opengraphdata cache")
}
s.createPushNotificationsHub(request.EmptyContext(s.Log()))
if err2 := i18n.InitTranslations(*s.platform.Config().LocalizationSettings.DefaultServerLocale, *s.platform.Config().LocalizationSettings.DefaultClientLocale); err2 != nil {
return nil, errors.Wrapf(err2, "unable to load Mattermost translation files")
}
templatesDir, ok := templates.GetTemplateDirectory()
if !ok {
return nil, errors.New("Failed find server templates in \"templates\" directory or MM_SERVER_PATH")
}
htmlTemplateWatcher, errorsChan, err2 := templates.NewWithWatcher(templatesDir)
if err2 != nil {
return nil, errors.Wrap(err2, "cannot initialize server templates")
}
s.Go(func() {
for err2 := range errorsChan {
mlog.Warn("Server templates error", mlog.Err(err2))
}
})
s.htmlTemplateWatcher = htmlTemplateWatcher
s.telemetryService, err = telemetry.New(New(ServerConnector(s.Channels())), s.Store(), s.platform.SearchEngine, s.Log(), *s.Config().LogSettings.VerboseDiagnostics)
if err != nil {
return nil, errors.Wrapf(err, "unable to initialize telemetry service")
}
s.platform.SetTelemetryId(s.TelemetryId()) // TODO: move this into platform once telemetry service moved to platform.
emailService, err := email.NewService(email.ServiceConfig{
ConfigFn: s.platform.Config,
LicenseFn: s.License,
TemplatesContainer: s.TemplatesContainer(),
UserService: s.userService,
Store: s.GetStore(),
})
if err != nil {
return nil, errors.Wrapf(err, "unable to initialize email service")
}
s.EmailService = emailService
s.platform.SetupFeatureFlags()
s.initJobs()
s.clusterLeaderListenerId = s.AddClusterLeaderChangedListener(func() {
mlog.Info("Cluster leader changed. Determining if job schedulers should be running:", mlog.Bool("isLeader", s.IsLeader()))
if s.Jobs != nil {
s.Jobs.HandleClusterLeaderChange(s.IsLeader())
}
s.platform.SetupFeatureFlags()
})
// If configured with a subpath, redirect 404s at the root back into the subpath.
if subpath != "/" {
s.RootRouter.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.URL.Path = path.Join(subpath, r.URL.Path)
http.Redirect(w, r, r.URL.String(), http.StatusFound)
})
}
if _, err = url.ParseRequestURI(*s.platform.Config().ServiceSettings.SiteURL); err != nil {
mlog.Error("SiteURL must be set. Some features will operate incorrectly if the SiteURL is not set. See documentation for details: https://docs.mattermost.com/configure/configuration-settings.html#site-url")
}
// Start email batching because it's not like the other jobs
s.platform.AddConfigListener(func(_, _ *model.Config) {
s.EmailService.InitEmailBatching()
})
logCurrentVersion := fmt.Sprintf("Current version is %v (%v/%v/%v/%v)", model.CurrentVersion, model.BuildNumber, model.BuildDate, model.BuildHash, model.BuildHashEnterprise)
mlog.Info(
logCurrentVersion,
mlog.String("current_version", model.CurrentVersion),
mlog.String("build_number", model.BuildNumber),
mlog.String("build_date", model.BuildDate),
mlog.String("build_hash", model.BuildHash),
mlog.String("build_hash_enterprise", model.BuildHashEnterprise),
)
if model.BuildEnterpriseReady == "true" {
mlog.Info("Enterprise Build", mlog.Bool("enterprise_build", true))
} else {
mlog.Info("Team Edition Build", mlog.Bool("enterprise_build", false))
}
pwd, _ := os.Getwd()
mlog.Info("Printing current working", mlog.String("directory", pwd))
mlog.Info("Loaded config", mlog.String("source", s.platform.DescribeConfig()))
license := s.License()
allowAdvancedLogging := license != nil && *license.Features.AdvancedLogging
if s.Audit == nil {
s.Audit = &audit.Audit{}
s.Audit.Init(audit.DefMaxQueueSize)
if err = s.configureAudit(s.Audit, allowAdvancedLogging); err != nil {
mlog.Error("Error configuring audit", mlog.Err(err))
}
}
s.platform.RemoveUnlicensedLogTargets(license)
s.platform.EnableLoggingMetrics()
s.loggerLicenseListenerId = s.AddLicenseListener(func(oldLicense, newLicense *model.License) {
s.platform.RemoveUnlicensedLogTargets(newLicense)
s.platform.EnableLoggingMetrics()
})
// if enabled - perform initial product notices fetch
if *s.platform.Config().AnnouncementSettings.AdminNoticesEnabled || *s.platform.Config().AnnouncementSettings.UserNoticesEnabled {
go func() {
appInstance := New(ServerConnector(s.Channels()))
if err := appInstance.UpdateProductNotices(); err != nil {
mlog.Warn("Failed to perform initial product notices fetch", mlog.Err(err))
}
}()
}
if s.skipPostInit {
return s, nil
}
s.platform.AddConfigListener(func(old, new *model.Config) {
appInstance := New(ServerConnector(s.Channels()))
if *old.GuestAccountsSettings.Enable && !*new.GuestAccountsSettings.Enable {
c := request.EmptyContext(s.Log())
if appErr := appInstance.DeactivateGuests(c); appErr != nil {
mlog.Error("Unable to deactivate guest accounts", mlog.Err(appErr))
}
}
})
// Disable active guest accounts on first run if guest accounts are disabled
if !*s.platform.Config().GuestAccountsSettings.Enable {
appInstance := New(ServerConnector(s.Channels()))
c := request.EmptyContext(s.Log())
if appErr := appInstance.DeactivateGuests(c); appErr != nil {
mlog.Error("Unable to deactivate guest accounts", mlog.Err(appErr))
}
}
if s.runEssentialJobs {
s.Go(func() {
appInstance := New(ServerConnector(s.Channels()))
s.runLicenseExpirationCheckJob()
runDNDStatusExpireJob(appInstance)
runPostReminderJob(appInstance)
})
s.runJobs()
}
s.doAppMigrations()
s.initPostMetadata()
// Dump the image cache if the proxy settings have changed. (need switch URLs to the correct proxy)
s.platform.AddConfigListener(func(oldCfg, newCfg *model.Config) {
if (oldCfg.ImageProxySettings.Enable != newCfg.ImageProxySettings.Enable) ||
(oldCfg.ImageProxySettings.ImageProxyType != newCfg.ImageProxySettings.ImageProxyType) ||
(oldCfg.ImageProxySettings.RemoteImageProxyURL != newCfg.ImageProxySettings.RemoteImageProxyURL) ||
(oldCfg.ImageProxySettings.RemoteImageProxyOptions != newCfg.ImageProxySettings.RemoteImageProxyOptions) {
s.openGraphDataCache.Purge()
}
})
return s, nil
}
func (s *Server) runJobs() {
s.Go(func() {
runSecurityJob(s)
})
s.Go(func() {
firstRun, err := s.getFirstServerRunTimestamp()
if err != nil {
mlog.Warn("Fetching time of first server run failed. Setting to 'now'.")
s.ensureFirstServerRunTimestamp()
firstRun = utils.MillisFromTime(time.Now())
}
s.telemetryService.RunTelemetryJob(firstRun)
})
s.Go(func() {
runSessionCleanupJob(s)
})
s.Go(func() {
runJobsCleanupJob(s)
})
s.Go(func() {
runTokenCleanupJob(s)
})
s.Go(func() {
runCommandWebhookCleanupJob(s)
})
s.Go(func() {
runConfigCleanupJob(s)
})
if complianceI := s.Channels().Compliance; complianceI != nil {
go complianceI.StartComplianceDailyJob()
}
if *s.platform.Config().JobSettings.RunJobs && s.Jobs != nil {
if err := s.Jobs.StartWorkers(); err != nil {
mlog.Error("Failed to start job server workers", mlog.Err(err))
}
}
if *s.platform.Config().JobSettings.RunScheduler && s.Jobs != nil {
if err := s.Jobs.StartSchedulers(); err != nil {
mlog.Error("Failed to start job server schedulers", mlog.Err(err))
}
}
if *s.platform.Config().ServiceSettings.EnableAWSMetering {
runReportToAWSMeterJob(s)
}
}
// Global app options that should be applied to apps created by this server
func (s *Server) AppOptions() []AppOption {
return []AppOption{
ServerConnector(s.Channels()),
}
}
func (s *Server) Channels() *Channels {
ch, _ := s.products["channels"].(*Channels)
return ch
}
// Return Database type (postgres or mysql) and current version of the schema
func (s *Server) DatabaseTypeAndSchemaVersion() (string, string) {
schemaVersion, _ := s.Store().GetDBSchemaVersion()
return *s.platform.Config().SqlSettings.DriverName, strconv.Itoa(schemaVersion)
}
func (s *Server) startInterClusterServices(license *model.License) error {
if license == nil {
mlog.Debug("No license provided; Remote Cluster services disabled")
return nil
}
// Remote Cluster service
// License check (assume enabled if shared channels enabled)
if !license.HasRemoteClusterService() && !license.HasSharedChannels() {
mlog.Debug("License does not have Remote Cluster services enabled")
return nil
}
// Config check
if !*s.platform.Config().ExperimentalSettings.EnableRemoteClusterService && !*s.platform.Config().ExperimentalSettings.EnableSharedChannels {
mlog.Debug("Remote Cluster Service disabled via config")
return nil
}
var err error
rcs, err := remotecluster.NewRemoteClusterService(s)
if err != nil {
return err
}
if err = rcs.Start(); err != nil {
return err
}
s.serviceMux.Lock()
s.remoteClusterService = rcs
s.serviceMux.Unlock()
// Shared Channels service (depends on remote cluster service)
// License check
if !license.HasSharedChannels() {
mlog.Debug("License does not have shared channels enabled")
return nil
}
// Config check
if !*s.platform.Config().ExperimentalSettings.EnableSharedChannels {
mlog.Debug("Shared Channels Service disabled via config")
return nil
}
appInstance := New(ServerConnector(s.Channels()))
scs, err := sharedchannel.NewSharedChannelService(s, appInstance)
if err != nil {
return err
}
s.platform.SetSharedChannelService(scs)
if err = scs.Start(); err != nil {
return err
}
s.serviceMux.Lock()
s.sharedChannelService = scs
s.serviceMux.Unlock()
return nil
}
const TimeToWaitForConnectionsToCloseOnServerShutdown = time.Second
func (s *Server) StopHTTPServer() {
if s.Server != nil {
ctx, cancel := context.WithTimeout(context.Background(), TimeToWaitForConnectionsToCloseOnServerShutdown)
defer cancel()
didShutdown := false
for s.didFinishListen != nil && !didShutdown {
if err := s.Server.Shutdown(ctx); err != nil {
mlog.Warn("Unable to shutdown server", mlog.Err(err))
}
timer := time.NewTimer(time.Millisecond * 50)
select {
case <-s.didFinishListen:
didShutdown = true
case <-timer.C:
}
timer.Stop()
}
s.Server.Close()
s.Server = nil
}
}
func (s *Server) Shutdown() {
s.Log().Info("Stopping Server...")
defer sentry.Flush(2 * time.Second)
s.RemoveLicenseListener(s.loggerLicenseListenerId)
s.RemoveClusterLeaderChangedListener(s.clusterLeaderListenerId)
if s.tracer != nil {
if err := s.tracer.Close(); err != nil {
s.Log().Warn("Unable to cleanly shutdown opentracing client", mlog.Err(err))
}
}
err := s.telemetryService.Shutdown()
if err != nil {
s.Log().Warn("Unable to cleanly shutdown telemetry client", mlog.Err(err))
}
s.serviceMux.RLock()
if s.sharedChannelService != nil {
if err = s.sharedChannelService.Shutdown(); err != nil {
s.Log().Error("Error shutting down shared channel services", mlog.Err(err))
}
}
if s.remoteClusterService != nil {
if err = s.remoteClusterService.Shutdown(); err != nil {
s.Log().Error("Error shutting down intercluster services", mlog.Err(err))
}
}
s.serviceMux.RUnlock()
s.StopHTTPServer()
s.stopLocalModeServer()
// Push notification hub needs to be shutdown after HTTP server
// to prevent stray requests from generating a push notification after it's shut down.
s.StopPushNotificationsHubWorkers()
s.htmlTemplateWatcher.Close()
s.platform.StopSearchEngine()
s.Audit.Shutdown()
s.platform.StopFeatureFlagUpdateJob()
if err = s.platform.ShutdownConfig(); err != nil {
s.Log().Warn("Failed to shut down config store", mlog.Err(err))
}
if s.platform.Cluster() != nil {
s.platform.Cluster().StopInterNodeCommunication()
}
if err = s.platform.ShutdownMetrics(); err != nil {
s.Log().Warn("Failed to stop metrics server", mlog.Err(err))
}
// Stopping email service after HTTP server has stopped to prevent
// any stray notifications from being queued.
s.EmailService.Stop()
// This must be done after the cluster is stopped.
if s.Jobs != nil {
// For simplicity we don't check if workers and schedulers are active
// before stopping them as both calls essentially become no-ops
// if nothing is running.
if err = s.Jobs.StopWorkers(); err != nil && !errors.Is(err, jobs.ErrWorkersNotRunning) {
s.Log().Warn("Failed to stop job server workers", mlog.Err(err))
}
if err = s.Jobs.StopSchedulers(); err != nil && !errors.Is(err, jobs.ErrSchedulersNotRunning) {
s.Log().Warn("Failed to stop job server schedulers", mlog.Err(err))
}
}
// Stop products.
// This needs to happen last because products are dependent
// on parent services.
for name, product := range s.products {
if err2 := product.Stop(); err2 != nil {
s.Log().Warn("Unable to cleanly stop product", mlog.String("name", name), mlog.Err(err2))
}
}
if err = s.platform.Shutdown(); err != nil {
s.Log().Warn("Failed to stop platform", mlog.Err(err))
}
s.Log().Info("Server stopped")
// shutdown main and notification loggers which will flush any remaining log records.
timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), time.Second*15)
defer timeoutCancel()
if err = s.NotificationsLog().ShutdownWithTimeout(timeoutCtx); err != nil {
fmt.Fprintf(os.Stderr, "Error shutting down notification logger: %v", err)
}
if err = s.Log().ShutdownWithTimeout(timeoutCtx); err != nil {
fmt.Fprintf(os.Stderr, "Error shutting down main logger: %v", err)
}
}
func (s *Server) Restart() error {
percentage, err := s.UpgradeToE0Status()
if err != nil || percentage != 100 {
return errors.Wrap(err, "unable to restart because the system has not been upgraded")
}
s.Shutdown()
argv0, err := exec.LookPath(os.Args[0])
if err != nil {
return err
}
if _, err = os.Stat(argv0); err != nil {
return err
}
mlog.Info("Restarting server")
return syscall.Exec(argv0, os.Args, os.Environ())
}
func (s *Server) CanIUpgradeToE0() error {
return upgrader.CanIUpgradeToE0()
}
func (s *Server) UpgradeToE0() error {
if err := upgrader.UpgradeToE0(); err != nil {
return err
}
upgradedFromTE := &model.System{Name: model.SystemUpgradedFromTeId, Value: "true"}
s.Store().System().Save(upgradedFromTE)
return nil
}
func (s *Server) UpgradeToE0Status() (int64, error) {
return upgrader.UpgradeToE0Status()
}
// Go creates a goroutine, but maintains a record of it to ensure that execution completes before
// the server is shutdown.
func (s *Server) Go(f func()) {
s.platform.Go(f)
}
// GoBuffered acts like a semaphore which creates a goroutine, but maintains a record of it
// to ensure that execution completes before the server is shutdown.
func (s *Server) GoBuffered(f func()) {
s.platform.GoBuffered(f)
}
var corsAllowedMethods = []string{
"POST",
"GET",
"OPTIONS",
"PUT",
"PATCH",
"DELETE",
}
// golang.org/x/crypto/acme/autocert/autocert.go
func handleHTTPRedirect(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" && r.Method != "HEAD" {
http.Error(w, "Use HTTPS", http.StatusBadRequest)
return
}
target := "https://" + stripPort(r.Host) + r.URL.RequestURI()
http.Redirect(w, r, target, http.StatusFound)
}
// golang.org/x/crypto/acme/autocert/autocert.go
func stripPort(hostport string) string {
host, _, err := net.SplitHostPort(hostport)
if err != nil {
return hostport
}
return net.JoinHostPort(host, "443")
}
func (s *Server) Start() error {
// Start products.
// This needs to happen before because products are dependent on the HTTP server.
// make sure channels starts first
if err := s.products["channels"].Start(); err != nil {
return errors.Wrap(err, "Unable to start channels")
}
for name, product := range s.products {
if name == "channels" {
continue
}
if err := product.Start(); err != nil {
return errors.Wrapf(err, "Unable to start %s", name)
}
}
if s.joinCluster && s.platform.Cluster() != nil {
s.registerClusterHandlers()
s.platform.Cluster().StartInterNodeCommunication()
}
if err := s.ensureInstallationDate(); err != nil {
return errors.Wrapf(err, "unable to ensure installation date")
}
if err := s.ensureFirstServerRunTimestamp(); err != nil {
return errors.Wrapf(err, "unable to ensure first run timestamp")
}
if err := s.Store().Status().ResetAll(); err != nil {
mlog.Error("Error to reset the server status.", mlog.Err(err))
}
if s.MailServiceConfig().SendEmailNotifications {
if err := mail.TestConnection(s.MailServiceConfig()); err != nil {
mlog.Error("Mail server connection test failed", mlog.Err(err))
}
}
err := s.FileBackend().TestConnection()
if err != nil {
if _, ok := err.(*filestore.S3FileBackendNoBucketError); ok {
err = s.FileBackend().(*filestore.S3FileBackend).MakeBucket()
}
if err != nil {
mlog.Error("Problem with file storage settings", mlog.Err(err))
}
}
s.checkPushNotificationServerURL()
s.platform.ReloadConfig()
mlog.Info("Starting Server...")
var handler http.Handler = s.RootRouter
if *s.platform.Config().LogSettings.EnableDiagnostics && *s.platform.Config().LogSettings.EnableSentry && !strings.Contains(SentryDSN, "placeholder") {
sentryHandler := sentryhttp.New(sentryhttp.Options{
Repanic: true,
})
handler = sentryHandler.Handle(handler)
}
if allowedOrigins := *s.platform.Config().ServiceSettings.AllowCorsFrom; allowedOrigins != "" {
exposedCorsHeaders := *s.platform.Config().ServiceSettings.CorsExposedHeaders
allowCredentials := *s.platform.Config().ServiceSettings.CorsAllowCredentials
debug := *s.platform.Config().ServiceSettings.CorsDebug
corsWrapper := cors.New(cors.Options{
AllowedOrigins: strings.Fields(allowedOrigins),
AllowedMethods: corsAllowedMethods,
AllowedHeaders: []string{"*"},
ExposedHeaders: strings.Fields(exposedCorsHeaders),
MaxAge: 86400,
AllowCredentials: allowCredentials,
Debug: debug,
})
// If we have debugging of CORS turned on then forward messages to logs
if debug {
corsWrapper.Log = s.Log().With(mlog.String("source", "cors")).StdLogger(mlog.LvlDebug)
}
handler = corsWrapper.Handler(handler)
}
if *s.platform.Config().RateLimitSettings.Enable {
mlog.Info("RateLimiter is enabled")
rateLimiter, err2 := NewRateLimiter(&s.platform.Config().RateLimitSettings, s.platform.Config().ServiceSettings.TrustedProxyIPHeader)
if err2 != nil {
return err2
}
s.RateLimiter = rateLimiter
handler = rateLimiter.RateLimitHandler(handler)
}
// Creating a logger for logging errors from http.Server at error level
errStdLog := s.Log().With(mlog.String("source", "httpserver")).StdLogger(mlog.LvlError)
s.Server = &http.Server{
Handler: handler,
ReadTimeout: time.Duration(*s.platform.Config().ServiceSettings.ReadTimeout) * time.Second,
WriteTimeout: time.Duration(*s.platform.Config().ServiceSettings.WriteTimeout) * time.Second,
IdleTimeout: time.Duration(*s.platform.Config().ServiceSettings.IdleTimeout) * time.Second,
ErrorLog: errStdLog,
}
addr := *s.platform.Config().ServiceSettings.ListenAddress
if addr == "" {
if *s.platform.Config().ServiceSettings.ConnectionSecurity == model.ConnSecurityTLS {
addr = ":https"
} else {
addr = ":http"
}
}
listener, err := net.Listen("tcp", addr)
if err != nil {
return errors.Wrapf(err, i18n.T("api.server.start_server.starting.critical"), err)
}
s.ListenAddr = listener.Addr().(*net.TCPAddr)
logListeningPort := fmt.Sprintf("Server is listening on %v", listener.Addr().String())
mlog.Info(logListeningPort, mlog.String("address", listener.Addr().String()))
m := &autocert.Manager{
Cache: autocert.DirCache(*s.platform.Config().ServiceSettings.LetsEncryptCertificateCacheFile),
Prompt: autocert.AcceptTOS,
}
if *s.platform.Config().ServiceSettings.Forward80To443 {
if host, port, err := net.SplitHostPort(addr); err != nil {
mlog.Error("Unable to setup forwarding", mlog.Err(err))
} else if port != "443" {
return fmt.Errorf(i18n.T("api.server.start_server.forward80to443.enabled_but_listening_on_wrong_port"), port)
} else {
httpListenAddress := net.JoinHostPort(host, "http")
if *s.platform.Config().ServiceSettings.UseLetsEncrypt {
server := &http.Server{
Addr: httpListenAddress,
Handler: m.HTTPHandler(nil),
ErrorLog: s.Log().With(mlog.String("source", "le_forwarder_server")).StdLogger(mlog.LvlError),
}
go server.ListenAndServe()
} else {
go func() {
redirectListener, err := net.Listen("tcp", httpListenAddress)
if err != nil {
mlog.Error("Unable to setup forwarding", mlog.Err(err))
return
}
defer redirectListener.Close()
server := &http.Server{
Handler: http.HandlerFunc(handleHTTPRedirect),
ErrorLog: s.Log().With(mlog.String("source", "forwarder_server")).StdLogger(mlog.LvlError),
}
server.Serve(redirectListener)
}()
}
}
} else if *s.platform.Config().ServiceSettings.UseLetsEncrypt {
return errors.New(i18n.T("api.server.start_server.forward80to443.disabled_while_using_lets_encrypt"))
}
s.didFinishListen = make(chan struct{})
go func() {
var err error
if *s.platform.Config().ServiceSettings.ConnectionSecurity == model.ConnSecurityTLS {
tlsConfig := &tls.Config{
PreferServerCipherSuites: true,
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
}
switch *s.platform.Config().ServiceSettings.TLSMinVer {
case "1.0":
tlsConfig.MinVersion = tls.VersionTLS10
case "1.1":
tlsConfig.MinVersion = tls.VersionTLS11
default:
tlsConfig.MinVersion = tls.VersionTLS12
}
defaultCiphers := []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
}
if len(s.platform.Config().ServiceSettings.TLSOverwriteCiphers) == 0 {
tlsConfig.CipherSuites = defaultCiphers
} else {
var cipherSuites []uint16
for _, cipher := range s.platform.Config().ServiceSettings.TLSOverwriteCiphers {
value, ok := model.ServerTLSSupportedCiphers[cipher]
if !ok {
mlog.Warn("Unsupported cipher passed", mlog.String("cipher", cipher))
continue
}
cipherSuites = append(cipherSuites, value)
}
if len(cipherSuites) == 0 {
mlog.Warn("No supported ciphers passed, fallback to default cipher suite")
cipherSuites = defaultCiphers
}
tlsConfig.CipherSuites = cipherSuites
}
certFile := ""
keyFile := ""
if *s.platform.Config().ServiceSettings.UseLetsEncrypt {
tlsConfig.GetCertificate = m.GetCertificate
tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2")
} else {
certFile = *s.platform.Config().ServiceSettings.TLSCertFile
keyFile = *s.platform.Config().ServiceSettings.TLSKeyFile
}
s.Server.TLSConfig = tlsConfig
err = s.Server.ServeTLS(listener, certFile, keyFile)
} else {
err = s.Server.Serve(listener)
}
if err != nil && err != http.ErrServerClosed {
mlog.Fatal("Error starting server", mlog.Err(err))
time.Sleep(time.Second)
}
close(s.didFinishListen)
}()
if *s.platform.Config().ServiceSettings.EnableLocalMode {
if err := s.startLocalModeServer(); err != nil {
mlog.Fatal(err.Error())
}
}
if err := s.startInterClusterServices(s.License()); err != nil {
mlog.Error("Error starting inter-cluster services", mlog.Err(err))
}
return nil
}
func (s *Server) startLocalModeServer() error {
s.localModeServer = &http.Server{
Handler: s.LocalRouter,
}
socket := *s.platform.Config().ServiceSettings.LocalModeSocketLocation
if err := os.RemoveAll(socket); err != nil {
return errors.Wrapf(err, i18n.T("api.server.start_server.starting.critical"), err)
}
unixListener, err := net.Listen("unix", socket)
if err != nil {
return errors.Wrapf(err, i18n.T("api.server.start_server.starting.critical"), err)
}
if err = os.Chmod(socket, 0600); err != nil {
return errors.Wrapf(err, i18n.T("api.server.start_server.starting.critical"), err)
}
go func() {
err = s.localModeServer.Serve(unixListener)
if err != nil && err != http.ErrServerClosed {
mlog.Fatal("Error starting unix socket server", mlog.Err(err))
}
}()
return nil
}
func (s *Server) stopLocalModeServer() {
if s.localModeServer != nil {
s.localModeServer.Close()
}
}
func (a *App) OriginChecker() func(*http.Request) bool {
if allowed := *a.Config().ServiceSettings.AllowCorsFrom; allowed != "" {
if allowed != "*" {
siteURL, err := url.Parse(*a.Config().ServiceSettings.SiteURL)
if err == nil {
siteURL.Path = ""
allowed += " " + siteURL.String()
}
}
return utils.OriginChecker(allowed)
}
return nil
}
func (s *Server) checkPushNotificationServerURL() {
notificationServer := *s.platform.Config().EmailSettings.PushNotificationServer
if strings.HasPrefix(notificationServer, "http://") {
mlog.Warn("Your push notification server is configured with HTTP. For improved security, update to HTTPS in your configuration.")
}
}
func runSecurityJob(s *Server) {
doSecurity(s)
model.CreateRecurringTask("Security", func() {
doSecurity(s)
}, time.Hour*4)
}
func runTokenCleanupJob(s *Server) {
doTokenCleanup(s)
model.CreateRecurringTask("Token Cleanup", func() {
doTokenCleanup(s)
}, time.Hour*1)
}
func runCommandWebhookCleanupJob(s *Server) {
doCommandWebhookCleanup(s)
model.CreateRecurringTask("Command Hook Cleanup", func() {
doCommandWebhookCleanup(s)
}, time.Hour*1)
}
func runSessionCleanupJob(s *Server) {
doSessionCleanup(s)
model.CreateRecurringTask("Session Cleanup", func() {
doSessionCleanup(s)
}, time.Hour*24)
}
func runJobsCleanupJob(s *Server) {
doJobsCleanup(s)
model.CreateRecurringTask("Job Cleanup", func() {
doJobsCleanup(s)
}, time.Hour*24)
}
func runConfigCleanupJob(s *Server) {
doConfigCleanup(s)
model.CreateRecurringTask("Configuration Cleanup", func() {
doConfigCleanup(s)
}, time.Hour*24)
}
func (s *Server) runLicenseExpirationCheckJob() {
s.doLicenseExpirationCheck()
model.CreateRecurringTask("License Expiration Check", func() {
s.doLicenseExpirationCheck()
}, time.Hour*24)
}
func runReportToAWSMeterJob(s *Server) {
model.CreateRecurringTask("Collect and send usage report to AWS Metering Service", func() {
doReportUsageToAWSMeteringService(s)
}, time.Hour*model.AwsMeteringReportInterval)
}
func doReportUsageToAWSMeteringService(s *Server) {
awsMeter := awsmeter.New(s.Store(), s.platform.Config())
if awsMeter == nil {
mlog.Error("Cannot obtain instance of AWS Metering Service.")
return
}
dimensions := []string{model.AwsMeteringDimensionUsageHrs}
reports := awsMeter.GetUserCategoryUsage(dimensions, time.Now().UTC(), time.Now().Add(-model.AwsMeteringReportInterval*time.Hour).UTC())
awsMeter.ReportUserCategoryUsage(reports)
}
func doSecurity(s *Server) {
s.DoSecurityUpdateCheck()
}
func doTokenCleanup(s *Server) {
expiry := model.GetMillis() - model.MaxTokenExipryTime
mlog.Debug("Cleaning up token store.")
s.Store().Token().Cleanup(expiry)
}
func doCommandWebhookCleanup(s *Server) {
s.Store().CommandWebhook().Cleanup()
}
const (
sessionsCleanupBatchSize = 1000
jobsCleanupBatchSize = 1000
)
func doSessionCleanup(s *Server) {
mlog.Debug("Cleaning up session store.")
err := s.Store().Session().Cleanup(model.GetMillis(), sessionsCleanupBatchSize)
if err != nil {
mlog.Warn("Error while cleaning up sessions", mlog.Err(err))
}
}
func doJobsCleanup(s *Server) {
if *s.platform.Config().JobSettings.CleanupJobsThresholdDays < 0 {
return
}
mlog.Debug("Cleaning up jobs store.")
dur := time.Duration(*s.platform.Config().JobSettings.CleanupJobsThresholdDays) * time.Hour * 24
expiry := model.GetMillisForTime(time.Now().Add(-dur))
err := s.Store().Job().Cleanup(expiry, jobsCleanupBatchSize)
if err != nil {
mlog.Warn("Error while cleaning up jobs", mlog.Err(err))
}
}
func doConfigCleanup(s *Server) {
if *s.platform.Config().JobSettings.CleanupConfigThresholdDays < 0 || !config.IsDatabaseDSN(s.platform.DescribeConfig()) {
return
}
mlog.Info("Cleaning up configuration store.")
if err := s.platform.CleanUpConfig(); err != nil {
mlog.Warn("Error while cleaning up configurations", mlog.Err(err))
}
}
func (s *Server) HandleMetrics(route string, h http.Handler) {
s.platform.HandleMetrics(route, h)
}
func (s *Server) sendLicenseUpForRenewalEmail(users map[string]*model.User, license *model.License) *model.AppError {
key := model.LicenseUpForRenewalEmailSent + license.Id
if _, err := s.Store().System().GetByName(key); err == nil {
// return early because the key already exists and that means we already executed the code below to send email successfully
return nil
}
daysToExpiration := license.DaysToExpiration()
ctaLink, tokenToBeUsedForRenew, appErr := s.GenerateLicenseRenewalLink()
if appErr != nil {
return model.NewAppError("s.sendLicenseUpForRenewalEmail", "api.server.license_up_for_renewal.error_generating_link", nil, "", http.StatusInternalServerError).Wrap(appErr)
}
status, err := s.Cloud.GetLicenseSelfServeStatus("", tokenToBeUsedForRenew)
if err != nil {
return model.NewAppError("s.sendLicenseUpForRenewalEmail", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// we want to at least have one email sent out to an admin
countNotOks := 0
for _, user := range users {
name := user.FirstName
if name == "" {
name = user.Username
}
T := i18n.GetUserTranslations(user.Locale)
ctaTitle := T("api.templates.license_up_for_renewal_subtitle_two")
ctaText := T("api.templates.license_up_for_renewal_renew_now")
if !status.IsRenewable {
ctaTitle = ""
ctaText = T("api.templates.license_up_for_renewal_contact_sales")
ctaLink = "https://mattermost.com/contact-sales/"
}
if err := s.EmailService.SendLicenseUpForRenewalEmail(user.Email, name, user.Locale, *s.platform.Config().ServiceSettings.SiteURL, ctaTitle, ctaLink, ctaText, daysToExpiration); err != nil {
mlog.Error("Error sending license up for renewal email to", mlog.String("user_email", user.Email), mlog.Err(err))
countNotOks++
}
}
// if not even one admin got an email, we consider that this operation errored
if countNotOks == len(users) {
return model.NewAppError("s.sendLicenseUpForRenewalEmail", "api.server.license_up_for_renewal.error_sending_email", nil, "", http.StatusInternalServerError)
}
system := model.System{
Name: key,
Value: "true",
}
if err := s.Store().System().Save(&system); err != nil {
mlog.Debug("Failed to mark license up for renewal email sending as completed.", mlog.Err(err))
}
return nil
}
func (s *Server) doLicenseExpirationCheck() {
s.LoadLicense()
// This takes care of a rare edge case reported here https://mattermost.atlassian.net/browse/MM-40962
// To reproduce that case locally, attach a license to a server that was started with enterprise enabled
// Then restart using BUILD_ENTERPRISE=false make restart-server to enter Team Edition
if model.BuildEnterpriseReady != "true" {
mlog.Debug("Skipping license expiration check because no license is expected on Team Edition")
return
}
license := s.License()
if license == nil {
mlog.Debug("License cannot be found.")
return
}
if license.IsCloud() {
mlog.Debug("Skipping license expiration check for Cloud")
return
}
users, err := s.Store().User().GetSystemAdminProfiles()
if err != nil {
mlog.Error("Failed to get system admins for license expired message from Mattermost.")
return
}
if license.IsWithinExpirationPeriod() {
appErr := s.sendLicenseUpForRenewalEmail(users, license)
if appErr != nil {
mlog.Debug(appErr.Error())
}
return
}
if !license.IsPastGracePeriod() {
mlog.Debug("License is not past the grace period.")
return
}
ctaLink, tokenToBeUsedForRenew, appErr := s.GenerateLicenseRenewalLink()
if appErr != nil {
mlog.Debug(model.NewAppError("s.sendLicenseUpForRenewalEmail", "api.server.license_up_for_renewal.error_generating_link", nil, "", http.StatusInternalServerError).Wrap(appErr).Error())
return
}
status, err := s.Cloud.GetLicenseSelfServeStatus("", tokenToBeUsedForRenew)
if err != nil {
mlog.Debug(model.NewAppError("s.sendLicenseUpForRenewalEmail", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err).Error())
return
}
//send email to admin(s)
for _, user := range users {
user := user
if user.Email == "" {
mlog.Error("Invalid system admin email.", mlog.String("user_email", user.Email))
continue
}
T := i18n.GetUserTranslations(user.Locale)
ctaText := T("api.templates.remove_expired_license.body.renew_button")
if !status.IsRenewable {
ctaText = T("api.templates.license_up_for_renewal_contact_sales")
ctaLink = "https://mattermost.com/contact-sales/"
}
mlog.Debug("Sending license expired email.", mlog.String("user_email", user.Email))
s.Go(func() {
if err := s.SendRemoveExpiredLicenseEmail(user.Email, ctaText, ctaLink, user.Locale, *s.platform.Config().ServiceSettings.SiteURL); err != nil {
mlog.Error("Error while sending the license expired email.", mlog.String("user_email", user.Email), mlog.Err(err))
}
})
}
//remove the license
s.RemoveLicense()
}
// SendRemoveExpiredLicenseEmail formats an email and uses the email service to send the email to user with link pointing to CWS
// to renew the user license
func (s *Server) SendRemoveExpiredLicenseEmail(email, ctaText, ctaLink, locale, siteURL string) *model.AppError {
if err := s.EmailService.SendRemoveExpiredLicenseEmail(ctaText, ctaLink, email, locale, siteURL); err != nil {
return model.NewAppError("SendRemoveExpiredLicenseEmail", "api.license.remove_expired_license.failed.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (s *Server) FileBackend() filestore.FileBackend {
return s.platform.FileBackend()
}
func (s *Server) TotalWebsocketConnections() int {
return s.Platform().TotalWebsocketConnections()
}
func (s *Server) ClusterHealthScore() int {
return s.platform.Cluster().HealthScore()
}
func (ch *Channels) ClientConfigHash() string {
return ch.srv.Platform().ClientConfigHash()
}
func (s *Server) initJobs() {
s.Jobs = jobs.NewJobServer(s.platform, s.Store(), s.GetMetrics())
if jobsDataRetentionJobInterface != nil {
builder := jobsDataRetentionJobInterface(s)
s.Jobs.RegisterJobType(model.JobTypeDataRetention, builder.MakeWorker(), builder.MakeScheduler())
}
if jobsMessageExportJobInterface != nil {
builder := jobsMessageExportJobInterface(s)
s.Jobs.RegisterJobType(model.JobTypeMessageExport, builder.MakeWorker(), builder.MakeScheduler())
}
if jobsElasticsearchAggregatorInterface != nil {
builder := jobsElasticsearchAggregatorInterface(s)
s.Jobs.RegisterJobType(model.JobTypeElasticsearchPostAggregation, builder.MakeWorker(), builder.MakeScheduler())
}
if jobsElasticsearchIndexerInterface != nil {
builder := jobsElasticsearchIndexerInterface(s)
s.Jobs.RegisterJobType(model.JobTypeElasticsearchPostIndexing, builder.MakeWorker(), nil)
}
if jobsLdapSyncInterface != nil {
builder := jobsLdapSyncInterface(New(ServerConnector(s.Channels())))
s.Jobs.RegisterJobType(model.JobTypeLdapSync, builder.MakeWorker(), builder.MakeScheduler())
}
s.Jobs.RegisterJobType(
model.JobTypeBlevePostIndexing,
indexer.MakeWorker(s.Jobs, s.platform.SearchEngine.BleveEngine.(*bleveengine.BleveEngine)),
nil,
)
s.Jobs.RegisterJobType(
model.JobTypeMigrations,
migrations.MakeWorker(s.Jobs, s.Store()),
migrations.MakeScheduler(s.Jobs, s.Store()),
)
s.Jobs.RegisterJobType(
model.JobTypePlugins,
scheduler.MakeWorker(s.Jobs, New(ServerConnector(s.Channels()))),
scheduler.MakeScheduler(s.Jobs),
)
s.Jobs.RegisterJobType(
model.JobTypeExpiryNotify,
expirynotify.MakeWorker(s.Jobs, New(ServerConnector(s.Channels())).NotifySessionsExpired),
expirynotify.MakeScheduler(s.Jobs),
)
s.Jobs.RegisterJobType(
model.JobTypeProductNotices,
product_notices.MakeWorker(s.Jobs, New(ServerConnector(s.Channels()))),
product_notices.MakeScheduler(s.Jobs),
)
s.Jobs.RegisterJobType(
model.JobTypeImportProcess,
import_process.MakeWorker(s.Jobs, New(ServerConnector(s.Channels()))),
nil,
)
s.Jobs.RegisterJobType(
model.JobTypeImportDelete,
import_delete.MakeWorker(s.Jobs, New(ServerConnector(s.Channels())), s.Store()),
import_delete.MakeScheduler(s.Jobs),
)
s.Jobs.RegisterJobType(
model.JobTypeExportDelete,
export_delete.MakeWorker(s.Jobs, New(ServerConnector(s.Channels()))),
export_delete.MakeScheduler(s.Jobs),
)
s.Jobs.RegisterJobType(
model.JobTypeExportProcess,
export_process.MakeWorker(s.Jobs, New(ServerConnector(s.Channels()))),
nil,
)
s.Jobs.RegisterJobType(
model.JobTypeActiveUsers,
active_users.MakeWorker(s.Jobs, s.Store(), func() einterfaces.MetricsInterface { return s.GetMetrics() }),
active_users.MakeScheduler(s.Jobs),
)
s.Jobs.RegisterJobType(
model.JobTypeResendInvitationEmail,
resend_invitation_email.MakeWorker(s.Jobs, New(ServerConnector(s.Channels())), s.Store(), s.telemetryService),
nil,
)
s.Jobs.RegisterJobType(
model.JobTypeExtractContent,
extract_content.MakeWorker(s.Jobs, New(ServerConnector(s.Channels())), s.Store()),
nil,
)
s.Jobs.RegisterJobType(
model.JobTypeLastAccessiblePost,
last_accessible_post.MakeWorker(s.Jobs, s.License(), New(ServerConnector(s.Channels()))),
last_accessible_post.MakeScheduler(s.Jobs, s.License()),
)
s.Jobs.RegisterJobType(
model.JobTypeLastAccessibleFile,
last_accessible_file.MakeWorker(s.Jobs, s.License(), New(ServerConnector(s.Channels()))),
last_accessible_file.MakeScheduler(s.Jobs, s.License()),
)
s.Jobs.RegisterJobType(
model.JobTypeUpgradeNotifyAdmin,
notify_admin.MakeUpgradeNotifyWorker(s.Jobs, s.License(), New(ServerConnector(s.Channels()))),
notify_admin.MakeScheduler(s.Jobs, s.License(), model.JobTypeUpgradeNotifyAdmin),
)
s.Jobs.RegisterJobType(
model.JobTypeTrialNotifyAdmin,
notify_admin.MakeTrialNotifyWorker(s.Jobs, s.License(), New(ServerConnector(s.Channels()))),
notify_admin.MakeScheduler(s.Jobs, s.License(), model.JobTypeTrialNotifyAdmin),
)
s.Jobs.RegisterJobType(
model.JobTypeInstallPluginNotifyAdmin,
notify_admin.MakeInstallPluginNotifyWorker(s.Jobs, New(ServerConnector(s.Channels()))),
notify_admin.MakeInstallPluginScheduler(s.Jobs, s.License(), model.JobTypeInstallPluginNotifyAdmin),
)
s.Jobs.RegisterJobType(
model.JobTypeHostedPurchaseScreening,
hosted_purchase_screening.MakeWorker(s.Jobs, s.License(), s.Store().System()),
hosted_purchase_screening.MakeScheduler(s.Jobs, s.License()),
)
s.platform.Jobs = s.Jobs
}
func (s *Server) TelemetryId() string {
if s.telemetryService == nil {
return ""
}
return s.telemetryService.TelemetryID
}
func (s *Server) HTTPService() httpservice.HTTPService {
return s.httpService
}
// GetStore returns the server's Store. Exposing via a method
// allows interfaces to be created with subsets of server APIs.
func (s *Server) GetStore() store.Store {
return s.Store()
}
// GetRemoteClusterService returns the `RemoteClusterService` instantiated by the server.
// May be nil if the service is not enabled via license.
func (s *Server) GetRemoteClusterService() remotecluster.RemoteClusterServiceIFace {
s.serviceMux.RLock()
defer s.serviceMux.RUnlock()
return s.remoteClusterService
}
// GetSharedChannelSyncService returns the `SharedChannelSyncService` instantiated by the server.
// May be nil if the service is not enabled via license.
func (s *Server) GetSharedChannelSyncService() SharedChannelServiceIFace {
s.serviceMux.RLock()
defer s.serviceMux.RUnlock()
return s.sharedChannelService
}
// GetMetrics returns the server's Metrics interface. Exposing via a method
// allows interfaces to be created with subsets of server APIs.
func (s *Server) GetMetrics() einterfaces.MetricsInterface {
if s.platform == nil {
return nil
}
return s.platform.Metrics()
}
// SetRemoteClusterService sets the `RemoteClusterService` to be used by the server.
// For testing only.
func (s *Server) SetRemoteClusterService(remoteClusterService remotecluster.RemoteClusterServiceIFace) {
s.serviceMux.Lock()
defer s.serviceMux.Unlock()
s.remoteClusterService = remoteClusterService
}
// SetSharedChannelSyncService sets the `SharedChannelSyncService` to be used by the server.
// For testing only.
func (s *Server) SetSharedChannelSyncService(sharedChannelService SharedChannelServiceIFace) {
s.serviceMux.Lock()
defer s.serviceMux.Unlock()
s.sharedChannelService = sharedChannelService
s.platform.SetSharedChannelService(sharedChannelService)
}
func (s *Server) GetProfileImage(user *model.User) ([]byte, bool, *model.AppError) {
if *s.platform.Config().FileSettings.DriverName == "" {
img, appErr := s.GetDefaultProfileImage(user)
if appErr != nil {
return nil, false, appErr
}
return img, false, nil
}
path := "users/" + user.Id + "/profile.png"
data, err := s.ReadFile(path)
if err != nil {
img, appErr := s.GetDefaultProfileImage(user)
if appErr != nil {
return nil, false, appErr
}
if user.LastPictureUpdate == 0 {
if _, err := s.writeFile(bytes.NewReader(img), path); err != nil {
return nil, false, err
}
}
return img, true, nil
}
return data, false, nil
}
func (s *Server) GetDefaultProfileImage(user *model.User) ([]byte, *model.AppError) {
img, err := s.userService.GetDefaultProfileImage(user)
if err != nil {
switch {
case errors.Is(err, users.DefaultFontError):
return nil, model.NewAppError("GetDefaultProfileImage", "api.user.create_profile_image.default_font.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
case errors.Is(err, users.UserInitialsError):
return nil, model.NewAppError("GetDefaultProfileImage", "api.user.create_profile_image.initial.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
default:
return nil, model.NewAppError("GetDefaultProfileImage", "api.user.create_profile_image.encode.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return img, nil
}
func (s *Server) ReadFile(path string) ([]byte, *model.AppError) {
result, nErr := s.FileBackend().ReadFile(path)
if nErr != nil {
return nil, model.NewAppError("ReadFile", "api.file.read_file.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return result, nil
}
func withMut(mut *sync.Mutex, f func()) {
mut.Lock()
defer mut.Unlock()
f()
}
func cancelTask(mut *sync.Mutex, taskPointer **model.ScheduledTask) {
mut.Lock()
defer mut.Unlock()
if *taskPointer != nil {
(*taskPointer).Cancel()
*taskPointer = nil
}
}
func runDNDStatusExpireJob(a *App) {
if a.IsLeader() {
withMut(&a.ch.dndTaskMut, func() {
a.ch.dndTask = model.CreateRecurringTaskFromNextIntervalTime("Unset DND Statuses", a.UpdateDNDStatusOfUsers, 5*time.Minute)
})
}
a.ch.srv.AddClusterLeaderChangedListener(func() {
mlog.Info("Cluster leader changed. Determining if unset DNS status task should be running", mlog.Bool("isLeader", a.IsLeader()))
if a.IsLeader() {
withMut(&a.ch.dndTaskMut, func() {
a.ch.dndTask = model.CreateRecurringTaskFromNextIntervalTime("Unset DND Statuses", a.UpdateDNDStatusOfUsers, 5*time.Minute)
})
} else {
cancelTask(&a.ch.dndTaskMut, &a.ch.dndTask)
}
})
}
func runPostReminderJob(a *App) {
if a.IsLeader() {
withMut(&a.ch.postReminderMut, func() {
a.ch.postReminderTask = model.CreateRecurringTaskFromNextIntervalTime("Check Post reminders", a.CheckPostReminders, 5*time.Minute)
})
}
a.ch.srv.AddClusterLeaderChangedListener(func() {
mlog.Info("Cluster leader changed. Determining if post reminder task should be running", mlog.Bool("isLeader", a.IsLeader()))
if a.IsLeader() {
withMut(&a.ch.postReminderMut, func() {
a.ch.postReminderTask = model.CreateRecurringTaskFromNextIntervalTime("Check Post reminders", a.CheckPostReminders, 5*time.Minute)
})
} else {
cancelTask(&a.ch.postReminderMut, &a.ch.postReminderTask)
}
})
}
func (a *App) GetAppliedSchemaMigrations() ([]model.AppliedMigration, *model.AppError) {
table, err := a.Srv().Store().GetAppliedMigrations()
if err != nil {
return nil, model.NewAppError("GetDBSchemaTable", "api.file.read_file.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return table, nil
}
// Expose platform service from server, this should be replaced with server itself in time.
func (s *Server) Platform() *platform.PlatformService {
return s.platform
}
func (s *Server) Log() *mlog.Logger {
return s.platform.Logger()
}
func (s *Server) NotificationsLog() *mlog.Logger {
return s.platform.NotificationsLogger()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"errors"
"math"
"net/http"
"os"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
"github.com/mattermost/mattermost-server/v6/server/channels/app/users"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *App) CreateSession(session *model.Session) (*model.Session, *model.AppError) {
session, err := a.ch.srv.platform.CreateSession(session)
if err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateSession", "app.session.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("CreateSession", "app.session.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return session, nil
}
func (a *App) GetCloudSession(token string) (*model.Session, *model.AppError) {
apiKey := os.Getenv("MM_CLOUD_API_KEY")
if apiKey != "" && apiKey == token {
// Need a bare-bones session object for later checks
session := &model.Session{
Token: token,
IsOAuth: false,
}
session.AddProp(model.SessionPropType, model.SessionTypeCloudKey)
return session, nil
}
return nil, model.NewAppError("GetCloudSession", "api.context.invalid_token.error", map[string]any{"Token": token, "Error": ""}, "The provided token is invalid", http.StatusUnauthorized)
}
func (a *App) GetRemoteClusterSession(token string, remoteId string) (*model.Session, *model.AppError) {
rc, appErr := a.GetRemoteCluster(remoteId)
if appErr == nil && rc.Token == token {
// Need a bare-bones session object for later checks
session := &model.Session{
Token: token,
IsOAuth: false,
}
session.AddProp(model.SessionPropType, model.SessionTypeRemoteclusterToken)
return session, nil
}
return nil, model.NewAppError("GetRemoteClusterSession", "api.context.invalid_token.error", map[string]any{"Token": token, "Error": ""}, "The provided token is invalid", http.StatusUnauthorized)
}
func (a *App) GetSession(token string) (*model.Session, *model.AppError) {
var session *model.Session
// We intentionally skip the error check here, we only want to check if the token is valid.
// If we don't have the session we are going to create one with the token eventually.
if session, _ = a.ch.srv.platform.GetSession(token); session != nil {
if session.Token != token {
return nil, model.NewAppError("GetSession", "api.context.invalid_token.error", map[string]any{"Token": token, "Error": ""}, "session token is different from the one in DB", http.StatusUnauthorized)
}
if !session.IsExpired() {
a.ch.srv.platform.AddSessionToCache(session)
}
}
var appErr *model.AppError
if session == nil || session.Id == "" {
session, appErr = a.createSessionForUserAccessToken(token)
if appErr != nil {
detailedError := ""
statusCode := http.StatusUnauthorized
if appErr.Id != "app.user_access_token.invalid_or_missing" {
detailedError = appErr.Error()
statusCode = appErr.StatusCode
} else {
mlog.Warn("Error while creating session for user access token", mlog.Err(appErr))
}
return nil, model.NewAppError("GetSession", "api.context.invalid_token.error", map[string]any{"Token": token, "Error": detailedError}, "", statusCode)
}
}
if session.Id == "" || session.IsExpired() {
return nil, model.NewAppError("GetSession", "api.context.invalid_token.error", map[string]any{"Token": token, "Error": ""}, "session is either nil or expired", http.StatusUnauthorized)
}
if *a.Config().ServiceSettings.SessionIdleTimeoutInMinutes > 0 &&
!session.IsOAuth && !session.IsMobileApp() &&
session.Props[model.SessionPropType] != model.SessionTypeUserAccessToken &&
!*a.Config().ServiceSettings.ExtendSessionLengthWithActivity {
timeout := int64(*a.Config().ServiceSettings.SessionIdleTimeoutInMinutes) * 1000 * 60
if (model.GetMillis() - session.LastActivityAt) > timeout {
// Revoking the session is an asynchronous task anyways since we are not checking
// for the return value of the call before returning the error.
// So moving this to a goroutine has 2 advantages:
// 1. We are treating this as a proper asynchronous task.
// 2. This also fixes a race condition in the web hub, where GetSession
// gets called from (*WebConn).isMemberOfTeam and revoking a session involves
// clearing the webconn cache, which needs the hub again.
a.Srv().Go(func() {
err := a.RevokeSessionById(session.Id)
if err != nil {
mlog.Warn("Error while revoking session", mlog.Err(err))
}
})
return nil, model.NewAppError("GetSession", "api.context.invalid_token.error", map[string]any{"Token": token, "Error": ""}, "idle timeout", http.StatusUnauthorized)
}
}
return session, nil
}
func (a *App) GetSessions(userID string) ([]*model.Session, *model.AppError) {
sessions, err := a.ch.srv.platform.GetSessions(userID)
if err != nil {
return nil, model.NewAppError("GetSessions", "app.session.get_sessions.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return sessions, nil
}
func (a *App) RevokeAllSessions(userID string) *model.AppError {
if err := a.ch.srv.platform.RevokeAllSessions(userID); err != nil {
switch {
case errors.Is(err, platform.GetSessionError):
return model.NewAppError("RevokeAllSessions", "app.session.get_sessions.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
case errors.Is(err, platform.DeleteSessionError):
return model.NewAppError("RevokeAllSessions", "app.session.remove.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
default:
return model.NewAppError("RevokeAllSessions", "app.session.remove.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return nil
}
func (a *App) AddSessionToCache(session *model.Session) {
a.ch.srv.platform.AddSessionToCache(session)
}
// RevokeSessionsFromAllUsers will go through all the sessions active
// in the server and revoke them
func (a *App) RevokeSessionsFromAllUsers() *model.AppError {
if err := a.ch.srv.platform.RevokeSessionsFromAllUsers(); err != nil {
switch {
case errors.Is(err, users.DeleteAllAccessDataError):
return model.NewAppError("RevokeSessionsFromAllUsers", "app.oauth.remove_access_data.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
default:
return model.NewAppError("RevokeSessionsFromAllUsers", "app.session.remove_all_sessions_for_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return nil
}
func (a *App) ReturnSessionToPool(session *model.Session) {
a.ch.srv.platform.ReturnSessionToPool(session)
}
func (a *App) ClearSessionCacheForUser(userID string) {
a.ch.srv.platform.ClearUserSessionCache(userID)
}
func (a *App) ClearSessionCacheForAllUsers() {
a.ch.srv.platform.ClearAllUsersSessionCache()
}
func (a *App) ClearSessionCacheForUserSkipClusterSend(userID string) {
a.Srv().Platform().ClearSessionCacheForUserSkipClusterSend(userID)
}
func (a *App) ClearSessionCacheForAllUsersSkipClusterSend() {
a.Srv().Platform().ClearSessionCacheForAllUsersSkipClusterSend()
}
func (a *App) RevokeSessionsForDeviceId(userID string, deviceID string, currentSessionId string) *model.AppError {
if err := a.ch.srv.platform.RevokeSessionsForDeviceId(userID, deviceID, currentSessionId); err != nil {
return model.NewAppError("RevokeSessionsForDeviceId", "app.session.get_sessions.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) GetSessionById(sessionID string) (*model.Session, *model.AppError) {
session, err := a.ch.srv.platform.GetSessionByID(sessionID)
if err != nil {
return nil, model.NewAppError("GetSessionById", "app.session.get.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
return session, nil
}
func (a *App) RevokeSessionById(sessionID string) *model.AppError {
session, err := a.GetSessionById(sessionID)
if err != nil {
return model.NewAppError("RevokeSessionById", "app.session.get.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
return a.RevokeSession(session)
}
func (a *App) RevokeSession(session *model.Session) *model.AppError {
if err := a.ch.srv.platform.RevokeSession(session); err != nil {
switch {
case errors.Is(err, platform.DeleteSessionError):
return model.NewAppError("RevokeSession", "app.session.remove.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
default:
return model.NewAppError("RevokeSession", "app.session.remove.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return nil
}
func (a *App) AttachDeviceId(sessionID string, deviceID string, expiresAt int64) *model.AppError {
_, err := a.Srv().Store().Session().UpdateDeviceId(sessionID, deviceID, expiresAt)
if err != nil {
return model.NewAppError("AttachDeviceId", "app.session.update_device_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
// ExtendSessionExpiryIfNeeded extends Session.ExpiresAt based on session lengths in config.
// A new ExpiresAt is only written if enough time has elapsed since last update.
// Returns true only if the session was extended.
func (a *App) ExtendSessionExpiryIfNeeded(session *model.Session) bool {
if !*a.Config().ServiceSettings.ExtendSessionLengthWithActivity {
return false
}
if session == nil || session.IsExpired() {
return false
}
sessionLength := a.GetSessionLengthInMillis(session)
// Only extend the expiry if the lessor of 1% or 1 day has elapsed within the
// current session duration.
threshold := int64(math.Min(float64(sessionLength)*0.01, float64(model.DayInMilliseconds)))
// Minimum session length is 1 day as of this writing, therefore a minimum ~14 minutes threshold.
// However we'll add a sanity check here in case that changes. Minimum 5 minute threshold,
// meaning we won't write a new expiry more than every 5 minutes.
if threshold < 5*60*1000 {
threshold = 5 * 60 * 1000
}
now := model.GetMillis()
elapsed := now - (session.ExpiresAt - sessionLength)
if elapsed < threshold {
return false
}
auditRec := a.MakeAuditRecord("extendSessionExpiry", audit.Fail)
defer a.LogAuditRec(auditRec, nil)
auditRec.AddEventPriorState(session)
newExpiry := now + sessionLength
if err := a.ch.srv.platform.ExtendSessionExpiry(session, newExpiry); err != nil {
mlog.Error("Failed to update ExpiresAt", mlog.String("user_id", session.UserId), mlog.String("session_id", session.Id), mlog.Err(err))
auditRec.AddMeta("err", err.Error())
return false
}
mlog.Debug("Session extended", mlog.String("user_id", session.UserId), mlog.String("session_id", session.Id),
mlog.Int64("newExpiry", newExpiry), mlog.Int64("session_length", sessionLength))
auditRec.Success()
auditRec.AddEventResultState(session)
return true
}
// GetSessionLengthInMillis returns the session length, in milliseconds,
// based on the type of session (Mobile, SSO, Web/LDAP).
func (a *App) GetSessionLengthInMillis(session *model.Session) int64 {
if session == nil {
return 0
}
var hours int
if session.IsMobileApp() {
hours = *a.Config().ServiceSettings.SessionLengthMobileInHours
} else if session.IsSSOLogin() {
hours = *a.Config().ServiceSettings.SessionLengthSSOInHours
} else {
hours = *a.Config().ServiceSettings.SessionLengthWebInHours
}
return int64(hours * 60 * 60 * 1000)
}
// SetSessionExpireInHours sets the session's expiry the specified number of hours
// relative to either the session creation date or the current time, depending
// on the `ExtendSessionOnActivity` config setting.
func (a *App) SetSessionExpireInHours(session *model.Session, hours int) {
a.ch.srv.platform.SetSessionExpireInHours(session, hours)
}
func (a *App) CreateUserAccessToken(token *model.UserAccessToken) (*model.UserAccessToken, *model.AppError) {
user, nErr := a.ch.srv.userService.GetUser(token.UserId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("CreateUserAccessToken", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("CreateUserAccessToken", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if !*a.Config().ServiceSettings.EnableUserAccessTokens && !user.IsBot {
return nil, model.NewAppError("CreateUserAccessToken", "app.user_access_token.disabled", nil, "", http.StatusNotImplemented)
}
token.Token = model.NewId()
token, nErr = a.Srv().Store().UserAccessToken().Save(token)
if nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CreateUserAccessToken", "app.user_access_token.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
// Don't send emails to bot users.
if !user.IsBot {
if err := a.Srv().EmailService.SendUserAccessTokenAddedEmail(user.Email, user.Locale, a.GetSiteURL()); err != nil {
a.Log().Error("Unable to send user access token added email", mlog.Err(err), mlog.String("user_id", user.Id))
}
}
return token, nil
}
func (a *App) createSessionForUserAccessToken(tokenString string) (*model.Session, *model.AppError) {
token, nErr := a.Srv().Store().UserAccessToken().GetByToken(tokenString)
if nErr != nil {
return nil, model.NewAppError("createSessionForUserAccessToken", "app.user_access_token.invalid_or_missing", nil, "", http.StatusUnauthorized).Wrap(nErr)
}
if !token.IsActive {
return nil, model.NewAppError("createSessionForUserAccessToken", "app.user_access_token.invalid_or_missing", nil, "inactive_token", http.StatusUnauthorized)
}
user, nErr := a.Srv().Store().User().Get(context.Background(), token.UserId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("createSessionForUserAccessToken", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("createSessionForUserAccessToken", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if !*a.Config().ServiceSettings.EnableUserAccessTokens && !user.IsBot {
return nil, model.NewAppError("createSessionForUserAccessToken", "app.user_access_token.invalid_or_missing", nil, "EnableUserAccessTokens=false", http.StatusUnauthorized)
}
if user.DeleteAt != 0 {
return nil, model.NewAppError("createSessionForUserAccessToken", "app.user_access_token.invalid_or_missing", nil, "inactive_user_id="+user.Id, http.StatusUnauthorized)
}
session := &model.Session{
Token: token.Token,
UserId: user.Id,
Roles: user.GetRawRoles(),
IsOAuth: false,
}
session.AddProp(model.SessionPropUserAccessTokenId, token.Id)
session.AddProp(model.SessionPropType, model.SessionTypeUserAccessToken)
if user.IsBot {
session.AddProp(model.SessionPropIsBot, model.SessionPropIsBotValue)
}
if user.IsGuest() {
session.AddProp(model.SessionPropIsGuest, "true")
} else {
session.AddProp(model.SessionPropIsGuest, "false")
}
a.ch.srv.platform.SetSessionExpireInHours(session, model.SessionUserAccessTokenExpiryHours)
session, nErr = a.Srv().Store().Session().Save(session)
if nErr != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(nErr, &invErr):
return nil, model.NewAppError("CreateSession", "app.session.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
default:
return nil, model.NewAppError("CreateSession", "app.session.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
a.ch.srv.platform.AddSessionToCache(session)
return session, nil
}
func (a *App) RevokeUserAccessToken(token *model.UserAccessToken) *model.AppError {
var session *model.Session
session, _ = a.ch.srv.platform.GetSessionContext(context.Background(), token.Token)
if err := a.Srv().Store().UserAccessToken().Delete(token.Id); err != nil {
return model.NewAppError("RevokeUserAccessToken", "app.user_access_token.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if session == nil {
return nil
}
return a.RevokeSession(session)
}
func (a *App) DisableUserAccessToken(token *model.UserAccessToken) *model.AppError {
var session *model.Session
session, _ = a.ch.srv.platform.GetSessionContext(context.Background(), token.Token)
if err := a.Srv().Store().UserAccessToken().UpdateTokenDisable(token.Id); err != nil {
return model.NewAppError("DisableUserAccessToken", "app.user_access_token.update_token_disable.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if session == nil {
return nil
}
return a.RevokeSession(session)
}
func (a *App) EnableUserAccessToken(token *model.UserAccessToken) *model.AppError {
var session *model.Session
session, _ = a.ch.srv.platform.GetSessionContext(context.Background(), token.Token)
err := a.Srv().Store().UserAccessToken().UpdateTokenEnable(token.Id)
if err != nil {
return model.NewAppError("EnableUserAccessToken", "app.user_access_token.update_token_enable.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if session == nil {
return nil
}
return nil
}
func (a *App) GetUserAccessTokens(page, perPage int) ([]*model.UserAccessToken, *model.AppError) {
tokens, err := a.Srv().Store().UserAccessToken().GetAll(page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetUserAccessTokens", "app.user_access_token.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, token := range tokens {
token.Token = ""
}
return tokens, nil
}
func (a *App) GetUserAccessTokensForUser(userID string, page, perPage int) ([]*model.UserAccessToken, *model.AppError) {
tokens, err := a.Srv().Store().UserAccessToken().GetByUser(userID, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetUserAccessTokensForUser", "app.user_access_token.get_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, token := range tokens {
token.Token = ""
}
return tokens, nil
}
func (a *App) GetUserAccessToken(tokenID string, sanitize bool) (*model.UserAccessToken, *model.AppError) {
token, err := a.Srv().Store().UserAccessToken().Get(tokenID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetUserAccessToken", "app.user_access_token.get_by_user.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetUserAccessToken", "app.user_access_token.get_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if sanitize {
token.Token = ""
}
return token, nil
}
func (a *App) SearchUserAccessTokens(term string) ([]*model.UserAccessToken, *model.AppError) {
tokens, err := a.Srv().Store().UserAccessToken().Search(term)
if err != nil {
return nil, model.NewAppError("SearchUserAccessTokens", "app.user_access_token.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, token := range tokens {
token.Token = ""
}
return tokens, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"fmt"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func (a *App) checkChannelNotShared(c request.CTX, channelId string) error {
// check that channel exists.
if _, err := a.GetChannel(c, channelId); err != nil {
return fmt.Errorf("cannot share this channel: %w", err)
}
// Check channel is not already shared.
if _, err := a.GetSharedChannel(channelId); err == nil {
var errNotFound *store.ErrNotFound
if errors.As(err, &errNotFound) {
return fmt.Errorf("channel is already shared: %w", err)
}
return fmt.Errorf("cannot find channel: %w", err)
}
return nil
}
func (a *App) checkChannelIsShared(channelId string) error {
if _, err := a.GetSharedChannel(channelId); err != nil {
var errNotFound *store.ErrNotFound
if errors.As(err, &errNotFound) {
return fmt.Errorf("channel is not shared: %w", err)
}
return fmt.Errorf("cannot find channel: %w", err)
}
return nil
}
func (a *App) CheckCanInviteToSharedChannel(channelId string) error {
sc, err := a.GetSharedChannel(channelId)
if err != nil {
var errNotFound *store.ErrNotFound
if errors.As(err, &errNotFound) {
return fmt.Errorf("channel is not shared: %w", err)
}
return fmt.Errorf("cannot find channel: %w", err)
}
if !sc.Home {
return errors.New("channel is homed on a remote cluster")
}
return nil
}
// SharedChannels
func (a *App) SaveSharedChannel(c request.CTX, sc *model.SharedChannel) (*model.SharedChannel, error) {
if err := a.checkChannelNotShared(c, sc.ChannelId); err != nil {
return nil, err
}
return a.Srv().Store().SharedChannel().Save(sc)
}
func (a *App) GetSharedChannel(channelID string) (*model.SharedChannel, error) {
return a.Srv().Store().SharedChannel().Get(channelID)
}
func (a *App) HasSharedChannel(channelID string) (bool, error) {
return a.Srv().Store().SharedChannel().HasChannel(channelID)
}
func (a *App) GetSharedChannels(page int, perPage int, opts model.SharedChannelFilterOpts) ([]*model.SharedChannel, *model.AppError) {
channels, err := a.Srv().Store().SharedChannel().GetAll(page*perPage, perPage, opts)
if err != nil {
return nil, model.NewAppError("GetSharedChannels", "app.channel.get_channels.not_found.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channels, nil
}
func (a *App) GetSharedChannelsCount(opts model.SharedChannelFilterOpts) (int64, error) {
return a.Srv().Store().SharedChannel().GetAllCount(opts)
}
func (a *App) UpdateSharedChannel(sc *model.SharedChannel) (*model.SharedChannel, error) {
return a.Srv().Store().SharedChannel().Update(sc)
}
func (a *App) DeleteSharedChannel(channelID string) (bool, error) {
return a.Srv().Store().SharedChannel().Delete(channelID)
}
// SharedChannelRemotes
func (a *App) SaveSharedChannelRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
if err := a.checkChannelIsShared(remote.ChannelId); err != nil {
return nil, err
}
return a.Srv().Store().SharedChannel().SaveRemote(remote)
}
func (a *App) GetSharedChannelRemote(id string) (*model.SharedChannelRemote, error) {
return a.Srv().Store().SharedChannel().GetRemote(id)
}
func (a *App) GetSharedChannelRemoteByIds(channelID string, remoteID string) (*model.SharedChannelRemote, error) {
return a.Srv().Store().SharedChannel().GetRemoteByIds(channelID, remoteID)
}
func (a *App) GetSharedChannelRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) {
return a.Srv().Store().SharedChannel().GetRemotes(opts)
}
// HasRemote returns whether a given channelID is present in the channel remotes or not.
func (a *App) HasRemote(channelID string, remoteID string) (bool, error) {
return a.Srv().Store().SharedChannel().HasRemote(channelID, remoteID)
}
func (a *App) GetRemoteClusterForUser(remoteID string, userID string) (*model.RemoteCluster, *model.AppError) {
rc, err := a.Srv().Store().SharedChannel().GetRemoteForUser(remoteID, userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetRemoteClusterForUser", "api.context.remote_id_invalid.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetRemoteClusterForUser", "api.context.remote_id_invalid.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return rc, nil
}
func (a *App) UpdateSharedChannelRemoteCursor(id string, cursor model.GetPostsSinceForSyncCursor) error {
return a.Srv().Store().SharedChannel().UpdateRemoteCursor(id, cursor)
}
func (a *App) DeleteSharedChannelRemote(id string) (bool, error) {
return a.Srv().Store().SharedChannel().DeleteRemote(id)
}
func (a *App) GetSharedChannelRemotesStatus(channelID string) ([]*model.SharedChannelRemoteStatus, error) {
if err := a.checkChannelIsShared(channelID); err != nil {
return nil, err
}
return a.Srv().Store().SharedChannel().GetRemotesStatus(channelID)
}
// SharedChannelUsers
func (a *App) NotifySharedChannelUserUpdate(user *model.User) {
a.sendUpdatedUserEvent(*user)
}
// onUserProfileChange is called when a user's profile has changed
// (username, email, profile image, ...)
func (a *App) onUserProfileChange(userID string) {
syncService := a.Srv().GetSharedChannelSyncService()
if syncService == nil || !syncService.Active() {
return
}
syncService.NotifyUserProfileChanged(userID)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
// TODO: platform: remove this and use from platform package
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/sharedchannel"
)
// SharedChannelServiceIFace is the interface to the shared channel service
type SharedChannelServiceIFace interface {
Shutdown() error
Start() error
NotifyChannelChanged(channelId string)
NotifyUserProfileChanged(userID string)
SendChannelInvite(channel *model.Channel, userId string, rc *model.RemoteCluster, options ...sharedchannel.InviteOption) error
Active() bool
}
type MockOptionSharedChannelService func(service *mockSharedChannelService)
func MockOptionSharedChannelServiceWithActive(active bool) MockOptionSharedChannelService {
return func(mrcs *mockSharedChannelService) {
mrcs.active = active
}
}
func NewMockSharedChannelService(service SharedChannelServiceIFace, options ...MockOptionSharedChannelService) *mockSharedChannelService {
mrcs := &mockSharedChannelService{service, true, []string{}, []string{}, 0}
for _, option := range options {
option(mrcs)
}
return mrcs
}
type mockSharedChannelService struct {
SharedChannelServiceIFace
active bool
channelNotifications []string
userProfileNotifications []string
numInvitations int
}
func (mrcs *mockSharedChannelService) NotifyChannelChanged(channelId string) {
mrcs.channelNotifications = append(mrcs.channelNotifications, channelId)
}
func (mrcs *mockSharedChannelService) NotifyUserProfileChanged(userId string) {
mrcs.userProfileNotifications = append(mrcs.userProfileNotifications, userId)
}
func (mrcs *mockSharedChannelService) Shutdown() error {
return nil
}
func (mrcs *mockSharedChannelService) Start() error {
return nil
}
func (mrcs *mockSharedChannelService) Active() bool {
return mrcs.active
}
func (mrcs *mockSharedChannelService) SendChannelInvite(channel *model.Channel, userId string, rc *model.RemoteCluster, options ...sharedchannel.InviteOption) error {
mrcs.numInvitations += 1
return nil
}
func (mrcs *mockSharedChannelService) NumInvitations() int {
return mrcs.numInvitations
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"context"
"fmt"
"image"
"mime/multipart"
"regexp"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/slackimport"
)
func (a *App) SlackImport(c *request.Context, fileData multipart.File, fileSize int64, teamID string) (*model.AppError, *bytes.Buffer) {
actions := slackimport.Actions{
UpdateActive: func(user *model.User, active bool) (*model.User, *model.AppError) {
return a.UpdateActive(c, user, active)
},
AddUserToChannel: a.AddUserToChannel,
JoinUserToTeam: func(team *model.Team, user *model.User, userRequestorId string) (*model.TeamMember, *model.AppError) {
return a.JoinUserToTeam(c, team, user, userRequestorId)
},
CreateDirectChannel: a.createDirectChannel,
CreateGroupChannel: a.createGroupChannel,
CreateChannel: func(channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) {
return a.CreateChannel(c, channel, addMember)
},
DoUploadFile: func(now time.Time, rawTeamId string, rawChannelId string, rawUserId string, rawFilename string, data []byte) (*model.FileInfo, *model.AppError) {
return a.DoUploadFile(c, now, rawTeamId, rawChannelId, rawUserId, rawFilename, data)
},
GenerateThumbnailImage: a.generateThumbnailImage,
GeneratePreviewImage: a.generatePreviewImage,
InvalidateAllCaches: func() { a.ch.srv.InvalidateAllCaches() },
MaxPostSize: func() int { return a.ch.srv.platform.MaxPostSize() },
PrepareImage: func(fileData []byte) (image.Image, string, func(), error) {
img, imgType, release, err := prepareImage(a.ch.imgDecoder, bytes.NewReader(fileData))
if err != nil {
return nil, "", nil, err
}
return img, imgType, release, err
},
}
importer := slackimport.New(a.Srv().Store(), actions, a.Config())
return importer.SlackImport(c, fileData, fileSize, teamID)
}
func (a *App) ProcessSlackText(text string) string {
text = expandAnnouncement(text)
text = replaceUserIds(a.Srv().Store().User(), text)
return text
}
// Expand announcements in incoming webhooks from Slack. Those announcements
// can be found in the text attribute, or in the pretext, text, title and value
// attributes of the attachment structure. The Slack attachment structure is
// documented here: https://api.slack.com/docs/attachments
func (a *App) ProcessSlackAttachments(attachments []*model.SlackAttachment) []*model.SlackAttachment {
var nonNilAttachments = model.StringifySlackFieldValue(attachments)
for _, attachment := range attachments {
attachment.Pretext = a.ProcessSlackText(attachment.Pretext)
attachment.Text = a.ProcessSlackText(attachment.Text)
attachment.Title = a.ProcessSlackText(attachment.Title)
for _, field := range attachment.Fields {
if field != nil && field.Value != nil {
// Ensure the value is set to a string if it is set
field.Value = a.ProcessSlackText(fmt.Sprintf("%v", field.Value))
}
}
}
return nonNilAttachments
}
// To mention @channel or @here via a webhook in Slack, the message should contain
// <!channel> or <!here>, as explained at the bottom of this article:
// https://get.slack.help/hc/en-us/articles/202009646-Making-announcements
func expandAnnouncement(text string) string {
a1 := [3]string{"<!channel>", "<!here>", "<!all>"}
a2 := [3]string{"@channel", "@here", "@all"}
for i, a := range a1 {
text = strings.Replace(text, a, a2[i], -1)
}
return text
}
// Replaces user IDs mentioned like this <@userID> to a normal username (eg. @bob)
// This is required so that Mattermost maintains Slack compatibility
// Refer to: https://api.slack.com/changelog/2017-09-the-one-about-usernames
func replaceUserIds(userStore store.UserStore, text string) string {
rgx, err := regexp.Compile("<@([a-zA-Z0-9]+)>")
if err == nil {
userIDs := make([]string, 0)
matches := rgx.FindAllStringSubmatch(text, -1)
for _, match := range matches {
userIDs = append(userIDs, match[1])
}
if users, err := userStore.GetProfileByIds(context.Background(), userIDs, nil, true); err == nil {
for _, user := range users {
text = strings.Replace(text, "<@"+user.Id+">", "@"+user.Username, -1)
}
}
}
return text
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
type AutoChannelCreator struct {
a *app.App
userID string
team *model.Team
Fuzzy bool
DisplayNameLen utils.Range
DisplayNameCharset string
NameLen utils.Range
NameCharset string
ChannelType model.ChannelType
CreateTime int64
}
func NewAutoChannelCreator(a *app.App, team *model.Team, userID string) *AutoChannelCreator {
return &AutoChannelCreator{
a: a,
team: team,
userID: userID,
Fuzzy: false,
DisplayNameLen: ChannelDisplayNameLen,
DisplayNameCharset: utils.ALPHANUMERIC,
NameLen: ChannelNameLen,
NameCharset: utils.LOWERCASE,
ChannelType: ChannelType,
CreateTime: 0,
}
}
func (cfg *AutoChannelCreator) createRandomChannel(c request.CTX) (*model.Channel, error) {
var displayName string
if cfg.Fuzzy {
displayName = utils.FuzzName()
} else {
displayName = utils.RandomName(cfg.NameLen, cfg.NameCharset)
}
name := utils.RandomName(cfg.NameLen, cfg.NameCharset)
channel := &model.Channel{
TeamId: cfg.team.Id,
DisplayName: displayName,
Name: name,
Type: cfg.ChannelType,
CreatorId: cfg.userID,
CreateAt: cfg.CreateTime,
}
channel, err := cfg.a.CreateChannel(c, channel, true)
if err != nil {
return nil, err
}
return channel, nil
}
func (cfg *AutoChannelCreator) CreateTestChannels(c request.CTX, num utils.Range) ([]*model.Channel, error) {
numChannels := utils.RandIntFromRange(num)
channels := make([]*model.Channel, numChannels)
for i := 0; i < numChannels; i++ {
var err error
channels[i], err = cfg.createRandomChannel(c)
if err != nil {
return nil, err
}
}
return channels, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"math/rand"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
type TestEnvironment struct {
Teams []*model.Team
Environments []TeamEnvironment
}
func CreateTestEnvironmentWithTeams(a *app.App, c request.CTX, client *model.Client4, rangeTeams utils.Range, rangeChannels utils.Range, rangeUsers utils.Range, rangePosts utils.Range, fuzzy bool) (TestEnvironment, error) {
rand.Seed(time.Now().UTC().UnixNano())
teamCreator := NewAutoTeamCreator(client)
teamCreator.Fuzzy = fuzzy
teams, err := teamCreator.CreateTestTeams(rangeTeams)
if err != nil {
return TestEnvironment{}, err
}
environment := TestEnvironment{teams, make([]TeamEnvironment, len(teams))}
for i, team := range teams {
userCreator := NewAutoUserCreator(a, client, team)
userCreator.Fuzzy = fuzzy
randomUser, err := userCreator.createRandomUser(c)
if err != nil {
return TestEnvironment{}, err
}
client.LoginById(randomUser.Id, UserPassword)
teamEnvironment, err := CreateTestEnvironmentInTeam(a, c, client, team, rangeChannels, rangeUsers, rangePosts, fuzzy)
if err != nil {
return TestEnvironment{}, err
}
environment.Environments[i] = teamEnvironment
}
return environment, nil
}
func CreateTestEnvironmentInTeam(a *app.App, c request.CTX, client *model.Client4, team *model.Team, rangeChannels utils.Range, rangeUsers utils.Range, rangePosts utils.Range, fuzzy bool) (TeamEnvironment, error) {
rand.Seed(time.Now().UTC().UnixNano())
// We need to create at least one user
if rangeUsers.Begin <= 0 {
rangeUsers.Begin = 1
}
userCreator := NewAutoUserCreator(a, client, team)
userCreator.Fuzzy = fuzzy
users, err := userCreator.CreateTestUsers(c, rangeUsers)
if err != nil {
return TeamEnvironment{}, nil
}
usernames := make([]string, len(users))
for i, user := range users {
usernames[i] = user.Username
}
channelCreator := NewAutoChannelCreator(a, team, users[0].Id)
channelCreator.Fuzzy = fuzzy
channels, err := channelCreator.CreateTestChannels(c, rangeChannels)
if err != nil {
return TeamEnvironment{}, nil
}
// Have every user join every channel
for _, user := range users {
for _, channel := range channels {
_, _, err := client.LoginById(user.Id, UserPassword)
if err != nil {
return TeamEnvironment{}, err
}
_, _, err = client.AddChannelMember(channel.Id, user.Id)
if err != nil {
return TeamEnvironment{}, err
}
}
}
numPosts := utils.RandIntFromRange(rangePosts)
numImages := utils.RandIntFromRange(rangePosts) / 4
for j := 0; j < numPosts; j++ {
user := users[utils.RandIntFromRange(utils.Range{Begin: 0, End: len(users) - 1})]
_, _, err := client.LoginById(user.Id, UserPassword)
if err != nil {
return TeamEnvironment{}, err
}
for i, channel := range channels {
postCreator := NewAutoPostCreator(a, channel.Id, user.Id)
postCreator.HasImage = i < numImages
postCreator.Users = usernames
postCreator.Fuzzy = fuzzy
_, err := postCreator.CreateRandomPost(c)
if err != nil {
return TeamEnvironment{}, err
}
}
}
return TeamEnvironment{users, channels}, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"bytes"
"io"
"os"
"path/filepath"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
)
type AutoPostCreator struct {
a *app.App
channelid string
userid string
Fuzzy bool
TextLength utils.Range
HasImage bool
ImageFilenames []string
Users []string
UsersToPostFrom []string
Mentions utils.Range
Tags utils.Range
CreateTime int64
postsCreated int
}
// Automatic poster used for testing
func NewAutoPostCreator(a *app.App, channelid, userid string) *AutoPostCreator {
return &AutoPostCreator{
a: a,
channelid: channelid,
userid: userid,
Fuzzy: false,
TextLength: utils.Range{Begin: 100, End: 200},
HasImage: false,
ImageFilenames: TestImageFileNames,
Users: []string{},
UsersToPostFrom: []string{},
Mentions: utils.Range{Begin: 0, End: 5},
Tags: utils.Range{Begin: 0, End: 7},
CreateTime: 0,
postsCreated: 0,
}
}
func (cfg *AutoPostCreator) UploadTestFile(c request.CTX) ([]string, error) {
filename := cfg.ImageFilenames[utils.RandIntFromRange(utils.Range{Begin: 0, End: len(cfg.ImageFilenames) - 1})]
path, _ := fileutils.FindDir("tests")
file, err := os.Open(filepath.Join(path, filename))
if err != nil {
return nil, err
}
defer file.Close()
data := &bytes.Buffer{}
_, err = io.Copy(data, file)
if err != nil {
return nil, err
}
fileResp, err2 := cfg.a.UploadFile(c, data.Bytes(), cfg.channelid, filename)
if err2 != nil {
return nil, err2
}
return []string{fileResp.Id}, nil
}
func (cfg *AutoPostCreator) CreateRandomPost(c request.CTX) (*model.Post, error) {
return cfg.CreateRandomPostNested(c, "")
}
func (cfg *AutoPostCreator) CreateRandomPostNested(c request.CTX, rootId string) (*model.Post, error) {
var fileIDs []string
if cfg.HasImage {
var err error
fileIDs, err = cfg.UploadTestFile(c)
if err != nil {
return nil, err
}
}
var postText string
if cfg.Fuzzy {
postText = utils.FuzzPost()
} else {
postText = utils.RandomText(cfg.TextLength, cfg.Tags, cfg.Mentions, cfg.Users)
}
post := &model.Post{
ChannelId: cfg.channelid,
UserId: cfg.userid,
RootId: rootId,
Message: postText,
FileIds: fileIDs,
}
if cfg.CreateTime != 0 {
// Creating posts with the exact same timestamp results in some posts being skipped
// when they are retrieved by the API based on timestamp.
post.CreateAt = cfg.CreateTime + int64(cfg.postsCreated)
}
if len(cfg.UsersToPostFrom) != 0 {
i := utils.RandIntFromRange(utils.Range{Begin: 0, End: len(cfg.UsersToPostFrom)})
if i < len(cfg.UsersToPostFrom) {
post.UserId = cfg.UsersToPostFrom[i]
}
}
rpost, err := cfg.a.CreatePostMissingChannel(c, post, true, true)
if err != nil {
return nil, err
}
cfg.postsCreated += 1
return rpost, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
type TeamEnvironment struct {
Users []*model.User
Channels []*model.Channel
}
type AutoTeamCreator struct {
client *model.Client4
Fuzzy bool
NameLength utils.Range
NameCharset string
DomainLength utils.Range
DomainCharset string
EmailLength utils.Range
EmailCharset string
}
func NewAutoTeamCreator(client *model.Client4) *AutoTeamCreator {
return &AutoTeamCreator{
client: client,
Fuzzy: false,
NameLength: TeamNameLen,
NameCharset: utils.LOWERCASE,
DomainLength: TeamDomainNameLen,
DomainCharset: utils.LOWERCASE,
EmailLength: TeamEmailLen,
EmailCharset: utils.LOWERCASE,
}
}
func (cfg *AutoTeamCreator) createRandomTeam() (*model.Team, error) {
var teamEmail string
var teamDisplayName string
var teamName string
if cfg.Fuzzy {
teamEmail = "success+" + model.NewId() + "simulator.amazonses.com"
teamDisplayName = utils.FuzzName()
teamName = model.NewRandomTeamName()
} else {
teamEmail = "success+" + model.NewId() + "simulator.amazonses.com"
teamDisplayName = utils.RandomName(cfg.NameLength, cfg.NameCharset)
teamName = utils.RandomName(cfg.NameLength, cfg.NameCharset) + model.NewId()
}
team := &model.Team{
DisplayName: teamDisplayName,
Name: teamName,
Email: teamEmail,
Type: model.TeamOpen,
}
createdTeam, _, err := cfg.client.CreateTeam(team)
if err != nil {
return nil, err
}
return createdTeam, nil
}
func (cfg *AutoTeamCreator) CreateTestTeams(num utils.Range) ([]*model.Team, error) {
numTeams := utils.RandIntFromRange(num)
teams := make([]*model.Team, numTeams)
for i := 0; i < numTeams; i++ {
var err error
teams[i], err = cfg.createRandomTeam()
if err != nil {
return nil, err
}
}
return teams, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
type AutoUserCreator struct {
app *app.App
client *model.Client4
team *model.Team
EmailLength utils.Range
EmailCharset string
NameLength utils.Range
NameCharset string
Fuzzy bool
JoinTime int64
}
func NewAutoUserCreator(a *app.App, client *model.Client4, team *model.Team) *AutoUserCreator {
return &AutoUserCreator{
app: a,
client: client,
team: team,
EmailLength: UserEmailLen,
EmailCharset: utils.LOWERCASE,
NameLength: UserNameLen,
NameCharset: utils.LOWERCASE,
Fuzzy: false,
JoinTime: 0,
}
}
// Basic test team and user so you always know one
func CreateBasicUser(a *app.App, client *model.Client4) error {
found, _, _ := client.TeamExists(BTestTeamName, "")
if found {
return nil
}
newteam := &model.Team{DisplayName: BTestTeamDisplayName, Name: BTestTeamName, Email: BTestTeamEmail, Type: BTestTeamType}
basicteam, _, err := client.CreateTeam(newteam)
if err != nil {
return err
}
newuser := &model.User{Email: BTestUserEmail, Nickname: BTestUserName, Password: BTestUserPassword}
ruser, _, err := client.CreateUser(newuser)
if err != nil {
return err
}
_, err = a.Srv().Store().User().VerifyEmail(ruser.Id, ruser.Email)
if err != nil {
return model.NewAppError("CreateBasicUser", "app.user.verify_email.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if _, nErr := a.Srv().Store().Team().SaveMember(&model.TeamMember{TeamId: basicteam.Id, UserId: ruser.Id, CreateAt: model.GetMillis()}, *a.Config().TeamSettings.MaxUsersPerTeam); nErr != nil {
var appErr *model.AppError
var conflictErr *store.ErrConflict
var limitExceededErr *store.ErrLimitExceeded
switch {
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
return appErr
case errors.As(nErr, &conflictErr):
return model.NewAppError("CreateBasicUser", "app.create_basic_user.save_member.conflict.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &limitExceededErr):
return model.NewAppError("CreateBasicUser", "app.create_basic_user.save_member.max_accounts.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
default: // last fallback in case it doesn't map to an existing app error.
return model.NewAppError("CreateBasicUser", "app.create_basic_user.save_member.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return nil
}
func (cfg *AutoUserCreator) createRandomUser(c request.CTX) (*model.User, error) {
var userEmail string
var userName string
if cfg.Fuzzy {
userEmail = "success+" + model.NewId() + "@simulator.amazonses.com"
userName = utils.FuzzName()
} else {
userEmail = "success+" + model.NewId() + "@simulator.amazonses.com"
userName = utils.RandomName(cfg.NameLength, cfg.NameCharset)
}
user := &model.User{
Email: userEmail,
Nickname: userName,
Password: UserPassword,
CreateAt: cfg.JoinTime,
}
ruser, appErr := cfg.app.CreateUserWithInviteId(c, user, cfg.team.InviteId, "")
if appErr != nil {
return nil, appErr
}
status := &model.Status{
UserId: ruser.Id,
Status: model.StatusOnline,
Manual: false,
LastActivityAt: ruser.CreateAt,
ActiveChannel: "",
}
if err := cfg.app.Srv().Store().Status().SaveOrUpdate(status); err != nil {
return nil, err
}
// We need to cheat to verify the user's email
_, err := cfg.app.Srv().Store().User().VerifyEmail(ruser.Id, ruser.Email)
if err != nil {
return nil, err
}
if cfg.JoinTime != 0 {
teamMember, appErr := cfg.app.GetTeamMember(cfg.team.Id, ruser.Id)
if appErr != nil {
return nil, appErr
}
teamMember.CreateAt = cfg.JoinTime
_, err := cfg.app.Srv().Store().Team().UpdateMember(teamMember)
if err != nil {
return nil, err
}
}
return ruser, nil
}
func (cfg *AutoUserCreator) CreateTestUsers(c request.CTX, num utils.Range) ([]*model.User, error) {
numUsers := utils.RandIntFromRange(num)
users := make([]*model.User, numUsers)
for i := 0; i < numUsers; i++ {
var err error
users[i], err = cfg.createRandomUser(c)
if err != nil {
return nil, err
}
}
return users, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type AwayProvider struct {
}
const (
CmdAway = "away"
)
func init() {
app.RegisterCommandProvider(&AwayProvider{})
}
func (*AwayProvider) GetTrigger() string {
return CmdAway
}
func (*AwayProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdAway,
AutoComplete: true,
AutoCompleteDesc: T("api.command_away.desc"),
DisplayName: T("api.command_away.name"),
}
}
func (*AwayProvider) DoCommand(a *app.App, _ request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
a.SetStatusAwayIfNeeded(args.UserId, true)
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command_away.success")}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type HeaderProvider struct {
}
const (
CmdHeader = "header"
)
func init() {
app.RegisterCommandProvider(&HeaderProvider{})
}
func (*HeaderProvider) GetTrigger() string {
return CmdHeader
}
func (*HeaderProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdHeader,
AutoComplete: true,
AutoCompleteDesc: T("api.command_channel_header.desc"),
AutoCompleteHint: T("api.command_channel_header.hint"),
DisplayName: T("api.command_channel_header.name"),
}
}
func (*HeaderProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
channel, err := a.GetChannel(c, args.ChannelId)
if err != nil {
return &model.CommandResponse{
Text: args.T("api.command_channel_header.channel.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
switch channel.Type {
case model.ChannelTypeOpen:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePublicChannelProperties) {
return &model.CommandResponse{
Text: args.T("api.command_channel_header.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
case model.ChannelTypePrivate:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePrivateChannelProperties) {
return &model.CommandResponse{
Text: args.T("api.command_channel_header.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
case model.ChannelTypeGroup, model.ChannelTypeDirect:
// Modifying the header is not linked to any specific permission for group/dm channels, so just check for membership.
var channelMember *model.ChannelMember
channelMember, err = a.GetChannelMember(c, args.ChannelId, args.UserId)
if err != nil || channelMember == nil {
return &model.CommandResponse{
Text: args.T("api.command_channel_header.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
default:
return &model.CommandResponse{
Text: args.T("api.command_channel_header.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if message == "" {
return &model.CommandResponse{
Text: args.T("api.command_channel_header.message.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
patch := &model.ChannelPatch{
Header: new(string),
}
*patch.Header = message
_, err = a.PatchChannel(c, channel, patch, args.UserId)
if err != nil {
text := args.T("api.command_channel_header.update_channel.app_error")
if err.Id == "model.channel.is_valid.header.app_error" {
text = args.T("api.command_channel_header.update_channel.max_length", map[string]any{
"MaxLength": model.ChannelHeaderMaxRunes,
})
}
return &model.CommandResponse{
Text: text,
ResponseType: model.CommandResponseTypeEphemeral,
}
}
return &model.CommandResponse{}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type PurposeProvider struct {
}
const (
CmdPurpose = "purpose"
)
func init() {
app.RegisterCommandProvider(&PurposeProvider{})
}
func (*PurposeProvider) GetTrigger() string {
return CmdPurpose
}
func (*PurposeProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdPurpose,
AutoComplete: true,
AutoCompleteDesc: T("api.command_channel_purpose.desc"),
AutoCompleteHint: T("api.command_channel_purpose.hint"),
DisplayName: T("api.command_channel_purpose.name"),
}
}
func (*PurposeProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
channel, err := a.GetChannel(c, args.ChannelId)
if err != nil {
return &model.CommandResponse{
Text: args.T("api.command_channel_purpose.channel.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
switch channel.Type {
case model.ChannelTypeOpen:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePublicChannelProperties) {
return &model.CommandResponse{
Text: args.T("api.command_channel_purpose.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
case model.ChannelTypePrivate:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePrivateChannelProperties) {
return &model.CommandResponse{
Text: args.T("api.command_channel_purpose.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
default:
return &model.CommandResponse{
Text: args.T("api.command_channel_purpose.direct_group.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if message == "" {
return &model.CommandResponse{
Text: args.T("api.command_channel_purpose.message.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
patch := &model.ChannelPatch{
Purpose: new(string),
}
*patch.Purpose = message
_, err = a.PatchChannel(c, channel, patch, args.UserId)
if err != nil {
text := args.T("api.command_channel_purpose.update_channel.app_error")
if err.Id == "model.channel.is_valid.purpose.app_error" {
text = args.T("api.command_channel_purpose.update_channel.max_length", map[string]any{
"MaxLength": model.ChannelPurposeMaxRunes,
})
}
return &model.CommandResponse{
Text: text,
ResponseType: model.CommandResponseTypeEphemeral,
}
}
return &model.CommandResponse{}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type RenameProvider struct {
}
const (
CmdRename = "rename"
)
func init() {
app.RegisterCommandProvider(&RenameProvider{})
}
func (*RenameProvider) GetTrigger() string {
return CmdRename
}
func (*RenameProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
renameAutocompleteData := model.NewAutocompleteData(CmdRename, T("api.command_channel_rename.hint"), T("api.command_channel_rename.desc"))
renameAutocompleteData.AddTextArgument(T("api.command_channel_rename.hint"), "[text]", "")
return &model.Command{
Trigger: CmdRename,
AutoComplete: true,
AutoCompleteDesc: T("api.command_channel_rename.desc"),
AutoCompleteHint: T("api.command_channel_rename.hint"),
DisplayName: T("api.command_channel_rename.name"),
AutocompleteData: renameAutocompleteData,
}
}
func (*RenameProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
channel, err := a.GetChannel(c, args.ChannelId)
if err != nil {
return &model.CommandResponse{
Text: args.T("api.command_channel_rename.channel.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
switch channel.Type {
case model.ChannelTypeOpen:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePublicChannelProperties) {
return &model.CommandResponse{
Text: args.T("api.command_channel_rename.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
case model.ChannelTypePrivate:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePrivateChannelProperties) {
return &model.CommandResponse{
Text: args.T("api.command_channel_rename.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
default:
return &model.CommandResponse{Text: args.T("api.command_channel_rename.direct_group.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if message == "" {
return &model.CommandResponse{
Text: args.T("api.command_channel_rename.message.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
} else if len(message) > model.ChannelNameMaxLength {
return &model.CommandResponse{
Text: args.T("api.command_channel_rename.too_long.app_error", map[string]any{
"Length": model.ChannelNameMaxLength,
}),
ResponseType: model.CommandResponseTypeEphemeral,
}
} else if len(message) < model.ChannelNameMinLength {
return &model.CommandResponse{
Text: args.T("api.command_channel_rename.too_short.app_error", map[string]any{
"Length": model.ChannelNameMinLength,
}),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
patch := &model.ChannelPatch{
DisplayName: new(string),
}
*patch.DisplayName = message
_, err = a.PatchChannel(c, channel, patch, args.UserId)
if err != nil {
return &model.CommandResponse{
Text: args.T("api.command_channel_rename.update_channel.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
return &model.CommandResponse{}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type CodeProvider struct {
}
const (
CmdCode = "code"
)
func init() {
app.RegisterCommandProvider(&CodeProvider{})
}
func (*CodeProvider) GetTrigger() string {
return CmdCode
}
func (*CodeProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdCode,
AutoComplete: true,
AutoCompleteDesc: T("api.command_code.desc"),
AutoCompleteHint: T("api.command_code.hint"),
DisplayName: T("api.command_code.name"),
}
}
func (*CodeProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
if message == "" {
return &model.CommandResponse{Text: args.T("api.command_code.message.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
rmsg := " " + strings.Join(strings.Split(message, "\n"), "\n ")
return &model.CommandResponse{ResponseType: model.CommandResponseTypeInChannel, Text: rmsg, SkipSlackParsing: true}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"regexp"
"strings"
"unicode/utf8"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type CustomStatusProvider struct {
}
const (
CmdCustomStatus = app.CmdCustomStatusTrigger
CmdCustomStatusClear = "clear"
)
func init() {
app.RegisterCommandProvider(&CustomStatusProvider{})
}
func (*CustomStatusProvider) GetTrigger() string {
return CmdCustomStatus
}
func (*CustomStatusProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdCustomStatus,
AutoComplete: true,
AutoCompleteDesc: T("api.command_custom_status.desc"),
AutoCompleteHint: T("api.command_custom_status.hint"),
DisplayName: T("api.command_custom_status.name"),
}
}
func (*CustomStatusProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
if !*a.Config().TeamSettings.EnableCustomUserStatuses {
return nil
}
message = strings.TrimSpace(message)
if message == CmdCustomStatusClear {
if err := a.RemoveCustomStatus(c, args.UserId); err != nil {
mlog.Debug(err.Error())
return &model.CommandResponse{Text: args.T("api.command_custom_status.clear.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: args.T("api.command_custom_status.clear.success"),
}
}
customStatus := GetCustomStatus(message)
customStatus.PreSave()
if err := a.SetCustomStatus(c, args.UserId, customStatus); err != nil {
mlog.Debug(err.Error())
return &model.CommandResponse{Text: args.T("api.command_custom_status.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: args.T("api.command_custom_status.success", map[string]any{
"EmojiName": ":" + customStatus.Emoji + ":",
"StatusMessage": customStatus.Text,
}),
}
}
func GetCustomStatus(message string) *model.CustomStatus {
customStatus := &model.CustomStatus{
Emoji: model.DefaultCustomStatusEmoji,
Text: message,
}
firstEmojiLocations := model.EmojiPattern.FindIndex([]byte(message))
if len(firstEmojiLocations) > 0 && firstEmojiLocations[0] == 0 {
// emoji found at starting index
customStatus.Emoji = message[firstEmojiLocations[0]+1 : firstEmojiLocations[1]-1]
customStatus.Text = strings.TrimSpace(message[firstEmojiLocations[1]:])
return customStatus
}
if message == "" {
return customStatus
}
spaceSeparatedMessage := strings.Fields(message)
if len(spaceSeparatedMessage) == 0 {
return customStatus
}
emojiString := spaceSeparatedMessage[0]
var unicode []string
for utf8.RuneCountInString(emojiString) >= 1 {
codepoint, size := utf8.DecodeRuneInString(emojiString)
code := model.RuneToHexadecimalString(codepoint)
unicode = append(unicode, code)
emojiString = emojiString[size:]
}
unicodeString := removeUnicodeSkinTone(strings.Join(unicode, "-"))
emoji, count := model.GetEmojiNameFromUnicode(unicodeString)
if count > 0 {
customStatus.Emoji = emoji
textString := strings.Join(spaceSeparatedMessage[1:], " ")
customStatus.Text = strings.TrimSpace(textString)
}
return customStatus
}
func removeUnicodeSkinTone(unicodeString string) string {
skinToneDetectorRegex := regexp.MustCompile("-(1f3fb|1f3fc|1f3fd|1f3fe|1f3ff)")
skinToneLocations := skinToneDetectorRegex.FindIndex([]byte(unicodeString))
if len(skinToneLocations) == 0 {
return unicodeString
}
if _, count := model.GetEmojiNameFromUnicode(unicodeString); count > 0 {
return unicodeString
}
unicodeWithRemovedSkinTone := unicodeString[:skinToneLocations[0]] + unicodeString[skinToneLocations[1]:]
unicodeWithVariationSelector := unicodeString[:skinToneLocations[0]] + "-fe0f" + unicodeString[skinToneLocations[1]:]
if _, count := model.GetEmojiNameFromUnicode(unicodeWithRemovedSkinTone); count > 0 {
unicodeString = unicodeWithRemovedSkinTone
} else if _, count := model.GetEmojiNameFromUnicode(unicodeWithVariationSelector); count > 0 {
unicodeString = unicodeWithVariationSelector
}
return unicodeString
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type DndProvider struct {
}
const (
CmdDND = "dnd"
)
func init() {
app.RegisterCommandProvider(&DndProvider{})
}
func (*DndProvider) GetTrigger() string {
return CmdDND
}
func (*DndProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdDND,
AutoComplete: true,
AutoCompleteDesc: T("api.command_dnd.desc"),
DisplayName: T("api.command_dnd.name"),
}
}
func (*DndProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
a.SetStatusDoNotDisturb(args.UserId)
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command_dnd.success")}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strconv"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var echoSem chan bool
type EchoProvider struct {
}
const (
CmdEcho = "echo"
)
func init() {
app.RegisterCommandProvider(&EchoProvider{})
}
func (*EchoProvider) GetTrigger() string {
return CmdEcho
}
func (*EchoProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdEcho,
AutoComplete: true,
AutoCompleteDesc: T("api.command_echo.desc"),
AutoCompleteHint: T("api.command_echo.hint"),
DisplayName: T("api.command_echo.name"),
}
}
func (*EchoProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
if message == "" {
return &model.CommandResponse{Text: args.T("api.command_echo.message.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
maxThreads := 100
delay := 0
if endMsg := strings.LastIndex(message, "\""); string(message[0]) == "\"" && endMsg > 1 {
if checkDelay, err := strconv.Atoi(strings.Trim(message[endMsg:], " \"")); err == nil {
delay = checkDelay
}
message = message[1:endMsg]
} else if strings.Contains(message, " ") {
delayIdx := strings.LastIndex(message, " ")
delayStr := strings.Trim(message[delayIdx:], " ")
if checkDelay, err := strconv.Atoi(delayStr); err == nil {
delay = checkDelay
message = message[:delayIdx]
}
}
if delay > 10000 {
return &model.CommandResponse{Text: args.T("api.command_echo.delay.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if echoSem == nil {
// We want one additional thread allowed so we never reach channel lockup
echoSem = make(chan bool, maxThreads+1)
}
if len(echoSem) >= maxThreads {
return &model.CommandResponse{Text: args.T("api.command_echo.high_volume.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
echoSem <- true
a.Srv().Go(func() {
defer func() { <-echoSem }()
post := &model.Post{}
post.ChannelId = args.ChannelId
post.RootId = args.RootId
post.Message = message
post.UserId = args.UserId
time.Sleep(time.Duration(delay) * time.Second)
if _, err := a.CreatePostMissingChannel(c, post, true, true); err != nil {
mlog.Error("Unable to create /echo post.", mlog.Err(err))
}
})
return &model.CommandResponse{}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"encoding/json"
"strconv"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type ExpandProvider struct {
}
type CollapseProvider struct {
}
const (
CmdExpand = "expand"
CmdCollapse = "collapse"
)
func init() {
app.RegisterCommandProvider(&ExpandProvider{})
app.RegisterCommandProvider(&CollapseProvider{})
}
func (*ExpandProvider) GetTrigger() string {
return CmdExpand
}
func (*CollapseProvider) GetTrigger() string {
return CmdCollapse
}
func (*ExpandProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdExpand,
AutoComplete: true,
AutoCompleteDesc: T("api.command_expand.desc"),
DisplayName: T("api.command_expand.name"),
}
}
func (*CollapseProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdCollapse,
AutoComplete: true,
AutoCompleteDesc: T("api.command_collapse.desc"),
DisplayName: T("api.command_collapse.name"),
}
}
func (*ExpandProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
return setCollapsePreference(a, args, false)
}
func (*CollapseProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
return setCollapsePreference(a, args, true)
}
func setCollapsePreference(a *app.App, args *model.CommandArgs, isCollapse bool) *model.CommandResponse {
pref := model.Preference{
UserId: args.UserId,
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameCollapseSetting,
Value: strconv.FormatBool(isCollapse),
}
if err := a.Srv().Store().Preference().Save(model.Preferences{pref}); err != nil {
return &model.CommandResponse{Text: args.T("api.command_expand_collapse.fail.app_error") + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}
}
socketMessage := model.NewWebSocketEvent(model.WebsocketEventPreferenceChanged, "", "", args.UserId, nil, "")
prefJSON, err := json.Marshal(pref)
if err != nil {
return &model.CommandResponse{Text: args.T("api.marshal_error") + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}
}
socketMessage.Add("preference", string(prefJSON))
a.Publish(socketMessage)
var rmsg string
if isCollapse {
rmsg = args.T("api.command_collapse.success")
} else {
rmsg = args.T("api.command_expand.success")
}
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: rmsg}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"fmt"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type groupmsgProvider struct {
}
const (
CmdGroupMsg = "groupmsg"
)
func init() {
app.RegisterCommandProvider(&groupmsgProvider{})
}
func (*groupmsgProvider) GetTrigger() string {
return CmdGroupMsg
}
func (*groupmsgProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdGroupMsg,
AutoComplete: true,
AutoCompleteDesc: T("api.command_groupmsg.desc"),
AutoCompleteHint: T("api.command_groupmsg.hint"),
DisplayName: T("api.command_groupmsg.name"),
}
}
func (*groupmsgProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
targetUsers := map[string]*model.User{}
targetUsersSlice := []string{args.UserId}
invalidUsernames := []string{}
users, parsedMessage := groupMsgUsernames(message)
for _, username := range users {
username = strings.TrimSpace(username)
username = strings.TrimPrefix(username, "@")
targetUser, nErr := a.Srv().Store().User().GetByUsername(username)
if nErr != nil {
invalidUsernames = append(invalidUsernames, username)
continue
}
canSee, err := a.UserCanSeeOtherUser(args.UserId, targetUser.Id)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_groupmsg.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if !canSee {
invalidUsernames = append(invalidUsernames, username)
continue
}
_, exists := targetUsers[targetUser.Id]
if !exists && targetUser.Id != args.UserId {
targetUsers[targetUser.Id] = targetUser
targetUsersSlice = append(targetUsersSlice, targetUser.Id)
}
}
if len(invalidUsernames) > 0 {
invalidUsersString := map[string]any{
"Users": "@" + strings.Join(invalidUsernames, ", @"),
}
return &model.CommandResponse{
Text: args.T("api.command_groupmsg.invalid_user.app_error", len(invalidUsernames), invalidUsersString),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if len(targetUsersSlice) == 2 {
return app.GetCommandProvider("msg").DoCommand(a, c, args, fmt.Sprintf("%s %s", targetUsers[targetUsersSlice[1]].Username, parsedMessage))
}
if len(targetUsersSlice) < model.ChannelGroupMinUsers {
minUsers := map[string]any{
"MinUsers": model.ChannelGroupMinUsers - 1,
}
return &model.CommandResponse{
Text: args.T("api.command_groupmsg.min_users.app_error", minUsers),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if len(targetUsersSlice) > model.ChannelGroupMaxUsers {
maxUsers := map[string]any{
"MaxUsers": model.ChannelGroupMaxUsers - 1,
}
return &model.CommandResponse{
Text: args.T("api.command_groupmsg.max_users.app_error", maxUsers),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
var groupChannel *model.Channel
var channelErr *model.AppError
if a.HasPermissionTo(args.UserId, model.PermissionCreateGroupChannel) {
groupChannel, channelErr = a.CreateGroupChannel(c, targetUsersSlice, args.UserId)
if channelErr != nil {
mlog.Error(channelErr.Error())
return &model.CommandResponse{Text: args.T("api.command_groupmsg.group_fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
} else {
groupChannel, channelErr = a.GetGroupChannel(c, targetUsersSlice)
if channelErr != nil {
return &model.CommandResponse{Text: args.T("api.command_groupmsg.permission.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
}
if parsedMessage != "" {
post := &model.Post{}
post.Message = parsedMessage
post.ChannelId = groupChannel.Id
post.UserId = args.UserId
if _, err := a.CreatePostMissingChannel(c, post, true, true); err != nil {
return &model.CommandResponse{Text: args.T("api.command_groupmsg.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
}
team, err := a.GetTeam(args.TeamId)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_groupmsg.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + groupChannel.Name, Text: "", ResponseType: model.CommandResponseTypeEphemeral}
}
func groupMsgUsernames(message string) ([]string, string) {
result := []string{}
resultMessage := ""
for idx, part := range strings.Split(message, ",") {
clean := strings.TrimPrefix(strings.TrimSpace(part), "@")
split := strings.Fields(clean)
if len(split) > 0 {
result = append(result, split[0])
}
if len(split) > 1 {
splitted := strings.SplitN(message, ",", idx+1)
resultMessage = strings.TrimPrefix(strings.TrimSpace(splitted[len(splitted)-1]), "@")
resultMessage = strings.TrimSpace(strings.TrimPrefix(resultMessage, split[0]))
break
}
}
return result, resultMessage
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type HelpProvider struct {
}
const (
CmdHelp = "help"
)
func init() {
app.RegisterCommandProvider(&HelpProvider{})
}
func (h *HelpProvider) GetTrigger() string {
return CmdHelp
}
func (h *HelpProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdHelp,
AutoComplete: true,
AutoCompleteDesc: T("api.command_help.desc"),
DisplayName: T("api.command_help.name"),
}
}
func (h *HelpProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
helpLink := *a.Config().SupportSettings.HelpLink
if helpLink == "" {
helpLink = model.SupportSettingsDefaultHelpLink
}
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: args.T("api.command_help.success", map[string]any{
"HelpLink": helpLink,
}),
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type InviteProvider struct {
}
const (
CmdInvite = "invite"
)
func init() {
app.RegisterCommandProvider(&InviteProvider{})
}
func (*InviteProvider) GetTrigger() string {
return CmdInvite
}
func (*InviteProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdInvite,
AutoComplete: true,
AutoCompleteDesc: T("api.command_invite.desc"),
AutoCompleteHint: T("api.command_invite.hint"),
DisplayName: T("api.command_invite.name"),
}
}
func (i *InviteProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
return &model.CommandResponse{
Text: i.doCommand(a, c, args, message),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
func (i *InviteProvider) doCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) string {
if message == "" {
return args.T("api.command_invite.missing_message.app_error")
}
resps := &[]string{}
targetUsers, targetChannels, resp := i.parseMessage(a, c, args, resps, message)
if resp != "" {
return resp
}
// Verify that the inviter has permissions to invite users to the every channel.
targetChannels = i.checkPermissions(a, c, args, resps, targetUsers[0], targetChannels)
for _, targetUser := range targetUsers {
for _, targetChannel := range targetChannels {
if resp = i.addUserToChannel(a, c, args, targetUser, targetChannel); resp != "" {
*resps = append(*resps, resp)
continue
}
if args.ChannelId != targetChannel.Id {
*resps = append(*resps, args.T("api.command_invite.success", map[string]any{
"User": targetUser.Username,
"Channel": targetChannel.Name,
}))
}
}
}
if len(*resps) > 0 {
return strings.Join(*resps, "\n")
}
return ""
}
func (i *InviteProvider) parseMessage(a *app.App, c request.CTX, args *model.CommandArgs, resps *[]string, message string) ([]*model.User, []*model.Channel, string) {
splitMessage := strings.Split(message, " ")
targetUsers := make([]*model.User, 0, 1)
targetChannels := make([]*model.Channel, 0)
for j, msg := range splitMessage {
if msg == "" {
continue
}
if msg[0] == '@' || (msg[0] != '~' && j == 0) {
targetUsername := strings.TrimPrefix(msg, "@")
userProfile := i.getUserProfile(a, targetUsername)
if userProfile == nil {
*resps = append(*resps, args.T("api.command_invite.missing_user.app_error", map[string]any{
"User": targetUsername,
}))
continue
}
targetUsers = append(targetUsers, userProfile)
} else {
targetChannelName := strings.TrimPrefix(msg, "~")
channelToJoin, err := a.GetChannelByName(c, targetChannelName, args.TeamId, false)
if err != nil {
*resps = append(*resps, args.T("api.command_invite.channel.error", map[string]any{
"Channel": targetChannelName,
}))
continue
}
targetChannels = append(targetChannels, channelToJoin)
}
}
if len(targetUsers) == 0 {
if len(*resps) != 0 {
return nil, nil, strings.Join(*resps, "\n")
}
return nil, nil, args.T("api.command_invite.missing_message.app_error")
}
if len(targetChannels) == 0 {
if len(*resps) != 0 {
return nil, nil, strings.Join(*resps, "\n")
}
channelToJoin, err := a.GetChannel(c, args.ChannelId)
if err != nil {
return nil, nil, args.T("api.command_invite.channel.app_error")
}
targetChannels = append(targetChannels, channelToJoin)
}
return targetUsers, targetChannels, ""
}
func (i *InviteProvider) getUserProfile(a *app.App, username string) *model.User {
userProfile, nErr := a.Srv().Store().User().GetByUsername(username)
if nErr != nil {
return nil
}
if userProfile.DeleteAt != 0 {
return nil
}
return userProfile
}
func (i *InviteProvider) checkPermissions(a *app.App, c request.CTX, args *model.CommandArgs, resps *[]string, targetUser *model.User, targetChannels []*model.Channel) []*model.Channel {
var err *model.AppError
validChannels := make([]*model.Channel, 0, len(targetChannels))
for _, targetChannel := range targetChannels {
switch targetChannel.Type {
case model.ChannelTypeOpen:
if !a.HasPermissionToChannel(c, args.UserId, targetChannel.Id, model.PermissionManagePublicChannelMembers) {
*resps = append(*resps, args.T("api.command_invite.permission.app_error", map[string]any{
"User": targetUser.Username,
"Channel": targetChannel.Name,
}))
continue
}
case model.ChannelTypePrivate:
if !a.HasPermissionToChannel(c, args.UserId, targetChannel.Id, model.PermissionManagePrivateChannelMembers) {
if _, err = a.GetChannelMember(c, targetChannel.Id, args.UserId); err == nil {
// User doing the inviting is a member of the channel.
*resps = append(*resps, args.T("api.command_invite.permission.app_error", map[string]any{
"User": targetUser.Username,
"Channel": targetChannel.Name,
}))
continue
}
// User doing the inviting is *not* a member of the channel.
*resps = append(*resps, args.T("api.command_invite.private_channel.app_error", map[string]any{
"Channel": targetChannel.Name,
}))
continue
}
default:
*resps = append(*resps, args.T("api.command_invite.directchannel.app_error"))
continue
}
validChannels = append(validChannels, targetChannel)
}
return validChannels
}
func (i *InviteProvider) addUserToChannel(a *app.App, c request.CTX, args *model.CommandArgs, userProfile *model.User, channelToJoin *model.Channel) string {
// Check if user is already in the channel
_, err := a.GetChannelMember(c, channelToJoin.Id, userProfile.Id)
if err == nil {
return args.T("api.command_invite.user_already_in_channel.app_error", map[string]any{
"User": userProfile.Username,
})
}
if _, err = a.AddChannelMember(c, userProfile.Id, channelToJoin, app.ChannelMemberOpts{UserRequestorID: args.UserId}); err != nil {
if err.Id == "api.channel.add_members.user_denied" {
return args.T("api.command_invite.group_constrained_user_denied")
} else if err.Id == "app.team.get_member.missing.app_error" ||
err.Id == "api.channel.add_user.to.channel.failed.deleted.app_error" {
return args.T("api.command_invite.user_not_in_team.app_error", map[string]any{
"Username": userProfile.Username,
})
}
return args.T("api.command_invite.fail.app_error")
}
return ""
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type InvitePeopleProvider struct {
}
const (
CmdInvite_PEOPLE = "invite_people"
)
func init() {
app.RegisterCommandProvider(&InvitePeopleProvider{})
}
func (*InvitePeopleProvider) GetTrigger() string {
return CmdInvite_PEOPLE
}
func (*InvitePeopleProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
autoComplete := true
if !*a.Config().EmailSettings.SendEmailNotifications || !*a.Config().TeamSettings.EnableUserCreation || !*a.Config().ServiceSettings.EnableEmailInvitations {
autoComplete = false
}
return &model.Command{
Trigger: CmdInvite_PEOPLE,
AutoComplete: autoComplete,
AutoCompleteDesc: T("api.command.invite_people.desc"),
AutoCompleteHint: T("api.command.invite_people.hint"),
DisplayName: T("api.command.invite_people.name"),
}
}
func (*InvitePeopleProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
if !a.HasPermissionToTeam(args.UserId, args.TeamId, model.PermissionInviteUser) {
return &model.CommandResponse{Text: args.T("api.command_invite_people.permission.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if !a.HasPermissionToTeam(args.UserId, args.TeamId, model.PermissionAddUserToTeam) {
return &model.CommandResponse{Text: args.T("api.command_invite_people.permission.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if !*a.Config().EmailSettings.SendEmailNotifications {
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command.invite_people.email_off")}
}
if !*a.Config().TeamSettings.EnableUserCreation {
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command.invite_people.invite_off")}
}
if !*a.Config().ServiceSettings.EnableEmailInvitations {
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command.invite_people.email_invitations_off")}
}
emailList := strings.Fields(message)
for i := len(emailList) - 1; i >= 0; i-- {
emailList[i] = strings.Trim(emailList[i], ",")
if !strings.Contains(emailList[i], "@") {
emailList = append(emailList[:i], emailList[i+1:]...)
}
}
if len(emailList) == 0 {
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command.invite_people.no_email")}
}
if err := a.InviteNewUsersToTeam(emailList, args.TeamId, args.UserId); err != nil {
mlog.Error(err.Error())
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command.invite_people.fail")}
}
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command.invite_people.sent")}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type JoinProvider struct {
}
const (
CmdJoin = "join"
)
func init() {
app.RegisterCommandProvider(&JoinProvider{})
}
func (*JoinProvider) GetTrigger() string {
return CmdJoin
}
func (*JoinProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdJoin,
AutoComplete: true,
AutoCompleteDesc: T("api.command_join.desc"),
AutoCompleteHint: T("api.command_join.hint"),
DisplayName: T("api.command_join.name"),
}
}
func (*JoinProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
channelName := strings.ToLower(message)
if strings.HasPrefix(message, "~") {
channelName = message[1:]
}
channel, err := a.Srv().Store().Channel().GetByName(args.TeamId, channelName, true)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_join.list.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if channel.Name != channelName {
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command_join.missing.app_error")}
}
switch channel.Type {
case model.ChannelTypeOpen:
if !a.HasPermissionToChannel(c, args.UserId, channel.Id, model.PermissionJoinPublicChannels) {
return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
case model.ChannelTypePrivate:
if !a.HasPermissionToChannel(c, args.UserId, channel.Id, model.PermissionReadChannel) {
return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
default:
return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if appErr := a.JoinChannel(c, channel, args.UserId); appErr != nil {
return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
team, appErr := a.GetTeam(channel.TeamId)
if appErr != nil {
return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + channel.Name}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type LeaveProvider struct {
}
const (
CmdLeave = "leave"
)
func init() {
app.RegisterCommandProvider(&LeaveProvider{})
}
func (*LeaveProvider) GetTrigger() string {
return CmdLeave
}
func (*LeaveProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdLeave,
AutoComplete: true,
AutoCompleteDesc: T("api.command_leave.desc"),
DisplayName: T("api.command_leave.name"),
}
}
func (*LeaveProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
var channel *model.Channel
var noChannelErr *model.AppError
if channel, noChannelErr = a.GetChannel(c, args.ChannelId); noChannelErr != nil {
return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
team, err := a.GetTeam(args.TeamId)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
err = a.LeaveChannel(c, args.ChannelId, args.UserId)
if err != nil {
if channel.Name == model.DefaultChannelName {
return &model.CommandResponse{Text: args.T("api.channel.leave.default.app_error", map[string]any{"Channel": model.DefaultChannelName}), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
member, err := a.GetTeamMember(team.Id, args.UserId)
if err != nil || member.DeleteAt != 0 {
return &model.CommandResponse{GotoLocation: args.SiteURL + "/"}
}
user, err := a.GetUser(args.UserId)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if user.IsGuest() {
members, err := a.GetChannelMembersForUser(c, team.Id, args.UserId)
if err != nil || len(members) == 0 {
return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
channel, err := a.GetChannel(c, members[0].ChannelId)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + channel.Name}
}
return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + model.DefaultChannelName}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"encoding/json"
"io"
"net/http"
"path"
"regexp"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var usage = `Mattermost testing commands to help configure the system
COMMANDS:
Setup - Creates a testing environment in current team.
/test setup [teams] [fuzz] <Num Channels> <Num Users> <NumPosts>
Example:
/test setup teams fuzz 10 20 50
Users - Add a specified number of random users with fuzz text to current team, at the specified Unix timestamp in milliseconds.
/test users [fuzz] [range=min[,max]] [time=user_join_timestamp]
Default: range=2,5 time=
Examples:
/test users fuzz range=3,8 time=1565076128000
/test users range=1
Channels - Add a specified number of random public (o) or private (p) channels with fuzz text to current team, at the specified Unix timestamp in milliseconds.
/test channels [fuzz] [range=min[,max]] [type=(o|p)] [time=channel_create_timestamp]
Default: range=2,5 type=o time=
Examples:
/test channels fuzz range=5,10 type=p time=1565076128000
/test channels range=1
DMs - Add a specified number of random DM messages between the current user and a specified user, at the specified Unix timestamp in milliseconds. If a timestamp is provided, posts are created one millisecond apart. Note: You may need to clear your browser cache in order to see these posts in the UI.
/test dms u=@username [range=min[,max]] [time=dm_create_timestamp]
Default: range=2,5 time=
Examples:
/test dms u=@user range=5,10 time=1565076128000
/test dms u=@user range=2
ThreadedPost - Create a threaded post with a specified number of replies at the specified Unix timestamp in milliseconds. If a timestamp is provided, posts are created one millisecond apart. Note: You may need to clear your browser cache in order to see these posts in the UI.
/test threaded_post [range=min[,max]] [time=post_timestamp]
Default: range=1000 time=
Examples:
/test threaded_post
/test threaded_post range=100,200 time=1565076128000
Posts - Add some random posts with fuzz text to current channel, at the specified Unix timestamp in milliseconds. If a timestamp is provided, posts are created one millisecond apart. Note: You may need to clear your browser cache in order to see these posts in the UI.
/test posts [fuzz] [range=min[,max]] [images=max_images] [time=post_timestamp]
Default: range=2,5 images=0 time=
Example:
/test posts fuzz range=5,10 images=3 time=1565076128000
/test posts range=2
Post - Add post to a channel as another user.
/test post u=@username p=passwd c=~channelname t=teamname "message"
Example:
/test post u=@user-1 p=user-1 c=~town-square t=ad-1 "message"
Url - Add a post containing the text from a given url to current channel.
/test url
Example:
/test http://www.example.com/sample_file.md
Json - Add a post using the JSON file as payload to the current channel.
/test json url
Example:
/test json http://www.example.com/sample_body.json
`
const (
CmdTest = "test"
)
var (
userRE = regexp.MustCompile(`u=@?([^\s]+)`)
passwdRE = regexp.MustCompile(`p=([^\s]+)`)
teamRE = regexp.MustCompile(`t=([^\s]+)`)
channelRE = regexp.MustCompile(`c=~([^\s]+)`)
messageRE = regexp.MustCompile(`"(.*)"`)
fuzzRE = regexp.MustCompile(`fuzz`)
rangeRE = regexp.MustCompile(`range=([^\s]+)`)
timeRE = regexp.MustCompile(`time=([^\s]+)`)
imagesRE = regexp.MustCompile(`images=([^\s]+)`)
typeRE = regexp.MustCompile(`type=([^\s])+`)
)
type LoadTestProvider struct {
}
func init() {
app.RegisterCommandProvider(&LoadTestProvider{})
}
func (*LoadTestProvider) GetTrigger() string {
return CmdTest
}
func (*LoadTestProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
if !*a.Config().ServiceSettings.EnableTesting {
return nil
}
return &model.Command{
Trigger: CmdTest,
AutoComplete: false,
AutoCompleteDesc: "Debug Load Testing",
AutoCompleteHint: "help",
DisplayName: "test",
}
}
func (lt *LoadTestProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
commandResponse, err := lt.doCommand(a, c, args, message)
if err != nil {
c.Logger().Error("failed command /"+CmdTest, mlog.Err(err))
}
return commandResponse
}
func (lt *LoadTestProvider) doCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
//This command is only available when EnableTesting is true
if !*a.Config().ServiceSettings.EnableTesting {
return &model.CommandResponse{}, nil
}
if strings.HasPrefix(message, "setup") {
return lt.SetupCommand(a, c, args, message)
}
if strings.HasPrefix(message, "users") {
return lt.UsersCommand(a, c, args, message)
}
if strings.HasPrefix(message, "activate_user") {
return lt.ActivateUserCommand(a, c, args, message)
}
if strings.HasPrefix(message, "deactivate_user") {
return lt.DeActivateUserCommand(a, c, args, message)
}
if strings.HasPrefix(message, "channels") {
return lt.ChannelsCommand(a, c, args, message)
}
if strings.HasPrefix(message, "dms") {
return lt.DMsCommand(a, c, args, message)
}
if strings.HasPrefix(message, "posts") {
return lt.PostsCommand(a, c, args, message)
}
if strings.HasPrefix(message, "post") {
return lt.PostCommand(a, c, args, message)
}
if strings.HasPrefix(message, "threaded_post") {
return lt.ThreadedPostCommand(a, c, args, message)
}
if strings.HasPrefix(message, "url") {
return lt.URLCommand(a, c, args, message)
}
if strings.HasPrefix(message, "json") {
return lt.JsonCommand(a, c, args, message)
}
return lt.HelpCommand(args, message), nil
}
func (*LoadTestProvider) HelpCommand(args *model.CommandArgs, message string) *model.CommandResponse {
return &model.CommandResponse{Text: usage, ResponseType: model.CommandResponseTypeEphemeral}
}
func (*LoadTestProvider) SetupCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
tokens := strings.Fields(strings.TrimPrefix(message, "setup"))
doTeams := contains(tokens, "teams")
doFuzz := contains(tokens, "fuzz")
numArgs := 0
if doTeams {
numArgs++
}
if doFuzz {
numArgs++
}
var numTeams int
var numChannels int
var numUsers int
var numPosts int
// Defaults
numTeams = 10
numChannels = 10
numUsers = 10
numPosts = 10
if doTeams {
if (len(tokens) - numArgs) >= 4 {
numTeams, _ = strconv.Atoi(tokens[numArgs+0])
numChannels, _ = strconv.Atoi(tokens[numArgs+1])
numUsers, _ = strconv.Atoi(tokens[numArgs+2])
numPosts, _ = strconv.Atoi(tokens[numArgs+3])
}
} else {
if (len(tokens) - numArgs) >= 3 {
numChannels, _ = strconv.Atoi(tokens[numArgs+0])
numUsers, _ = strconv.Atoi(tokens[numArgs+1])
numPosts, _ = strconv.Atoi(tokens[numArgs+2])
}
}
client := model.NewAPIv4Client(args.SiteURL)
if doTeams {
if err := CreateBasicUser(a, client); err != nil {
return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.CommandResponseTypeEphemeral}, err
}
_, _, err := client.Login(BTestUserEmail, BTestUserPassword)
if err != nil {
return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.CommandResponseTypeEphemeral}, err
}
environment, err := CreateTestEnvironmentWithTeams(
a,
c,
client,
utils.Range{Begin: numTeams, End: numTeams},
utils.Range{Begin: numChannels, End: numChannels},
utils.Range{Begin: numUsers, End: numUsers},
utils.Range{Begin: numPosts, End: numPosts},
doFuzz)
if err != nil {
return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.CommandResponseTypeEphemeral}, err
}
c.Logger().Info("Testing environment created")
for i := 0; i < len(environment.Teams); i++ {
c.Logger().Info("Team Created: " + environment.Teams[i].Name)
c.Logger().Info("\t User to login: " + environment.Environments[i].Users[0].Email + ", " + UserPassword)
}
} else {
team, err := a.Srv().Store().Team().Get(args.TeamId)
if err != nil {
return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.CommandResponseTypeEphemeral}, err
}
CreateTestEnvironmentInTeam(
a,
c,
client,
team,
utils.Range{Begin: numChannels, End: numChannels},
utils.Range{Begin: numUsers, End: numUsers},
utils.Range{Begin: numPosts, End: numPosts},
doFuzz)
}
return &model.CommandResponse{Text: "Created environment", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) ActivateUserCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
user_id := strings.TrimSpace(strings.TrimPrefix(message, "activate_user"))
if err := a.UpdateUserActive(c, user_id, true); err != nil {
return &model.CommandResponse{Text: "Failed to activate user", ResponseType: model.CommandResponseTypeEphemeral}, err
}
return &model.CommandResponse{Text: "Activated user", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) DeActivateUserCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
user_id := strings.TrimSpace(strings.TrimPrefix(message, "deactivate_user"))
if err := a.UpdateUserActive(c, user_id, false); err != nil {
return &model.CommandResponse{Text: "Failed to deactivate user", ResponseType: model.CommandResponseTypeEphemeral}, err
}
return &model.CommandResponse{Text: "DeActivated user", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) UsersCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
cmd := strings.TrimSpace(strings.TrimPrefix(message, "users"))
doFuzz := false
if fuzzRE.MatchString(cmd) {
doFuzz = true
}
var err error
rng := utils.Range{Begin: 2, End: 5}
rangeParam := getMatch(rangeRE, cmd)
if rangeParam != "" {
rng, err = parseRange(rangeParam)
if err != nil {
return &model.CommandResponse{Text: "Failed to add users: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
team, err := a.Srv().Store().Team().Get(args.TeamId)
if err != nil {
return &model.CommandResponse{Text: "Failed to add users", ResponseType: model.CommandResponseTypeEphemeral}, err
}
time := int64(0)
timeParam := getMatch(timeRE, cmd)
if timeParam != "" {
time, err = strconv.ParseInt(timeParam, 10, 64)
if err != nil || time < 0 {
return &model.CommandResponse{Text: "Failed to add users: Invalid time parameter", ResponseType: model.CommandResponseTypeEphemeral}, errors.New("Invalid time parameter")
}
}
client := model.NewAPIv4Client(args.SiteURL)
userCreator := NewAutoUserCreator(a, client, team)
userCreator.Fuzzy = doFuzz
userCreator.JoinTime = time
if _, err := userCreator.CreateTestUsers(c, rng); err != nil {
return &model.CommandResponse{Text: "Failed to add users: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
return &model.CommandResponse{Text: "Added users", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) ChannelsCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
cmd := strings.TrimSpace(strings.TrimPrefix(message, "channels"))
doFuzz := false
if fuzzRE.MatchString(cmd) {
doFuzz = true
}
var err error
rng := utils.Range{Begin: 2, End: 5}
rangeParam := getMatch(rangeRE, cmd)
if rangeParam != "" {
rng, err = parseRange(rangeParam)
if err != nil {
return &model.CommandResponse{Text: "Failed to add channels: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
team, err := a.Srv().Store().Team().Get(args.TeamId)
if err != nil {
return &model.CommandResponse{Text: "Failed to add channels", ResponseType: model.CommandResponseTypeEphemeral}, err
}
typ := model.ChannelTypeOpen
typeParam := getMatch(typeRE, cmd)
if typeParam != "" {
switch strings.ToUpper(typeParam) {
case "O":
case "P":
typ = model.ChannelTypePrivate
default:
return &model.CommandResponse{Text: "Failed to add channels: Invalid type parameter", ResponseType: model.CommandResponseTypeEphemeral}, errors.New("Invalid type parameter")
}
}
time := int64(0)
timeParam := getMatch(timeRE, cmd)
if timeParam != "" {
time, err = strconv.ParseInt(timeParam, 10, 64)
if err != nil || time < 0 {
return &model.CommandResponse{Text: "Failed to add channels: Invalid time parameter", ResponseType: model.CommandResponseTypeEphemeral}, errors.New("Invalid time parameter")
}
}
channelCreator := NewAutoChannelCreator(a, team, args.UserId)
channelCreator.Fuzzy = doFuzz
channelCreator.CreateTime = time
channelCreator.ChannelType = typ
if _, err := channelCreator.CreateTestChannels(c, rng); err != nil {
return &model.CommandResponse{Text: "Failed to create test channels: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
return &model.CommandResponse{Text: "Added channels", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) DMsCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
cmd := strings.TrimSpace(strings.TrimPrefix(message, "dms"))
var err error
username := getMatch(userRE, message)
user, appErr := a.GetUserByUsername(username)
if appErr != nil {
return &model.CommandResponse{Text: "Failed to add DMS: Invalid username", ResponseType: model.CommandResponseTypeEphemeral}, appErr
}
rng := utils.Range{Begin: 2, End: 5}
rangeParam := getMatch(rangeRE, cmd)
if rangeParam != "" {
rng, err = parseRange(rangeParam)
if err != nil {
return &model.CommandResponse{Text: "Failed to add DMs: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
time := int64(0)
timeParam := getMatch(timeRE, cmd)
if timeParam != "" {
time, err = strconv.ParseInt(timeParam, 10, 64)
if err != nil || time < 0 {
return &model.CommandResponse{Text: "Failed to add DMs: Invalid time parameter", ResponseType: model.CommandResponseTypeEphemeral}, errors.New("Invalid time parameter")
}
}
channel, err := a.GetOrCreateDirectChannel(c, args.UserId, user.Id)
postCreator := NewAutoPostCreator(a, channel.Id, args.UserId)
postCreator.CreateTime = time
postCreator.UsersToPostFrom = []string{user.Id}
numPosts := utils.RandIntFromRange(rng)
for i := 0; i < numPosts; i++ {
if _, err := postCreator.CreateRandomPost(c); err != nil {
return &model.CommandResponse{Text: "Failed to create test DMs: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
return &model.CommandResponse{Text: "Added DMs", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) ThreadedPostCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
cmd := strings.TrimSpace(strings.TrimPrefix(message, "threaded_post"))
var err error
rng := utils.Range{Begin: 1000, End: 1000}
rangeParam := getMatch(rangeRE, cmd)
if rangeParam != "" {
rng, err = parseRange(rangeParam)
if err != nil {
return &model.CommandResponse{Text: "Failed to create post: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
time := int64(0)
timeParam := getMatch(timeRE, cmd)
if timeParam != "" {
time, err = strconv.ParseInt(timeParam, 10, 64)
if err != nil || time < 0 {
return &model.CommandResponse{Text: "Failed to create post: Invalid time parameter", ResponseType: model.CommandResponseTypeEphemeral}, errors.New("Invalid time parameter")
}
}
var usernames []string
options := &model.UserGetOptions{InTeamId: args.TeamId, Page: 0, PerPage: 1000}
if profileUsers, err := a.Srv().Store().User().GetProfiles(options); err == nil {
usernames = make([]string, len(profileUsers))
i := 0
for _, userprof := range profileUsers {
usernames[i] = userprof.Username
i++
}
}
testPoster := NewAutoPostCreator(a, args.ChannelId, args.UserId)
testPoster.Fuzzy = true
testPoster.Users = usernames
testPoster.CreateTime = time
rpost, err2 := testPoster.CreateRandomPost(c)
if err2 != nil {
return &model.CommandResponse{Text: "Failed to create a post", ResponseType: model.CommandResponseTypeEphemeral}, err2
}
numPosts := utils.RandIntFromRange(rng)
for i := 0; i < numPosts; i++ {
testPoster.CreateRandomPostNested(c, rpost.Id)
}
return &model.CommandResponse{Text: "Added threaded post", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) PostsCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
cmd := strings.TrimSpace(strings.TrimPrefix(message, "posts"))
doFuzz := false
if fuzzRE.MatchString(cmd) {
doFuzz = true
}
var err error
rng := utils.Range{Begin: 2, End: 5}
rangeParam := getMatch(rangeRE, cmd)
if rangeParam != "" {
rng, err = parseRange(rangeParam)
if err != nil {
return &model.CommandResponse{Text: "Failed to add posts: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
maxImages := 0
imagesParam := getMatch(imagesRE, cmd)
if imagesParam != "" {
maxImages, err = strconv.Atoi(imagesParam)
if err != nil {
return &model.CommandResponse{Text: "Failed to add posts: Invalid images parameter", ResponseType: model.CommandResponseTypeEphemeral}, errors.New("Invalid images parameter")
}
}
time := int64(0)
timeParam := getMatch(timeRE, cmd)
if timeParam != "" {
time, err = strconv.ParseInt(timeParam, 10, 64)
if err != nil || time < 0 {
return &model.CommandResponse{Text: "Failed to add posts: Invalid time parameter", ResponseType: model.CommandResponseTypeEphemeral}, errors.New("Invalid time parameter")
}
}
var usernames []string
options := &model.UserGetOptions{InTeamId: args.TeamId, Page: 0, PerPage: 1000}
if profileUsers, err := a.Srv().Store().User().GetProfiles(options); err == nil {
usernames = make([]string, len(profileUsers))
i := 0
for _, userprof := range profileUsers {
usernames[i] = userprof.Username
i++
}
}
testPoster := NewAutoPostCreator(a, args.ChannelId, args.UserId)
testPoster.Fuzzy = doFuzz
testPoster.Users = usernames
testPoster.CreateTime = time
numImages := utils.RandIntFromRange(utils.Range{Begin: 0, End: maxImages})
numPosts := utils.RandIntFromRange(rng)
for i := 0; i < numPosts; i++ {
testPoster.HasImage = (i < numImages)
_, err := testPoster.CreateRandomPost(c)
if err != nil {
return &model.CommandResponse{Text: "Failed to add posts", ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
return &model.CommandResponse{Text: "Added posts", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func getMatch(re *regexp.Regexp, text string) string {
if match := re.FindStringSubmatch(text); match != nil {
return match[1]
}
return ""
}
func (*LoadTestProvider) PostCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
textMessage := getMatch(messageRE, message)
if textMessage == "" {
return &model.CommandResponse{Text: "No message to post", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
teamName := getMatch(teamRE, message)
team, err := a.GetTeamByName(teamName)
if err != nil {
return &model.CommandResponse{Text: "Failed to get a team", ResponseType: model.CommandResponseTypeEphemeral}, err
}
channelName := getMatch(channelRE, message)
channel, err := a.GetChannelByName(c, channelName, team.Id, true)
if err != nil {
return &model.CommandResponse{Text: "Failed to get a channel", ResponseType: model.CommandResponseTypeEphemeral}, err
}
passwd := getMatch(passwdRE, message)
username := getMatch(userRE, message)
user, err := a.GetUserByUsername(username)
if err != nil {
return &model.CommandResponse{Text: "Failed to get a user", ResponseType: model.CommandResponseTypeEphemeral}, err
}
client := model.NewAPIv4Client(args.SiteURL)
_, _, nErr := client.LoginById(user.Id, passwd)
if nErr != nil {
return &model.CommandResponse{Text: "Failed to login a user", ResponseType: model.CommandResponseTypeEphemeral}, nErr
}
post := &model.Post{
ChannelId: channel.Id,
Message: textMessage,
}
_, _, nErr = client.CreatePost(post)
if nErr != nil {
return &model.CommandResponse{Text: "Failed to create a post", ResponseType: model.CommandResponseTypeEphemeral}, nErr
}
return &model.CommandResponse{Text: "Added a post to " + channel.DisplayName, ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) URLCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
url := strings.TrimSpace(strings.TrimPrefix(message, "url"))
if url == "" {
return &model.CommandResponse{Text: "Command must contain a url", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
// provide a shortcut to easily access tests stored in doc/developer/tests
if !strings.HasPrefix(url, "http") {
url = "https://raw.githubusercontent.com/mattermost/mattermost-server/master/tests/" + url
if path.Ext(url) == "" {
url += ".md"
}
}
r, err := http.Get(url)
if err != nil {
return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.CommandResponseTypeEphemeral}, err
}
defer func() {
io.Copy(io.Discard, r.Body)
r.Body.Close()
}()
if r.StatusCode > 400 {
return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.CommandResponseTypeEphemeral}, errors.Errorf("unexpected status code %d", r.StatusCode)
}
bytes := make([]byte, 4000)
// break contents into 4000 byte posts
for {
length, err := r.Body.Read(bytes)
if err != nil && err != io.EOF {
return &model.CommandResponse{Text: "Encountered error reading file", ResponseType: model.CommandResponseTypeEphemeral}, err
}
if length == 0 {
break
}
post := &model.Post{}
post.Message = string(bytes[:length])
post.ChannelId = args.ChannelId
post.UserId = args.UserId
if _, err := a.CreatePostMissingChannel(c, post, false, true); err != nil {
return &model.CommandResponse{Text: "Unable to create post", ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
return &model.CommandResponse{Text: "Loaded data", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) JsonCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
url := strings.TrimSpace(strings.TrimPrefix(message, "json"))
if url == "" {
return &model.CommandResponse{Text: "Command must contain a url", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
// provide a shortcut to easily access tests stored in doc/developer/tests
if !strings.HasPrefix(url, "http") {
url = "https://raw.githubusercontent.com/mattermost/mattermost-server/master/tests/" + url
if path.Ext(url) == "" {
url += ".json"
}
}
r, err := http.Get(url)
if err != nil {
return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.CommandResponseTypeEphemeral}, err
}
if r.StatusCode > 400 {
return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.CommandResponseTypeEphemeral}, errors.Errorf("unexpected status code %d", r.StatusCode)
}
defer func() {
io.Copy(io.Discard, r.Body)
r.Body.Close()
}()
var post model.Post
if jsonErr := json.NewDecoder(r.Body).Decode(&post); jsonErr != nil {
return &model.CommandResponse{Text: "Unable to decode post", ResponseType: model.CommandResponseTypeEphemeral}, errors.Wrapf(jsonErr, "could not decode post from json")
}
post.ChannelId = args.ChannelId
post.UserId = args.UserId
if post.Message == "" {
post.Message = message
}
if _, err := a.CreatePostMissingChannel(c, &post, false, true); err != nil {
return &model.CommandResponse{Text: "Unable to create post", ResponseType: model.CommandResponseTypeEphemeral}, err
}
return &model.CommandResponse{Text: "Loaded data", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func parseRange(rng string) (utils.Range, error) {
tokens := strings.Split(rng, ",")
var begin int
var end int
var err1 error
var err2 error
switch {
case len(tokens) == 1:
begin, err1 = strconv.Atoi(tokens[0])
if err1 != nil || begin < 0 {
return utils.Range{Begin: 0, End: 0}, errors.New("Invalid range parameter")
}
end = begin
case len(tokens) == 2:
begin, err1 = strconv.Atoi(tokens[0])
end, err2 = strconv.Atoi(tokens[1])
if err1 != nil || err2 != nil || begin < 0 || end < begin {
return utils.Range{Begin: 0, End: 0}, errors.New("Invalid range parameter")
}
default:
return utils.Range{Begin: 0, End: 0}, errors.New("Invalid range parameter")
}
return utils.Range{Begin: begin, End: end}, nil
}
func contains(items []string, token string) bool {
for _, elem := range items {
if elem == token {
return true
}
}
return false
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type LogoutProvider struct {
}
const (
CmdLogout = "logout"
)
func init() {
app.RegisterCommandProvider(&LogoutProvider{})
}
func (*LogoutProvider) GetTrigger() string {
return CmdLogout
}
func (*LogoutProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdLogout,
AutoComplete: true,
AutoCompleteDesc: T("api.command_logout.desc"),
AutoCompleteHint: "",
DisplayName: T("api.command_logout.name"),
}
}
func (*LogoutProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
// Actual logout is handled client side.
return &model.CommandResponse{GotoLocation: "/login"}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type MarketplaceProvider struct {
}
const (
CmdMarketplace = "marketplace"
)
func init() {
app.RegisterCommandProvider(&MarketplaceProvider{})
}
func (h *MarketplaceProvider) GetTrigger() string {
return CmdMarketplace
}
func (h *MarketplaceProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
enabled := false
pluginSettings := a.Config().PluginSettings
if *pluginSettings.Enable && *pluginSettings.EnableMarketplace {
enabled = true
}
return &model.Command{
Trigger: CmdMarketplace,
AutoComplete: enabled,
AutoCompleteDesc: T("api.command_marketplace.desc"),
DisplayName: T("api.command_marketplace.name"),
}
}
func (h *MarketplaceProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
// This command is handled client-side and shouldn't hit the server.
return &model.CommandResponse{
Text: args.T("api.command_marketplace.unsupported.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type MeProvider struct {
}
const (
CmdMe = "me"
)
func init() {
app.RegisterCommandProvider(&MeProvider{})
}
func (*MeProvider) GetTrigger() string {
return CmdMe
}
func (*MeProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdMe,
AutoComplete: true,
AutoCompleteDesc: T("api.command_me.desc"),
AutoCompleteHint: T("api.command_me.hint"),
DisplayName: T("api.command_me.name"),
}
}
func (*MeProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeInChannel,
Type: model.PostTypeMe,
Text: "*" + message + "*",
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"errors"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type msgProvider struct {
}
const (
CmdMsg = "msg"
)
func init() {
app.RegisterCommandProvider(&msgProvider{})
}
func (*msgProvider) GetTrigger() string {
return CmdMsg
}
func (*msgProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdMsg,
AutoComplete: true,
AutoCompleteDesc: T("api.command_msg.desc"),
AutoCompleteHint: T("api.command_msg.hint"),
DisplayName: T("api.command_msg.name"),
}
}
func (*msgProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
splitMessage := strings.SplitN(message, " ", 2)
parsedMessage := ""
targetUsername := ""
if len(splitMessage) > 1 {
parsedMessage = strings.SplitN(message, " ", 2)[1]
}
targetUsername = strings.SplitN(message, " ", 2)[0]
targetUsername = strings.TrimPrefix(targetUsername, "@")
userProfile, nErr := a.Srv().Store().User().GetByUsername(targetUsername)
if nErr != nil {
mlog.Error(nErr.Error())
return &model.CommandResponse{Text: args.T("api.command_msg.missing.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if userProfile.Id == args.UserId {
return &model.CommandResponse{Text: args.T("api.command_msg.missing.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
canSee, err := a.UserCanSeeOtherUser(args.UserId, userProfile.Id)
if err != nil {
mlog.Error(err.Error())
return &model.CommandResponse{Text: args.T("api.command_msg.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if !canSee {
return &model.CommandResponse{Text: args.T("api.command_msg.missing.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
// Find the channel based on this user
channelName := model.GetDMNameFromIds(args.UserId, userProfile.Id)
targetChannelId := ""
if channel, channelErr := a.Srv().Store().Channel().GetByName(args.TeamId, channelName, true); channelErr != nil {
var nfErr *store.ErrNotFound
if errors.As(channelErr, &nfErr) {
if !a.HasPermissionTo(args.UserId, model.PermissionCreateDirectChannel) {
return &model.CommandResponse{Text: args.T("api.command_msg.permission.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
var directChannel *model.Channel
if directChannel, err = a.GetOrCreateDirectChannel(c, args.UserId, userProfile.Id); err != nil {
mlog.Error(err.Error())
return &model.CommandResponse{Text: args.T(err.Id), ResponseType: model.CommandResponseTypeEphemeral}
}
targetChannelId = directChannel.Id
} else {
mlog.Error(channelErr.Error())
return &model.CommandResponse{Text: args.T("api.command_msg.dm_fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
} else {
targetChannelId = channel.Id
}
if parsedMessage != "" {
post := &model.Post{}
post.Message = parsedMessage
post.ChannelId = targetChannelId
post.UserId = args.UserId
if _, err = a.CreatePostMissingChannel(c, post, true, true); err != nil {
return &model.CommandResponse{Text: args.T("api.command_msg.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
}
team, err := a.GetTeam(args.TeamId)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_msg.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + channelName, Text: "", ResponseType: model.CommandResponseTypeEphemeral}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type MuteProvider struct {
}
const (
CmdMute = "mute"
)
func init() {
app.RegisterCommandProvider(&MuteProvider{})
}
func (*MuteProvider) GetTrigger() string {
return CmdMute
}
func (*MuteProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdMute,
AutoComplete: true,
AutoCompleteDesc: T("api.command_mute.desc"),
AutoCompleteHint: T("api.command_mute.hint"),
DisplayName: T("api.command_mute.name"),
}
}
func (*MuteProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
var channel *model.Channel
var noChannelErr *model.AppError
if channel, noChannelErr = a.GetChannel(c, args.ChannelId); noChannelErr != nil {
return &model.CommandResponse{Text: args.T("api.command_mute.no_channel.error"), ResponseType: model.CommandResponseTypeEphemeral}
}
channelName := ""
splitMessage := strings.Split(message, " ")
// Overwrite channel with channel-handle if set
if strings.HasPrefix(message, "~") {
channelName = splitMessage[0][1:]
} else {
channelName = splitMessage[0]
}
if channelName != "" && message != "" {
channel, _ = a.Srv().Store().Channel().GetByName(channel.TeamId, channelName, true)
if channel == nil {
return &model.CommandResponse{Text: args.T("api.command_mute.error", map[string]any{"Channel": channelName}), ResponseType: model.CommandResponseTypeEphemeral}
}
}
channelMember, err := a.ToggleMuteChannel(c, channel.Id, args.UserId)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_mute.not_member.error", map[string]any{"Channel": channelName}), ResponseType: model.CommandResponseTypeEphemeral}
}
// Direct and Group messages won't have a nice channel title, omit it
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
if channelMember.NotifyProps[model.MarkUnreadNotifyProp] == model.ChannelNotifyMention {
return &model.CommandResponse{Text: args.T("api.command_mute.success_mute_direct_msg"), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{Text: args.T("api.command_mute.success_unmute_direct_msg"), ResponseType: model.CommandResponseTypeEphemeral}
}
if channelMember.NotifyProps[model.MarkUnreadNotifyProp] == model.ChannelNotifyMention {
return &model.CommandResponse{Text: args.T("api.command_mute.success_mute", map[string]any{"Channel": channel.DisplayName}), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{Text: args.T("api.command_mute.success_unmute", map[string]any{"Channel": channel.DisplayName}), ResponseType: model.CommandResponseTypeEphemeral}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type OfflineProvider struct {
}
const (
CmdOffline = "offline"
)
func init() {
app.RegisterCommandProvider(&OfflineProvider{})
}
func (*OfflineProvider) GetTrigger() string {
return CmdOffline
}
func (*OfflineProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdOffline,
AutoComplete: true,
AutoCompleteDesc: T("api.command_offline.desc"),
DisplayName: T("api.command_offline.name"),
}
}
func (*OfflineProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
a.SetStatusOffline(args.UserId, true)
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command_offline.success")}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type OnlineProvider struct {
}
const (
CmdOnline = "online"
)
func init() {
app.RegisterCommandProvider(&OnlineProvider{})
}
func (*OnlineProvider) GetTrigger() string {
return CmdOnline
}
func (*OnlineProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdOnline,
AutoComplete: true,
AutoCompleteDesc: T("api.command_online.desc"),
DisplayName: T("api.command_online.name"),
}
}
func (*OnlineProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
a.SetStatusOnline(args.UserId, true)
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command_online.success")}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type OpenProvider struct {
JoinProvider
}
const (
CmdOpen = "open"
)
func init() {
app.RegisterCommandProvider(&OpenProvider{})
}
func (open *OpenProvider) GetTrigger() string {
return CmdOpen
}
func (open *OpenProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
cmd := open.JoinProvider.GetCommand(a, T)
cmd.Trigger = CmdOpen
cmd.DisplayName = T("api.command_open.name")
return cmd
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"encoding/base64"
"errors"
"fmt"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
const (
AvailableRemoteActions = "create, accept, remove, status"
)
type RemoteProvider struct {
}
const (
CommandTriggerRemote = "secure-connection"
)
func init() {
app.RegisterCommandProvider(&RemoteProvider{})
}
func (rp *RemoteProvider) GetTrigger() string {
return CommandTriggerRemote
}
func (rp *RemoteProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
remote := model.NewAutocompleteData(rp.GetTrigger(), "[action]", T("api.command_remote.remote_add_remove.help", map[string]any{"Actions": AvailableRemoteActions}))
create := model.NewAutocompleteData("create", "", T("api.command_remote.invite.help"))
create.AddNamedTextArgument("name", T("api.command_remote.name.help"), T("api.command_remote.name.hint"), "", true)
create.AddNamedTextArgument("displayname", T("api.command_remote.displayname.help"), T("api.command_remote.displayname.hint"), "", false)
create.AddNamedTextArgument("password", T("api.command_remote.invite_password.help"), T("api.command_remote.invite_password.hint"), "", true)
accept := model.NewAutocompleteData("accept", "", T("api.command_remote.accept.help"))
accept.AddNamedTextArgument("name", T("api.command_remote.name.help"), T("api.command_remote.name.hint"), "", true)
accept.AddNamedTextArgument("displayname", T("api.command_remote.displayname.help"), T("api.command_remote.displayname.hint"), "", false)
accept.AddNamedTextArgument("password", T("api.command_remote.invite_password.help"), T("api.command_remote.invite_password.hint"), "", true)
accept.AddNamedTextArgument("invite", T("api.command_remote.invitation.help"), T("api.command_remote.invitation.hint"), "", true)
remove := model.NewAutocompleteData("remove", "", T("api.command_remote.remove.help"))
remove.AddNamedDynamicListArgument("connectionID", T("api.command_remote.remove_remote_id.help"), "builtin:"+CommandTriggerRemote, true)
status := model.NewAutocompleteData("status", "", T("api.command_remote.status.help"))
remote.AddCommand(create)
remote.AddCommand(accept)
remote.AddCommand(remove)
remote.AddCommand(status)
return &model.Command{
Trigger: rp.GetTrigger(),
AutoComplete: true,
AutoCompleteDesc: T("api.command_remote.desc"),
AutoCompleteHint: T("api.command_remote.hint"),
DisplayName: T("api.command_remote.name"),
AutocompleteData: remote,
}
}
func (rp *RemoteProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
if !a.HasPermissionTo(args.UserId, model.PermissionManageSecureConnections) {
return responsef(args.T("api.command_remote.permission_required", map[string]any{"Permission": "manage_secure_connections"}))
}
margs := parseNamedArgs(args.Command)
action, ok := margs[ActionKey]
if !ok {
return responsef(args.T("api.command_remote.missing_command", map[string]any{"Actions": AvailableRemoteActions}))
}
switch action {
case "create":
return rp.doCreate(a, args, margs)
case "accept":
return rp.doAccept(a, args, margs)
case "remove":
return rp.doRemove(a, args, margs)
case "status":
return rp.doStatus(a, args, margs)
}
return responsef(args.T("api.command_remote.unknown_action", map[string]any{"Action": action}))
}
func (rp *RemoteProvider) GetAutoCompleteListItems(a *app.App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error) {
if !a.HasPermissionTo(commandArgs.UserId, model.PermissionManageSecureConnections) {
return nil, errors.New("You require `manage_secure_connections` permission to manage secure connections.")
}
if arg.Name == "connectionID" && strings.Contains(parsed, " remove ") {
return getRemoteClusterAutocompleteListItems(a, true)
}
return nil, fmt.Errorf("`%s` is not a dynamic argument", arg.Name)
}
// doCreate creates and displays an encrypted invite that can be used by a remote site to establish a simple trust.
func (rp *RemoteProvider) doCreate(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
password := margs["password"]
if password == "" {
return responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "password"}))
}
name := margs["name"]
if name == "" {
return responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "name"}))
}
displayname := margs["displayname"]
if displayname == "" {
displayname = name
}
url := a.GetSiteURL()
if url == "" {
return responsef(args.T("api.command_remote.site_url_not_set"))
}
rc := &model.RemoteCluster{
Name: name,
DisplayName: displayname,
Token: model.NewId(),
CreatorId: args.UserId,
}
rcSaved, appErr := a.AddRemoteCluster(rc)
if appErr != nil {
return responsef(args.T("api.command_remote.add_remote.error", map[string]any{"Error": appErr.Error()}))
}
// Display the encrypted invitation
invite := &model.RemoteClusterInvite{
RemoteId: rcSaved.RemoteId,
RemoteTeamId: args.TeamId,
SiteURL: url,
Token: rcSaved.Token,
}
encrypted, err := invite.Encrypt(password)
if err != nil {
return responsef(args.T("api.command_remote.encrypt_invitation.error", map[string]any{"Error": err.Error()}))
}
encoded := base64.URLEncoding.EncodeToString(encrypted)
return responsef("##### " + args.T("api.command_remote.invitation_created") + "\n" +
args.T("api.command_remote.invite_summary", map[string]any{"Command": "/secure-connection accept", "Invitation": encoded, "SiteURL": invite.SiteURL}))
}
// doAccept accepts an invitation generated by a remote site.
func (rp *RemoteProvider) doAccept(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
password := margs["password"]
if password == "" {
return responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "password"}))
}
name := margs["name"]
if name == "" {
return responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "name"}))
}
displayname := margs["displayname"]
if displayname == "" {
displayname = name
}
blob := margs["invite"]
if blob == "" {
return responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "invite"}))
}
// invite is encoded as base64 and encrypted
decoded, err := base64.URLEncoding.DecodeString(blob)
if err != nil {
return responsef(args.T("api.command_remote.decode_invitation.error", map[string]any{"Error": err.Error()}))
}
invite := &model.RemoteClusterInvite{}
err = invite.Decrypt(decoded, password)
if err != nil {
return responsef(args.T("api.command_remote.incorrect_password.error", map[string]any{"Error": err.Error()}))
}
rcs, _ := a.GetRemoteClusterService()
if rcs == nil {
return responsef(args.T("api.command_remote.service_not_enabled"))
}
url := a.GetSiteURL()
if url == "" {
return responsef(args.T("api.command_remote.site_url_not_set"))
}
rc, err := rcs.AcceptInvitation(invite, name, displayname, args.UserId, args.TeamId, url)
if err != nil {
return responsef(args.T("api.command_remote.accept_invitation.error", map[string]any{"Error": err.Error()}))
}
return responsef("##### " + args.T("api.command_remote.accept_invitation", map[string]any{"SiteURL": rc.SiteURL}))
}
// doRemove removes a remote cluster from the database, effectively revoking the trust relationship.
func (rp *RemoteProvider) doRemove(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
id, ok := margs["connectionID"]
if !ok {
return responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "remoteId"}))
}
deleted, err := a.DeleteRemoteCluster(id)
if err != nil {
responsef(args.T("api.command_remote.remove_remote.error", map[string]any{"Error": err.Error()}))
}
result := "removed"
if !deleted {
result = "**NOT FOUND**"
}
return responsef("##### " + args.T("api.command_remote.cluster_removed", map[string]any{"RemoteId": id, "Result": result}))
}
// doStatus displays connection status for all remote clusters.
func (rp *RemoteProvider) doStatus(a *app.App, args *model.CommandArgs, _ map[string]string) *model.CommandResponse {
list, err := a.GetAllRemoteClusters(model.RemoteClusterQueryFilter{})
if err != nil {
responsef(args.T("api.command_remote.fetch_status.error", map[string]any{"Error": err.Error()}))
}
if len(list) == 0 {
return responsef("** " + args.T("api.command_remote.remotes_not_found") + " **")
}
var sb strings.Builder
fmt.Fprintf(&sb, args.T("api.command_remote.remote_table_header")+" \n")
// | Secure Connection | Display name | ConnectionID | Site URL | Invite accepted | Online | Last ping |
fmt.Fprintf(&sb, "| :---- | :---- | :---- | :---- | :---- | :---- | :---- | \n")
for _, rc := range list {
accepted := formatBool(args.T, rc.SiteURL != "")
online := formatBool(args.T, isOnline(rc.LastPingAt))
lastPing := formatTimestamp(rc.LastPingAt)
fmt.Fprintf(&sb, "| %s | %s | %s | %s | %s | %s | %s |\n", rc.Name, rc.DisplayName, rc.RemoteId, rc.SiteURL, accepted, online, lastPing)
}
return responsef(sb.String())
}
func isOnline(lastPing int64) bool {
return lastPing > model.GetMillis()-model.RemoteOfflineAfterMillis
}
func getRemoteClusterAutocompleteListItems(a *app.App, includeOffline bool) ([]model.AutocompleteListItem, error) {
filter := model.RemoteClusterQueryFilter{
ExcludeOffline: !includeOffline,
}
clusters, err := a.GetAllRemoteClusters(filter)
if err != nil || len(clusters) == 0 {
return []model.AutocompleteListItem{}, nil
}
list := make([]model.AutocompleteListItem, 0, len(clusters))
for _, rc := range clusters {
item := model.AutocompleteListItem{
Item: rc.RemoteId,
HelpText: fmt.Sprintf("%s (%s)", rc.DisplayName, rc.SiteURL)}
list = append(list, item)
}
return list, nil
}
func getRemoteClusterAutocompleteListItemsNotInChannel(a *app.App, channelId string, includeOffline bool) ([]model.AutocompleteListItem, error) {
filter := model.RemoteClusterQueryFilter{
ExcludeOffline: !includeOffline,
NotInChannel: channelId,
}
all, err := a.GetAllRemoteClusters(filter)
if err != nil || len(all) == 0 {
return []model.AutocompleteListItem{}, nil
}
list := make([]model.AutocompleteListItem, 0, len(all))
for _, rc := range all {
item := model.AutocompleteListItem{
Item: rc.RemoteId,
HelpText: fmt.Sprintf("%s (%s)", rc.DisplayName, rc.SiteURL)}
list = append(list, item)
}
return list, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type RemoveProvider struct {
}
type KickProvider struct {
}
const (
CmdRemove = "remove"
CmdKick = "kick"
)
func init() {
app.RegisterCommandProvider(&RemoveProvider{})
app.RegisterCommandProvider(&KickProvider{})
}
func (*RemoveProvider) GetTrigger() string {
return CmdRemove
}
func (*KickProvider) GetTrigger() string {
return CmdKick
}
func (*RemoveProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdRemove,
AutoComplete: true,
AutoCompleteDesc: T("api.command_remove.desc"),
AutoCompleteHint: T("api.command_remove.hint"),
DisplayName: T("api.command_remove.name"),
}
}
func (*KickProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdKick,
AutoComplete: true,
AutoCompleteDesc: T("api.command_remove.desc"),
AutoCompleteHint: T("api.command_remove.hint"),
DisplayName: T("api.command_kick.name"),
}
}
func (*RemoveProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
return doCommand(a, c, args, message)
}
func (*KickProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
return doCommand(a, c, args, message)
}
func doCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
channel, err := a.GetChannel(c, args.ChannelId)
if err != nil {
return &model.CommandResponse{
Text: args.T("api.command_channel_remove.channel.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
switch channel.Type {
case model.ChannelTypeOpen:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePublicChannelMembers) {
return &model.CommandResponse{
Text: args.T("api.command_remove.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
case model.ChannelTypePrivate:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePrivateChannelMembers) {
return &model.CommandResponse{
Text: args.T("api.command_remove.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
default:
return &model.CommandResponse{
Text: args.T("api.command_remove.direct_group.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if message == "" {
return &model.CommandResponse{
Text: args.T("api.command_remove.message.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
targetUsername := ""
targetUsername = strings.SplitN(message, " ", 2)[0]
targetUsername = strings.TrimPrefix(targetUsername, "@")
userProfile, nErr := a.Srv().Store().User().GetByUsername(targetUsername)
if nErr != nil {
mlog.Error(nErr.Error())
return &model.CommandResponse{
Text: args.T("api.command_remove.missing.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if userProfile.DeleteAt != 0 {
return &model.CommandResponse{
Text: args.T("api.command_remove.missing.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
_, err = a.GetChannelMember(c, args.ChannelId, userProfile.Id)
if err != nil {
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
return &model.CommandResponse{
Text: args.T("api.command_remove.user_not_in_channel", map[string]any{
"Username": userProfile.GetDisplayName(nameFormat),
}),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if err = a.RemoveUserFromChannel(c, userProfile.Id, args.UserId, channel); err != nil {
var text string
if err.Id == "api.channel.remove_members.denied" {
text = args.T("api.command_remove.group_constrained_user_denied")
} else {
text = args.T(err.Id, map[string]any{
"Channel": model.DefaultChannelName,
})
}
return &model.CommandResponse{
Text: text,
ResponseType: model.CommandResponseTypeEphemeral,
}
}
return &model.CommandResponse{}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type SearchProvider struct {
}
const (
CmdSearch = "search"
)
func init() {
app.RegisterCommandProvider(&SearchProvider{})
}
func (search *SearchProvider) GetTrigger() string {
return CmdSearch
}
func (search *SearchProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdSearch,
AutoComplete: true,
AutoCompleteDesc: T("api.command_search.desc"),
AutoCompleteHint: T("api.command_search.hint"),
DisplayName: T("api.command_search.name"),
}
}
func (search *SearchProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
// This command is handled client-side and shouldn't hit the server.
return &model.CommandResponse{
Text: args.T("api.command_search.unsupported.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type SettingsProvider struct {
}
const (
CmdSettings = "settings"
)
func init() {
app.RegisterCommandProvider(&SettingsProvider{})
}
func (settings *SettingsProvider) GetTrigger() string {
return CmdSettings
}
func (settings *SettingsProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdSettings,
AutoComplete: true,
AutoCompleteDesc: T("api.command_settings.desc"),
AutoCompleteHint: "",
DisplayName: T("api.command_settings.name"),
}
}
func (settings *SettingsProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
// This command is handled client-side and shouldn't hit the server.
return &model.CommandResponse{
Text: args.T("api.command_settings.unsupported.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"errors"
"fmt"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type ShareProvider struct {
}
const (
CommandTriggerShare = "share-channel"
AvailableShareActions = "invite, uninvite, unshare, status"
)
func init() {
app.RegisterCommandProvider(&ShareProvider{})
}
func (sp *ShareProvider) GetTrigger() string {
return CommandTriggerShare
}
func (sp *ShareProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
share := model.NewAutocompleteData(CommandTriggerShare, "[action]", T("api.command_share.available_actions", map[string]any{"Actions": AvailableShareActions}))
inviteRemote := model.NewAutocompleteData("invite", "", T("api.command_share.invite_remote.help"))
inviteRemote.AddNamedDynamicListArgument("connectionID", T("api.command_share.remote_id.help"), "builtin:"+CommandTriggerShare, true)
inviteRemote.AddNamedTextArgument("readonly", T("api.command_share.share_read_only.help"), T("api.command_share.share_read_only.hint"), "Y|N|y|n", false)
unInviteRemote := model.NewAutocompleteData("uninvite", "", T("api.command_share.uninvite_remote.help"))
unInviteRemote.AddNamedDynamicListArgument("connectionID", T("api.command_share.uninvite_remote_id.help"), "builtin:"+CommandTriggerShare, true)
unshareChannel := model.NewAutocompleteData("unshare", "", T("api.command_share.unshare_channel.help"))
status := model.NewAutocompleteData("status", "", T("api.command_share.channel_status.help"))
share.AddCommand(inviteRemote)
share.AddCommand(unInviteRemote)
share.AddCommand(unshareChannel)
share.AddCommand(status)
return &model.Command{
Trigger: CommandTriggerShare,
AutoComplete: true,
AutoCompleteDesc: T("api.command_share.desc"),
AutoCompleteHint: T("api.command_share.hint"),
DisplayName: T("api.command_share.name"),
AutocompleteData: share,
}
}
func (sp *ShareProvider) GetAutoCompleteListItems(c request.CTX, a *app.App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error) {
switch {
case strings.Contains(parsed, " share "):
return sp.getAutoCompleteShareChannel(c, a, commandArgs, arg)
case strings.Contains(parsed, " invite "):
return sp.getAutoCompleteInviteRemote(a, commandArgs, arg)
case strings.Contains(parsed, " uninvite "):
return sp.getAutoCompleteUnInviteRemote(a, commandArgs, arg)
}
return nil, errors.New("invalid action")
}
func (sp *ShareProvider) getAutoCompleteShareChannel(c request.CTX, a *app.App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg) ([]model.AutocompleteListItem, error) {
channel, err := a.GetChannel(c, commandArgs.ChannelId)
if err != nil {
return nil, err
}
var item model.AutocompleteListItem
switch arg.Name {
case "name":
item = model.AutocompleteListItem{
Item: channel.Name,
HelpText: channel.DisplayName,
}
case "displayname":
item = model.AutocompleteListItem{
Item: channel.DisplayName,
HelpText: channel.Name,
}
default:
return nil, fmt.Errorf("%s not a dynamic argument", arg.Name)
}
return []model.AutocompleteListItem{item}, nil
}
func (sp *ShareProvider) getAutoCompleteInviteRemote(a *app.App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg) ([]model.AutocompleteListItem, error) {
switch arg.Name {
case "connectionID":
return getRemoteClusterAutocompleteListItemsNotInChannel(a, commandArgs.ChannelId, true)
default:
return nil, fmt.Errorf("%s not a dynamic argument", arg.Name)
}
}
func (sp *ShareProvider) getAutoCompleteUnInviteRemote(a *app.App, _ *model.CommandArgs, arg *model.AutocompleteArg) ([]model.AutocompleteListItem, error) {
switch arg.Name {
case "connectionID":
return getRemoteClusterAutocompleteListItems(a, true)
default:
return nil, fmt.Errorf("%s not a dynamic argument", arg.Name)
}
}
func (sp *ShareProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
if !a.HasPermissionTo(args.UserId, model.PermissionManageSharedChannels) {
return responsef(args.T("api.command_share.permission_required", map[string]any{"Permission": "manage_shared_channels"}))
}
if a.Srv().GetSharedChannelSyncService() == nil {
return responsef(args.T("api.command_share.service_disabled"))
}
if a.Srv().GetRemoteClusterService() == nil {
return responsef(args.T("api.command_remote.service_disabled"))
}
margs := parseNamedArgs(args.Command)
action, ok := margs[ActionKey]
if !ok {
return responsef(args.T("api.command_share.missing_action", map[string]any{"Actions": AvailableShareActions}))
}
switch action {
case "share":
return sp.doShareChannel(a, c, args, margs)
case "unshare":
return sp.doUnshareChannel(a, args, margs)
case "invite":
return sp.doInviteRemote(a, c, args, margs)
case "uninvite":
return sp.doUninviteRemote(a, args, margs)
case "status":
return sp.doStatus(a, args, margs)
}
return responsef(args.T("api.command_share.unknown_action", map[string]any{"Action": action, "Actions": AvailableShareActions}))
}
func (sp *ShareProvider) doShareChannel(a *app.App, c request.CTX, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
// check that channel exists.
channel, errApp := a.GetChannel(c, args.ChannelId)
if errApp != nil {
return responsef(args.T("api.command_share.share_channel.error", map[string]any{"Error": errApp.Error()}))
}
if name := margs["name"]; name == "" {
margs["name"] = channel.Name
}
if name := margs["displayname"]; name == "" {
margs["displayname"] = channel.DisplayName
}
if name := margs["purpose"]; name == "" {
margs["purpose"] = channel.Purpose
}
if name := margs["header"]; name == "" {
margs["header"] = channel.Header
}
if _, ok := margs["readonly"]; !ok {
margs["readonly"] = "N"
}
readonly, err := parseBool(margs["readonly"])
if err != nil {
return responsef(args.T("api.command_share.invalid_value.error", map[string]any{"Arg": "readonly", "Error": err.Error()}))
}
sc := &model.SharedChannel{
ChannelId: args.ChannelId,
TeamId: args.TeamId,
Home: true,
ReadOnly: readonly,
ShareName: margs["name"],
ShareDisplayName: margs["displayname"],
SharePurpose: margs["purpose"],
ShareHeader: margs["header"],
CreatorId: args.UserId,
}
if _, err := a.SaveSharedChannel(c, sc); err != nil {
return responsef(args.T("api.command_share.share_channel.error", map[string]any{"Error": err.Error()}))
}
notifyClientsForChannelUpdate(a, sc)
return responsef("##### " + args.T("api.command_share.channel_shared"))
}
func (sp *ShareProvider) doUnshareChannel(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
sc, appErr := a.GetSharedChannel(args.ChannelId)
if appErr != nil {
return responsef(args.T("api.command_share.shared_channel_unshare.error", map[string]any{"Error": appErr.Error()}))
}
deleted, err := a.DeleteSharedChannel(args.ChannelId)
if err != nil {
return responsef(args.T("api.command_share.shared_channel_unshare.error", map[string]any{"Error": err.Error()}))
}
if !deleted {
return responsef(args.T("api.command_share.not_shared_channel_unshare"))
}
notifyClientsForChannelUpdate(a, sc)
return responsef("##### " + args.T("api.command_share.shared_channel_unavailable"))
}
func (sp *ShareProvider) doInviteRemote(a *app.App, c request.CTX, args *model.CommandArgs, margs map[string]string) (resp *model.CommandResponse) {
remoteId, ok := margs["connectionID"]
if !ok || remoteId == "" {
return responsef(args.T("api.command_share.must_specify_valid_remote"))
}
hasRemote, err := a.HasRemote(args.ChannelId, remoteId)
if err != nil {
return responsef(args.T("api.command_share.fetch_remote.error", map[string]any{"Error": err.Error()}))
}
if hasRemote {
return responsef(args.T("api.command_share.remote_already_invited"))
}
// Check if channel is shared or not.
hasChan, err := a.HasSharedChannel(args.ChannelId)
if err != nil {
return responsef(args.T("api.command_share.check_channel_exist.error", map[string]any{"Error": err.Error()}))
}
if !hasChan {
// If it doesn't exist, then create it.
resp2 := sp.doShareChannel(a, c, args, margs)
// We modify the outgoing response by prepending the text
// from the shareChannel response.
defer func() {
resp.Text = resp2.Text + "\n" + resp.Text
}()
}
// don't allow invitation to shared channel originating from remote.
// (also blocks cyclic invitations)
if err := a.CheckCanInviteToSharedChannel(args.ChannelId); err != nil {
return responsef(args.T("api.command_share.channel_invite_not_home.error"))
}
rc, appErr := a.GetRemoteCluster(remoteId)
if appErr != nil {
return responsef(args.T("api.command_share.remote_id_invalid.error", map[string]any{"Error": appErr.Error()}))
}
channel, errApp := a.GetChannel(c, args.ChannelId)
if errApp != nil {
return responsef(args.T("api.command_share.channel_invite.error", map[string]any{"Name": rc.DisplayName, "Error": errApp.Error()}))
}
// send channel invite to remote cluster
if err := a.Srv().GetSharedChannelSyncService().SendChannelInvite(channel, args.UserId, rc); err != nil {
return responsef(args.T("api.command_share.channel_invite.error", map[string]any{"Name": rc.DisplayName, "Error": err.Error()}))
}
return responsef("##### " + args.T("api.command_share.invitation_sent", map[string]any{"Name": rc.DisplayName, "SiteURL": rc.SiteURL}))
}
func (sp *ShareProvider) doUninviteRemote(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
remoteId, ok := margs["connectionID"]
if !ok || remoteId == "" {
return responsef(args.T("api.command_share.remote_not_valid"))
}
scr, err := a.GetSharedChannelRemoteByIds(args.ChannelId, remoteId)
if err != nil || scr.ChannelId != args.ChannelId {
return responsef(args.T("api.command_share.channel_remote_id_not_exists", map[string]any{"RemoteId": remoteId}))
}
deleted, err := a.DeleteSharedChannelRemote(scr.Id)
if err != nil || !deleted {
return responsef(args.T("api.command_share.could_not_uninvite.error", map[string]any{"RemoteId": remoteId, "Error": err.Error()}))
}
return responsef("##### " + args.T("api.command_share.remote_uninvited", map[string]any{"RemoteId": remoteId}))
}
func (sp *ShareProvider) doStatus(a *app.App, args *model.CommandArgs, _ map[string]string) *model.CommandResponse {
statuses, err := a.GetSharedChannelRemotesStatus(args.ChannelId)
if err != nil {
return responsef(args.T("api.command_share.fetch_remote_status.error", map[string]any{"Error": err.Error()}))
}
if len(statuses) == 0 {
return responsef(args.T("api.command_share.no_remote_invited"))
}
var sb strings.Builder
fmt.Fprintf(&sb, args.T("api.command_share.channel_status_id", map[string]any{"ChannelId": statuses[0].ChannelId})+"\n\n")
fmt.Fprintf(&sb, args.T("api.command_share.remote_table_header")+" \n")
// "| Secure Connection | SiteURL | ReadOnly | InviteAccepted | Online | Last Sync |"
fmt.Fprintf(&sb, "| ---- | ---- | ---- | ---- | ---- | ---- | \n")
for _, status := range statuses {
readonly := formatBool(args.T, status.ReadOnly)
accepted := formatBool(args.T, status.IsInviteAccepted)
online := formatBool(args.T, isOnline(status.LastPingAt))
lastSync := formatTimestamp(status.NextSyncAt)
fmt.Fprintf(&sb, "| %s | %s | %s | %s | %s | %s |\n",
status.DisplayName, status.SiteURL, readonly, accepted, online, lastSync)
}
return responsef(sb.String())
}
func notifyClientsForChannelUpdate(a *app.App, sharedChannel *model.SharedChannel) {
messageWs := model.NewWebSocketEvent(model.WebsocketEventChannelConverted, sharedChannel.TeamId, "", "", nil, "")
messageWs.Add("channel_id", sharedChannel.ChannelId)
a.Publish(messageWs)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type ShortcutsProvider struct {
}
const (
CmdShortcuts = "shortcuts"
)
func init() {
app.RegisterCommandProvider(&ShortcutsProvider{})
}
func (*ShortcutsProvider) GetTrigger() string {
return CmdShortcuts
}
func (*ShortcutsProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdShortcuts,
AutoComplete: true,
AutoCompleteDesc: T("api.command_shortcuts.desc"),
AutoCompleteHint: "",
DisplayName: T("api.command_shortcuts.name"),
}
}
func (*ShortcutsProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
// This command is handled client-side and shouldn't hit the server.
return &model.CommandResponse{
Text: args.T("api.command_shortcuts.unsupported.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type ShrugProvider struct {
}
const (
CmdShrug = "shrug"
)
func init() {
app.RegisterCommandProvider(&ShrugProvider{})
}
func (*ShrugProvider) GetTrigger() string {
return CmdShrug
}
func (*ShrugProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdShrug,
AutoComplete: true,
AutoCompleteDesc: T("api.command_shrug.desc"),
AutoCompleteHint: T("api.command_shrug.hint"),
DisplayName: T("api.command_shrug.name"),
}
}
func (*ShrugProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
rmsg := `¯\\\_(ツ)\_/¯`
if message != "" {
rmsg = message + " " + rmsg
}
return &model.CommandResponse{ResponseType: model.CommandResponseTypeInChannel, Text: rmsg}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type TemplatesProvider struct {
}
const (
CmdTemplates = "templates"
)
func init() {
app.RegisterCommandProvider(&TemplatesProvider{})
}
func (h *TemplatesProvider) GetTrigger() string {
return CmdTemplates
}
func (h *TemplatesProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
workTemplateEnabled := a.Config().FeatureFlags.WorkTemplate
pbActive, err := a.IsPluginActive(model.PluginIdPlaybooks)
if err != nil {
pbActive = false
}
hasBoard, err := a.HasBoardProduct()
if err != nil {
hasBoard = false
}
return &model.Command{
Trigger: CmdTemplates,
AutoComplete: hasBoard && pbActive && workTemplateEnabled,
AutoCompleteDesc: T("api.command_templates.desc"),
DisplayName: T("api.command_templates.name"),
}
}
func (h *TemplatesProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
// This command is handled client-side and shouldn't hit the server.
return &model.CommandResponse{
Text: args.T("api.command_templates.unsupported.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"fmt"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
const (
ActionKey = "-action"
)
// responsef creates an ephemeral command response using printf syntax.
func responsef(format string, args ...any) *model.CommandResponse {
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: fmt.Sprintf(format, args...),
Type: model.PostTypeDefault,
}
}
// parseNamedArgs parses a command string into a map of arguments. It is assumed the
// command string is of the form `<action> --arg1 value1 ...` Supports empty values.
// Arg names are limited to [0-9a-zA-Z_].
func parseNamedArgs(cmd string) map[string]string {
m := make(map[string]string)
split := strings.Fields(cmd)
// check for optional action
if len(split) >= 2 && !strings.HasPrefix(split[1], "--") {
m[ActionKey] = split[1] // prefix with hyphen to avoid collision with arg named "action"
}
for i := 0; i < len(split); i++ {
if !strings.HasPrefix(split[i], "--") {
continue
}
var val string
arg := trimSpaceAndQuotes(strings.Trim(split[i], "-"))
if i < len(split)-1 && !strings.HasPrefix(split[i+1], "--") {
val = trimSpaceAndQuotes(split[i+1])
}
if arg != "" {
m[arg] = val
}
}
return m
}
func trimSpaceAndQuotes(s string) string {
trimmed := strings.TrimSpace(s)
trimmed = strings.TrimPrefix(trimmed, "\"")
trimmed = strings.TrimPrefix(trimmed, "'")
trimmed = strings.TrimSuffix(trimmed, "\"")
trimmed = strings.TrimSuffix(trimmed, "'")
return trimmed
}
func parseBool(s string) (bool, error) {
switch strings.ToLower(s) {
case "1", "t", "true", "yes", "y":
return true, nil
case "0", "f", "false", "no", "n":
return false, nil
}
return false, fmt.Errorf("cannot parse '%s' as a boolean", s)
}
func formatBool(fn i18n.TranslateFunc, b bool) string {
if b {
return fn("True")
}
return fn("False")
}
func formatTimestamp(timestamp int64) string {
if timestamp == 0 {
return "--"
}
ts := model.GetTimeForMillis(timestamp)
if !isToday(ts) {
return ts.Format("Jan 2 15:04:05 MST 2006")
}
date := ts.Format("15:04:05 MST 2006")
return fmt.Sprintf("Today %s", date)
}
func isToday(ts time.Time) bool {
now := time.Now()
year, month, day := ts.Date()
nowYear, nowMonth, nowDay := now.Date()
return year == nowYear && month == nowMonth && day == nowDay
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// GetUserStatusesByIds used by apiV4
func (a *App) GetUserStatusesByIds(userIDs []string) ([]*model.Status, *model.AppError) {
return a.Srv().Platform().GetUserStatusesByIds(userIDs)
}
// SetStatusLastActivityAt sets the last activity at for a user on the local app server and updates
// status to away if needed. Used by the WS to set status to away if an 'online' device disconnects
// while an 'away' device is still connected
func (a *App) SetStatusLastActivityAt(userID string, activityAt int64) {
a.Srv().Platform().SetStatusLastActivityAt(userID, activityAt)
}
func (a *App) SetStatusOnline(userID string, manual bool) {
a.Srv().Platform().SetStatusOnline(userID, manual)
}
func (a *App) SetStatusOffline(userID string, manual bool) {
a.Srv().Platform().SetStatusOffline(userID, manual)
}
func (a *App) SetStatusAwayIfNeeded(userID string, manual bool) {
a.Srv().Platform().SetStatusAwayIfNeeded(userID, manual)
}
// SetStatusDoNotDisturbTimed takes endtime in unix epoch format in UTC
// and sets status of given userId to dnd which will be restored back after endtime
func (a *App) SetStatusDoNotDisturbTimed(userId string, endtime int64) {
a.Srv().Platform().SetStatusDoNotDisturbTimed(userId, endtime)
}
func (a *App) SetStatusDoNotDisturb(userID string) {
a.Srv().Platform().SetStatusDoNotDisturb(userID)
}
func (a *App) SetStatusOutOfOffice(userID string) {
a.Srv().Platform().SetStatusOutOfOffice(userID)
}
func (a *App) GetStatusFromCache(userID string) *model.Status {
return a.Srv().Platform().GetStatusFromCache(userID)
}
func (a *App) GetStatus(userID string) (*model.Status, *model.AppError) {
return a.Srv().Platform().GetStatus(userID)
}
// UpdateDNDStatusOfUsers is a recurring task which is started when server starts
// which unsets dnd status of users if needed and saves and broadcasts it
func (a *App) UpdateDNDStatusOfUsers() {
statuses, err := a.UpdateExpiredDNDStatuses()
if err != nil {
mlog.Warn("Failed to fetch dnd statues from store", mlog.String("err", err.Error()))
return
}
for i := range statuses {
a.Srv().Platform().AddStatusCache(statuses[i])
a.Srv().Platform().BroadcastStatus(statuses[i])
}
}
func (a *App) SetCustomStatus(c request.CTX, userID string, cs *model.CustomStatus) *model.AppError {
if cs == nil || (cs.Emoji == "" && cs.Text == "") {
return model.NewAppError("SetCustomStatus", "api.custom_status.set_custom_statuses.update.app_error", nil, "", http.StatusBadRequest)
}
user, err := a.GetUser(userID)
if err != nil {
return err
}
user.SetCustomStatus(cs)
_, updateErr := a.UpdateUser(c, user, true)
if updateErr != nil {
return updateErr
}
if err := a.addRecentCustomStatus(userID, cs); err != nil {
c.Logger().Error("Can't add recent custom status for", mlog.String("userID", userID), mlog.Err(err))
}
return nil
}
func (a *App) RemoveCustomStatus(c request.CTX, userID string) *model.AppError {
user, err := a.GetUser(userID)
if err != nil {
return err
}
user.ClearCustomStatus()
_, updateErr := a.UpdateUser(c, user, true)
if updateErr != nil {
return updateErr
}
return nil
}
func (a *App) GetCustomStatus(userID string) (*model.CustomStatus, *model.AppError) {
user, err := a.GetUser(userID)
if err != nil {
return &model.CustomStatus{}, err
}
return user.GetCustomStatus(), nil
}
func (a *App) addRecentCustomStatus(userID string, status *model.CustomStatus) *model.AppError {
var newRCS model.RecentCustomStatuses
pref, appErr := a.GetPreferenceByCategoryAndNameForUser(userID, model.PreferenceCategoryCustomStatus, model.PreferenceNameRecentCustomStatuses)
if appErr != nil || pref.Value == "" {
newRCS = model.RecentCustomStatuses{*status}
} else {
var existingRCS model.RecentCustomStatuses
if err := json.Unmarshal([]byte(pref.Value), &existingRCS); err != nil {
return model.NewAppError("addRecentCustomStatus", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(err)
}
newRCS = existingRCS.Add(status)
}
newRCSJSON, err := json.Marshal(newRCS)
if err != nil {
return model.NewAppError("addRecentCustomStatus", "api.marshal_error", nil, "", http.StatusBadRequest).Wrap(err)
}
pref = &model.Preference{
UserId: userID,
Category: model.PreferenceCategoryCustomStatus,
Name: model.PreferenceNameRecentCustomStatuses,
Value: string(newRCSJSON),
}
if appErr := a.UpdatePreferences(userID, model.Preferences{*pref}); appErr != nil {
return appErr
}
return nil
}
func (a *App) RemoveRecentCustomStatus(userID string, status *model.CustomStatus) *model.AppError {
pref, appErr := a.GetPreferenceByCategoryAndNameForUser(userID, model.PreferenceCategoryCustomStatus, model.PreferenceNameRecentCustomStatuses)
if appErr != nil {
return appErr
}
if pref.Value == "" {
return model.NewAppError("RemoveRecentCustomStatus", "api.custom_status.recent_custom_statuses.delete.app_error", nil, "", http.StatusBadRequest)
}
var existingRCS model.RecentCustomStatuses
if err := json.Unmarshal([]byte(pref.Value), &existingRCS); err != nil {
return model.NewAppError("RemoveRecentCustomStatus", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if ok, err := existingRCS.Contains(status); !ok || err != nil {
return model.NewAppError("RemoveRecentCustomStatus", "api.custom_status.recent_custom_statuses.delete.app_error", nil, "", http.StatusBadRequest)
}
newRCS, err := existingRCS.Remove(status)
if err != nil {
return model.NewAppError("RemoveRecentCustomStatus", "api.custom_status.recent_custom_statuses.delete.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
newRCSJSON, err := json.Marshal(newRCS)
if err != nil {
return model.NewAppError("RemoveRecentCustomStatus", "api.marshal_error", nil, "", http.StatusBadRequest).Wrap(err)
}
pref.Value = string(newRCSJSON)
if appErr := a.UpdatePreferences(userID, model.Preferences{*pref}); appErr != nil {
return appErr
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"fmt"
"os"
"runtime"
"strings"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/config"
)
func (a *App) GenerateSupportPacket() []model.FileData {
// If any errors we come across within this function, we will log it in a warning.txt file so that we know why certain files did not get produced if any
var warnings []string
// Creating an array of files that we are going to be adding to our zip file
fileDatas := []model.FileData{}
// A array of the functions that we can iterate through since they all have the same return value
functions := []func() (*model.FileData, string){
a.generateSupportPacketYaml,
a.createPluginsFile,
a.createSanitizedConfigFile,
a.getMattermostLog,
a.getNotificationsLog,
}
for _, fn := range functions {
fileData, warning := fn()
if fileData != nil {
fileDatas = append(fileDatas, *fileData)
} else {
warnings = append(warnings, warning)
}
}
// Adding a warning.txt file to the fileDatas if any warning
if len(warnings) > 0 {
finalWarning := strings.Join(warnings, "\n")
fileDatas = append(fileDatas, model.FileData{
Filename: "warning.txt",
Body: []byte(finalWarning),
})
}
return fileDatas
}
func (a *App) generateSupportPacketYaml() (*model.FileData, string) {
// Here we are getting information regarding Elastic Search
var elasticServerVersion string
var elasticServerPlugins []string
if a.Srv().Platform().SearchEngine.ElasticsearchEngine != nil {
elasticServerVersion = a.Srv().Platform().SearchEngine.ElasticsearchEngine.GetFullVersion()
elasticServerPlugins = a.Srv().Platform().SearchEngine.ElasticsearchEngine.GetPlugins()
}
// Here we are getting information regarding LDAP
ldapInterface := a.ch.Ldap
var vendorName, vendorVersion string
if ldapInterface != nil {
vendorName, vendorVersion = ldapInterface.GetVendorNameAndVendorVersion()
}
// Here we are getting information regarding the database (mysql/postgres + current schema version)
databaseType, databaseSchemaVersion := a.Srv().DatabaseTypeAndSchemaVersion()
databaseVersion, _ := a.Srv().Store().GetDbVersion(false)
uniqueUserCount, err := a.Srv().Store().User().Count(model.UserCountOptions{})
if err != nil {
return nil, errors.Wrap(err, "error while getting user count").Error()
}
analytics, err := a.GetAnalytics("standard", "")
if analytics == nil {
return nil, errors.Wrap(err, "error while getting analytics").Error()
}
elasticPostIndexing, _ := a.Srv().Store().Job().GetAllByTypePage("elasticsearch_post_indexing", 0, 2)
elasticPostAggregation, _ := a.Srv().Store().Job().GetAllByTypePage("elasticsearch_post_aggregation", 0, 2)
ldapSyncJobs, _ := a.Srv().Store().Job().GetAllByTypePage("ldap_sync", 0, 2)
messageExport, _ := a.Srv().Store().Job().GetAllByTypePage("message_export", 0, 2)
dataRetentionJobs, _ := a.Srv().Store().Job().GetAllByTypePage("data_retention", 0, 2)
complianceJobs, _ := a.Srv().Store().Job().GetAllByTypePage("compliance", 0, 2)
migrationJobs, _ := a.Srv().Store().Job().GetAllByTypePage("migrations", 0, 2)
licenseTo := ""
supportedUsers := 0
if license := a.Srv().License(); license != nil {
supportedUsers = *license.Features.Users
licenseTo = license.Customer.Company
}
// Creating the struct for support packet yaml file
supportPacket := model.SupportPacket{
LicenseTo: licenseTo,
ServerOS: runtime.GOOS,
ServerArchitecture: runtime.GOARCH,
ServerVersion: model.CurrentVersion,
BuildHash: model.BuildHash,
DatabaseType: databaseType,
DatabaseVersion: databaseVersion,
DatabaseSchemaVersion: databaseSchemaVersion,
LdapVendorName: vendorName,
LdapVendorVersion: vendorVersion,
ElasticServerVersion: elasticServerVersion,
ElasticServerPlugins: elasticServerPlugins,
ActiveUsers: int(uniqueUserCount),
LicenseSupportedUsers: supportedUsers,
TotalChannels: int(analytics[0].Value) + int(analytics[1].Value),
TotalPosts: int(analytics[2].Value),
TotalTeams: int(analytics[4].Value),
WebsocketConnections: int(analytics[5].Value),
MasterDbConnections: int(analytics[6].Value),
ReplicaDbConnections: int(analytics[7].Value),
DailyActiveUsers: int(analytics[8].Value),
MonthlyActiveUsers: int(analytics[9].Value),
InactiveUserCount: int(analytics[10].Value),
ElasticPostIndexingJobs: elasticPostIndexing,
ElasticPostAggregationJobs: elasticPostAggregation,
LdapSyncJobs: ldapSyncJobs,
MessageExportJobs: messageExport,
DataRetentionJobs: dataRetentionJobs,
ComplianceJobs: complianceJobs,
MigrationJobs: migrationJobs,
}
// Marshal to a Yaml File
supportPacketYaml, err := yaml.Marshal(&supportPacket)
if err == nil {
fileData := model.FileData{
Filename: "support_packet.yaml",
Body: supportPacketYaml,
}
return &fileData, ""
}
warning := fmt.Sprintf("yaml.Marshal(&supportPacket) Error: %s", err.Error())
return nil, warning
}
func (a *App) createPluginsFile() (*model.FileData, string) {
var warning string
// Getting the plugins installed on the server, prettify it, and then add them to the file data array
pluginsResponse, appErr := a.GetPlugins()
if appErr == nil {
pluginsPrettyJSON, err := json.MarshalIndent(pluginsResponse, "", " ")
if err == nil {
fileData := model.FileData{
Filename: "plugins.json",
Body: pluginsPrettyJSON,
}
return &fileData, ""
}
warning = fmt.Sprintf("json.MarshalIndent(pluginsResponse) Error: %s", err.Error())
} else {
warning = fmt.Sprintf("c.App.GetPlugins() Error: %s", appErr.Error())
}
return nil, warning
}
func (a *App) getNotificationsLog() (*model.FileData, string) {
var warning string
// Getting notifications.log
if *a.Config().NotificationLogSettings.EnableFile {
// notifications.log
notificationsLog := config.GetNotificationsLogFileLocation(*a.Config().LogSettings.FileLocation)
notificationsLogFileData, notificationsLogFileDataErr := os.ReadFile(notificationsLog)
if notificationsLogFileDataErr == nil {
fileData := model.FileData{
Filename: "notifications.log",
Body: notificationsLogFileData,
}
return &fileData, ""
}
warning = fmt.Sprintf("os.ReadFile(notificationsLog) Error: %s", notificationsLogFileDataErr.Error())
} else {
warning = "Unable to retrieve notifications.log because LogSettings: EnableFile is false in config.json"
}
return nil, warning
}
func (a *App) getMattermostLog() (*model.FileData, string) {
var warning string
// Getting mattermost.log
if *a.Config().LogSettings.EnableFile {
// mattermost.log
mattermostLog := config.GetLogFileLocation(*a.Config().LogSettings.FileLocation)
mattermostLogFileData, mattermostLogFileDataErr := os.ReadFile(mattermostLog)
if mattermostLogFileDataErr == nil {
fileData := model.FileData{
Filename: "mattermost.log",
Body: mattermostLogFileData,
}
return &fileData, ""
}
warning = fmt.Sprintf("os.ReadFile(mattermostLog) Error: %s", mattermostLogFileDataErr.Error())
} else {
warning = "Unable to retrieve mattermost.log because LogSettings: EnableFile is false in config.json"
}
return nil, warning
}
func (a *App) createSanitizedConfigFile() (*model.FileData, string) {
// Getting sanitized config, prettifying it, and then adding it to our file data array
sanitizedConfigPrettyJSON, err := json.MarshalIndent(a.GetSanitizedConfig(), "", " ")
if err == nil {
fileData := model.FileData{
Filename: "sanitized_config.json",
Body: sanitizedConfigPrettyJSON,
}
return &fileData, ""
}
warning := fmt.Sprintf("json.MarshalIndent(c.App.GetSanitizedConfig()) Error: %s", err.Error())
return nil, warning
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"fmt"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// createDefaultChannelMemberships adds users to channels based on their group memberships and how those groups are
// configured to sync with channels for group members on or after the given timestamp. If a channelID is given
// only that channel's members are created. If channelID is nil all channel memberships are created.
// If includeRemovedMembers is true, then channel members who left or were removed from the channel will
// be re-added; otherwise, they will not be re-added.
func (a *App) createDefaultChannelMemberships(c request.CTX, params model.CreateDefaultMembershipParams) error {
channelMembers, appErr := a.ChannelMembersToAdd(params.Since, params.ScopedChannelID, params.ReAddRemovedMembers)
if appErr != nil {
return appErr
}
for _, userChannel := range channelMembers {
if params.ScopedUserID != nil && *params.ScopedUserID != userChannel.UserID {
continue
}
channel, err := a.GetChannel(c, userChannel.ChannelID)
if err != nil {
return err
}
tmem, err := a.GetTeamMember(channel.TeamId, userChannel.UserID)
if err != nil && err.Id != "app.team.get_member.missing.app_error" {
return err
}
// First add user to team
if tmem == nil {
_, err = a.AddTeamMember(c, channel.TeamId, userChannel.UserID)
if err != nil {
if err.Id == "api.team.join_user_to_team.allowed_domains.app_error" {
c.Logger().Info("User not added to channel - the domain associated with the user is not in the list of allowed team domains",
mlog.String("user_id", userChannel.UserID),
mlog.String("channel_id", userChannel.ChannelID),
mlog.String("team_id", channel.TeamId),
)
continue
}
return err
}
c.Logger().Info("added teammember",
mlog.String("user_id", userChannel.UserID),
mlog.String("team_id", channel.TeamId),
)
}
_, err = a.AddChannelMember(c, userChannel.UserID, channel, ChannelMemberOpts{
SkipTeamMemberIntegrityCheck: true,
})
if err != nil {
if err.Id == "api.channel.add_user.to.channel.failed.deleted.app_error" {
c.Logger().Info("Not adding user to channel because they have already left the team",
mlog.String("user_id", userChannel.UserID),
mlog.String("channel_id", userChannel.ChannelID),
)
} else {
return err
}
}
c.Logger().Info("added channelmember",
mlog.String("user_id", userChannel.UserID),
mlog.String("channel_id", userChannel.ChannelID),
)
}
return nil
}
// createDefaultTeamMemberships adds users to teams based on their group memberships and how those groups are
// configured to sync with teams for group members on or after the given timestamp. If a teamID is given
// only that team's members are created. If teamID is nil all team memberships are created.
// If includeRemovedMembers is true, then team members who left or were removed from the team will
// be re-added; otherwise, they will not be re-added.
func (a *App) createDefaultTeamMemberships(c request.CTX, params model.CreateDefaultMembershipParams) error {
teamMembers, appErr := a.TeamMembersToAdd(params.Since, params.ScopedTeamID, params.ReAddRemovedMembers)
if appErr != nil {
return appErr
}
for _, userTeam := range teamMembers {
if params.ScopedUserID != nil && *params.ScopedUserID != userTeam.UserID {
continue
}
_, err := a.AddTeamMember(c, userTeam.TeamID, userTeam.UserID)
if err != nil {
if err.Id == "api.team.join_user_to_team.allowed_domains.app_error" {
c.Logger().Info("User not added to team - the domain associated with the user is not in the list of allowed team domains",
mlog.String("user_id", userTeam.UserID),
mlog.String("team_id", userTeam.TeamID),
)
continue
}
return err
}
c.Logger().Info("added teammember",
mlog.String("user_id", userTeam.UserID),
mlog.String("team_id", userTeam.TeamID),
)
}
return nil
}
// CreateDefaultMemberships adds users to teams and channels based on their group memberships and how those groups
// are configured to sync with teams and channels for group members on or after the given timestamp.
// If includeRemovedMembers is true, then members who left or were removed from a team/channel will
// be re-added; otherwise, they will not be re-added.
func (a *App) CreateDefaultMemberships(c *request.Context, params model.CreateDefaultMembershipParams) error {
err := a.createDefaultTeamMemberships(c, params)
if err != nil {
return err
}
err = a.createDefaultChannelMemberships(c, params)
if err != nil {
return err
}
return nil
}
// DeleteGroupConstrainedMemberships deletes team and channel memberships of users who aren't members of the allowed
// groups of all group-constrained teams and channels.
func (a *App) DeleteGroupConstrainedMemberships(c *request.Context) error {
err := a.deleteGroupConstrainedChannelMemberships(c, nil)
if err != nil {
return err
}
err = a.deleteGroupConstrainedTeamMemberships(c, nil)
if err != nil {
return err
}
return nil
}
// deleteGroupConstrainedTeamMemberships deletes team memberships of users who aren't members of the allowed
// groups of the given group-constrained team. If a teamID is given then the procedure is scoped to the given team,
// if teamID is nil then the procedure affects all teams.
func (a *App) deleteGroupConstrainedTeamMemberships(c request.CTX, teamID *string) error {
teamMembers, appErr := a.TeamMembersToRemove(teamID)
if appErr != nil {
return appErr
}
for _, userTeam := range teamMembers {
err := a.RemoveUserFromTeam(c, userTeam.TeamId, userTeam.UserId, "")
if err != nil {
return err
}
c.Logger().Info("removed teammember",
mlog.String("user_id", userTeam.UserId),
mlog.String("team_id", userTeam.TeamId),
)
}
return nil
}
// deleteGroupConstrainedChannelMemberships deletes channel memberships of users who aren't members of the allowed
// groups of the given group-constrained channel. If a channelID is given then the procedure is scoped to the given team,
// if channelID is nil then the procedure affects all teams.
func (a *App) deleteGroupConstrainedChannelMemberships(c request.CTX, channelID *string) error {
channelMembers, appErr := a.ChannelMembersToRemove(channelID)
if appErr != nil {
return appErr
}
for _, userChannel := range channelMembers {
channel, err := a.GetChannel(c, userChannel.ChannelId)
if err != nil {
return err
}
err = a.RemoveUserFromChannel(c, userChannel.UserId, "", channel)
if err != nil {
return err
}
a.Log().Info("removed channelmember",
mlog.String("user_id", userChannel.UserId),
mlog.String("channel_id", channel.Id),
)
}
return nil
}
// SyncSyncableRoles updates the SchemeAdmin field value of the given syncable's members based on the configuration of
// the member's group memberships and the configuration of those groups to the syncable. This method should only
// be invoked on group-synced (aka group-constrained) syncables.
func (a *App) SyncSyncableRoles(syncableID string, syncableType model.GroupSyncableType) *model.AppError {
permittedAdmins, err := a.Srv().Store().Group().PermittedSyncableAdmins(syncableID, syncableType)
if err != nil {
return model.NewAppError("SyncSyncableRoles", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.Log().Info(
fmt.Sprintf("Permitted admins for %s", syncableType),
mlog.String(strings.ToLower(fmt.Sprintf("%s_id", syncableType)), syncableID),
mlog.Any("permitted_admins", permittedAdmins),
)
switch syncableType {
case model.GroupSyncableTypeTeam:
nErr := a.Srv().Store().Team().UpdateMembersRole(syncableID, permittedAdmins)
if nErr != nil {
return model.NewAppError("App.SyncSyncableRoles", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return nil
case model.GroupSyncableTypeChannel:
nErr := a.Srv().Store().Channel().UpdateMembersRole(syncableID, permittedAdmins)
if nErr != nil {
return model.NewAppError("App.SyncSyncableRoles", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return nil
default:
return model.NewAppError("App.SyncSyncableRoles", "groups.unsupported_syncable_type", map[string]any{"Value": syncableType}, "", http.StatusInternalServerError)
}
}
// SyncRolesAndMembership updates the SchemeAdmin status and membership of all of the members of the given
// syncable.
func (a *App) SyncRolesAndMembership(c request.CTX, syncableID string, syncableType model.GroupSyncableType, includeRemovedMembers bool) {
a.SyncSyncableRoles(syncableID, syncableType)
lastJob, _ := a.Srv().Store().Job().GetNewestJobByStatusAndType(model.JobStatusSuccess, model.JobTypeLdapSync)
var since int64
if lastJob != nil {
since = lastJob.StartAt
}
params := model.CreateDefaultMembershipParams{Since: since, ReAddRemovedMembers: includeRemovedMembers}
switch syncableType {
case model.GroupSyncableTypeTeam:
params.ScopedTeamID = &syncableID
a.createDefaultTeamMemberships(c, params)
a.deleteGroupConstrainedTeamMemberships(c, &syncableID)
if err := a.ClearTeamMembersCache(syncableID); err != nil {
c.Logger().Warn("Error clearing team members cache", mlog.Err(err))
}
case model.GroupSyncableTypeChannel:
params.ScopedChannelID = &syncableID
a.createDefaultChannelMemberships(c, params)
a.deleteGroupConstrainedChannelMemberships(c, &syncableID)
if err := a.ClearChannelMembersCache(c, syncableID); err != nil {
c.Logger().Warn("Error clearing channel members cache", mlog.Err(err))
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"context"
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"image"
"io"
"mime/multipart"
"net/http"
"net/url"
"sort"
"strings"
fb_model "github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/email"
"github.com/mattermost/mattermost-server/v6/server/channels/app/imaging"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/app/teams"
"github.com/mattermost/mattermost-server/v6/server/channels/app/users"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// teamServiceWrapper provides an implementation of `product.TeamService` to be used by products.
type teamServiceWrapper struct {
app AppIface
}
func (w *teamServiceWrapper) GetMember(teamID, userID string) (*model.TeamMember, *model.AppError) {
return w.app.GetTeamMember(teamID, userID)
}
func (w *teamServiceWrapper) CreateMember(ctx *request.Context, teamID, userID string) (*model.TeamMember, *model.AppError) {
return w.app.AddTeamMember(ctx, teamID, userID)
}
func (w *teamServiceWrapper) GetGroup(groupID string) (*model.Group, *model.AppError) {
return w.app.GetGroup(groupID, nil, nil)
}
func (w *teamServiceWrapper) GetTeam(teamID string) (*model.Team, *model.AppError) {
return w.app.GetTeam(teamID)
}
func (w *teamServiceWrapper) GetGroupMemberUsers(groupID string, page, perPage int) ([]*model.User, *model.AppError) {
users, _, err := w.app.GetGroupMemberUsersPage(groupID, page, perPage, nil)
return users, err
}
// Ensure the wrapper implements the product service.
var _ product.TeamService = (*teamServiceWrapper)(nil)
func (a *App) AdjustTeamsFromProductLimits(teamLimits *model.TeamsLimits) *model.AppError {
maxActiveTeams := *teamLimits.Active
teams, appErr := a.GetAllTeams()
if appErr != nil {
return appErr
}
if teams == nil {
return nil
}
// Sort the list of teams based on their creation date
sort.Slice(teams, func(i, j int) bool {
return teams[i].CreateAt < teams[j].CreateAt
})
var activeTeams []*model.Team
var cloudArchivedTeams []*model.Team
for _, team := range teams {
if team.DeleteAt == 0 {
activeTeams = append(activeTeams, team)
}
if team.DeleteAt > 0 && team.CloudLimitsArchived {
cloudArchivedTeams = append(cloudArchivedTeams, team)
}
}
if len(activeTeams) > maxActiveTeams {
// If there are more active teams than allowed, we must archive them
// Remove the first n elements (where n is the allowed number of teams) so they aren't archived
teamsToArchive := activeTeams[maxActiveTeams:]
for _, team := range teamsToArchive {
cloudLimitsArchived := true
// Archive the remainder
patch := model.TeamPatch{CloudLimitsArchived: &cloudLimitsArchived}
_, err := a.PatchTeam(team.Id, &patch)
if err != nil {
return err
}
err = a.SoftDeleteTeam(team.Id)
if err != nil {
return err
}
}
} else if len(activeTeams) < maxActiveTeams && len(cloudArchivedTeams) > 0 {
// If the number of activeTeams is less than the allowed limit, and there are some cloudArchivedTeams, we can restore these cloudArchivedTeams
activeTeamsBeforeLimit := maxActiveTeams - len(activeTeams)
teamsToRestore := cloudArchivedTeams
// If the number of active teams remaining before the limit is hit is fewer than the number of cloudArchivedTeams, trim the list (still according to CreateAt)
// Otherwise, we can restore all of the cloudArchivedTeams without hitting the limit, so don't filter the list
if activeTeamsBeforeLimit < len(cloudArchivedTeams) {
teamsToRestore = cloudArchivedTeams[:(activeTeamsBeforeLimit)]
}
cloudLimitsArchived := false
patch := &model.TeamPatch{CloudLimitsArchived: &cloudLimitsArchived}
for _, team := range teamsToRestore {
err := a.RestoreTeam(team.Id)
if err != nil {
return err
}
_, err = a.PatchTeam(team.Id, patch)
if err != nil {
return err
}
}
}
return nil
}
func (a *App) SoftDeleteAllTeamsExcept(teamID string) *model.AppError {
teams, appErr := a.GetAllTeams()
if appErr != nil {
return appErr
}
if teams == nil {
return nil
}
cloudLimitsArchived := true
patch := &model.TeamPatch{CloudLimitsArchived: &cloudLimitsArchived}
for _, team := range teams {
if team.Id != teamID {
_, err := a.PatchTeam(team.Id, patch)
if err != nil {
return err
}
err = a.SoftDeleteTeam(team.Id)
if err != nil {
return err
}
}
}
return nil
}
// MM-48246 A/B test show linked boards
const preferenceName = "linked_board_created"
func (a *App) shouldCreateOnboardingLinkedBoard(c request.CTX, teamId string) bool {
ffEnabled := a.Config().FeatureFlags.OnboardingAutoShowLinkedBoard
if !ffEnabled {
return false
}
hasBoard, err := a.HasBoardProduct()
if err != nil {
a.Log().Error("error checking the existence of boards product: ", mlog.Err(err))
return false
}
if !hasBoard {
a.Log().Warn("board product not found")
return false
}
data, sysValErr := a.Srv().Store().System().GetByName(model.PreferenceOnboarding + "_" + preferenceName)
if sysValErr != nil {
var nfErr *store.ErrNotFound
if errors.As(sysValErr, &nfErr) { // if no board has been registered, it can create one for this team
return true
}
a.Log().Error("cannot get the system values", mlog.Err(sysValErr))
return false
}
// get the team list and check if the team value has been already stored, if so, no need to create a board in town square in that team
teamsList := strings.Split(data.Value, ",")
for _, team := range teamsList {
if team == teamId {
return false
}
}
return true
}
func (a *App) createOnboardingLinkedBoard(c request.CTX, teamId string) (*fb_model.Board, *model.AppError) {
const defaultTemplatesTeam = "0"
// see https://github.com/mattermost/mattermost-server/v6/server/boards/blob/main/server/services/store/sqlstore/board.go#L302
// and https://github.com/mattermost/mattermost-server/pull/22201#discussion_r1099536430
const defaultTemplateTitle = "Welcome to Boards!"
welcomeToBoardsTemplateId := fmt.Sprintf("%x", md5.Sum([]byte(defaultTemplateTitle)))
userId := c.Session().UserId
boardServiceItf, ok := a.Srv().services[product.BoardsKey]
if !ok {
return nil, model.NewAppError("CreateBoard", "app.team.create_onboarding_linked_board.product_key_not_found", nil, "", http.StatusBadRequest)
}
boardService, typeOk := boardServiceItf.(product.BoardsService)
if !typeOk {
// boardServiceItf is NOT of type product.BoardsService
return nil, model.NewAppError("CreateBoard", "app.team.create_onboarding_linked_board.itf_not_of_type", nil, "", http.StatusBadRequest)
}
templates, err := boardService.GetTemplates(defaultTemplatesTeam, userId)
if err != nil {
return nil, model.NewAppError("CreateBoard", "app.team.create_onboarding_linked_board.error_getting_templates", nil, "", http.StatusBadRequest).Wrap(err)
}
channel, appErr := a.GetChannelByName(c, model.DefaultChannelName, teamId, false)
if appErr != nil {
return nil, appErr
}
var template *fb_model.Board = nil
for _, t := range templates {
v := t.Properties["trackingTemplateId"]
if v == welcomeToBoardsTemplateId {
template = t
break
}
}
if template == nil && len(templates) > 0 {
template = templates[0]
}
// Duplicate board From template
boardsAndBlocks, _, err := boardService.DuplicateBoard(template.ID, userId, teamId, false)
if err != nil {
return nil, model.NewAppError("CreateBoard", "app.team.create_onboarding_linked_board.error_duplicating_board", nil, "", http.StatusBadRequest).Wrap(err)
}
if len(boardsAndBlocks.Boards) != 1 {
return nil, model.NewAppError("CreateBoard", "app.team.create_onboarding_linked_board.error_no_board", nil, "", http.StatusBadRequest).Wrap(err)
}
// link the board with the channel
patchedBoard, err := boardService.PatchBoard(&fb_model.BoardPatch{
ChannelID: &channel.Id,
}, boardsAndBlocks.Boards[0].ID, userId)
if err != nil && patchedBoard == nil {
return nil, model.NewAppError("CreateBoard", "app.team.create_onboarding_linked_board.error_patching_board", nil, "", http.StatusBadRequest).Wrap(err)
}
// Save in the system preferences that the board was already created once per team
data, sysValErr := a.Srv().Store().System().GetByName(model.PreferenceOnboarding + "_" + preferenceName)
if sysValErr != nil {
c.Logger().Error("cannot get the system preferences", mlog.Err(sysValErr))
}
teamsList := teamId
// if data is not nil, data.Value contains the list of teams where the A/B test has alredy created a channel for town square
if data != nil {
teamsList = data.Value + "," + teamId
}
if err := a.Srv().Store().System().SaveOrUpdate(&model.System{
Name: model.PreferenceOnboarding + "_" + preferenceName,
Value: teamsList,
}); err != nil {
c.Logger().Warn("encountered error saving user preferences", mlog.Err(err))
}
return patchedBoard, nil
}
func (a *App) CreateTeam(c request.CTX, team *model.Team) (*model.Team, *model.AppError) {
rteam, err := a.ch.srv.teamService.CreateTeam(team)
if err != nil {
var invErr *store.ErrInvalidInput
var cErr *store.ErrConflict
var ltErr *store.ErrLimitExceeded
var appErr *model.AppError
switch {
case errors.As(err, &invErr):
switch {
case invErr.Entity == "Channel" && invErr.Field == "DeleteAt":
return nil, model.NewAppError("CreateTeam", "store.sql_channel.save.archived_channel.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case invErr.Entity == "Channel" && invErr.Field == "Type":
return nil, model.NewAppError("CreateTeam", "store.sql_channel.save.direct_channel.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case invErr.Entity == "Channel" && invErr.Field == "Id":
return nil, model.NewAppError("CreateTeam", "store.sql_channel.save_channel.existing.app_error", nil, "id="+invErr.Value.(string), http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("CreateTeam", "app.team.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
case errors.As(err, &cErr):
return nil, model.NewAppError("CreateTeam", store.ChannelExistsError, nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, <Err):
return nil, model.NewAppError("CreateTeam", "store.sql_channel.save_channel.limit.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CreateTeam", "app.team.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// MM-48246 A/B test show linked boards. Create a welcome to boards linked board per user
if a.shouldCreateOnboardingLinkedBoard(c, team.Id) {
board, aErr := a.createOnboardingLinkedBoard(c, team.Id)
if aErr != nil || board == nil {
a.Log().Warn("Error creating the linked board, only team created", mlog.Err(err))
return rteam, nil
}
if board.ID != "" {
logInfo := fmt.Sprintf("Board created with id %s and associated to channel %s in team %s", board.ID, board.ChannelID, team.Id)
a.Log().Info(logInfo, mlog.Err(err))
}
}
return rteam, nil
}
func (a *App) CreateTeamWithUser(c *request.Context, team *model.Team, userID string) (*model.Team, *model.AppError) {
user, err := a.GetUser(userID)
if err != nil {
return nil, err
}
team.Email = user.Email
if !a.ch.srv.teamService.IsTeamEmailAllowed(user, team) {
return nil, model.NewAppError("CreateTeamWithUser", "api.team.is_team_creation_allowed.domain.app_error", nil, "", http.StatusBadRequest)
}
rteam, err := a.CreateTeam(c, team)
if err != nil {
return nil, err
}
if _, err := a.JoinUserToTeam(c, rteam, user, ""); err != nil {
return nil, err
}
return rteam, nil
}
func (a *App) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
oldTeam, err := a.ch.srv.teamService.UpdateTeam(team, teams.UpdateOptions{Sanitized: true})
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
var domErr *teams.DomainError
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("UpdateTeam", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &invErr):
return nil, model.NewAppError("UpdateTeam", "app.team.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &domErr):
return nil, model.NewAppError("UpdateTeam", "api.team.update_restricted_domains.mismatch.app_error", map[string]any{"Domain": domErr.Domain}, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpdateTeam", "app.team.update.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if appErr := a.sendTeamEvent(oldTeam, model.WebsocketEventUpdateTeam); appErr != nil {
return nil, appErr
}
return oldTeam, nil
}
// RenameTeam is used to rename the team Name and the DisplayName fields
func (a *App) RenameTeam(team *model.Team, newTeamName string, newDisplayName string) (*model.Team, *model.AppError) {
// check if name is occupied
_, errnf := a.GetTeamByName(newTeamName)
// "-" can be used as a newTeamName if only DisplayName change is wanted
if errnf == nil && newTeamName != "-" {
errbody := fmt.Sprintf("team with name %s already exists", newTeamName)
return nil, model.NewAppError("RenameTeam", "app.team.rename_team.name_occupied", nil, errbody, http.StatusBadRequest)
}
if newTeamName != "-" {
team.Name = newTeamName
}
if newDisplayName != "" {
team.DisplayName = newDisplayName
}
newTeam, err := a.ch.srv.teamService.UpdateTeam(team, teams.UpdateOptions{})
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
var domErr *teams.DomainError
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("RenameTeam", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &invErr):
return nil, model.NewAppError("RenameTeam", "app.team.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &domErr):
return nil, model.NewAppError("RenameTeam", "api.team.update_restricted_domains.mismatch.app_error", map[string]any{"Domain": domErr.Domain}, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("RenameTeam", "app.team.update.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return newTeam, nil
}
func (a *App) UpdateTeamScheme(team *model.Team) (*model.Team, *model.AppError) {
oldTeam, err := a.GetTeam(team.Id)
if err != nil {
return nil, err
}
oldTeam.SchemeId = team.SchemeId
oldTeam, nErr := a.Srv().Store().Team().Update(oldTeam)
if nErr != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(nErr, &invErr):
return nil, model.NewAppError("UpdateTeamScheme", "app.team.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("UpdateTeamScheme", "app.team.update.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
nErr = a.ClearTeamMembersCache(team.Id)
if nErr != nil {
return nil, model.NewAppError("UpdateTeamScheme", "app.team.clear_cache.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
a.Srv().Store().Channel().ClearMembersForUserCache()
if appErr := a.sendTeamEvent(oldTeam, model.WebsocketEventUpdateTeamScheme); appErr != nil {
return nil, appErr
}
return oldTeam, nil
}
func (a *App) UpdateTeamPrivacy(teamID string, teamType string, allowOpenInvite bool) *model.AppError {
oldTeam, err := a.GetTeam(teamID)
if err != nil {
return err
}
// Force a regeneration of the invite token if changing a team to restricted.
if (allowOpenInvite != oldTeam.AllowOpenInvite || teamType != oldTeam.Type) && (!allowOpenInvite || teamType == model.TeamInvite) {
oldTeam.InviteId = model.NewId()
}
oldTeam.Type = teamType
oldTeam.AllowOpenInvite = allowOpenInvite
oldTeam, nErr := a.Srv().Store().Team().Update(oldTeam)
if nErr != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(nErr, &invErr):
return model.NewAppError("UpdateTeamPrivacy", "app.team.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &appErr):
return appErr
default:
return model.NewAppError("UpdateTeamPrivacy", "app.team.update.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if appErr := a.sendTeamEvent(oldTeam, model.WebsocketEventUpdateTeam); appErr != nil {
return appErr
}
return nil
}
func (a *App) PatchTeam(teamID string, patch *model.TeamPatch) (*model.Team, *model.AppError) {
team, err := a.ch.srv.teamService.PatchTeam(teamID, patch)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
var domErr *teams.DomainError
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("PatchTeam", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &invErr):
return nil, model.NewAppError("PatchTeam", "app.team.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &domErr):
return nil, model.NewAppError("PatchTeam", "api.team.update_restricted_domains.mismatch.app_error", map[string]any{"Domain": domErr.Domain}, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("PatchTeam", "app.team.update.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if appErr := a.sendTeamEvent(team, model.WebsocketEventUpdateTeam); appErr != nil {
return nil, appErr
}
return team, nil
}
func (a *App) RegenerateTeamInviteId(teamID string) (*model.Team, *model.AppError) {
team, err := a.GetTeam(teamID)
if err != nil {
return nil, err
}
team.InviteId = model.NewId()
updatedTeam, nErr := a.Srv().Store().Team().Update(team)
if nErr != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(nErr, &invErr):
return nil, model.NewAppError("RegenerateTeamInviteId", "app.team.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("RegenerateTeamInviteId", "app.team.update.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if appErr := a.sendTeamEvent(updatedTeam, model.WebsocketEventUpdateTeam); appErr != nil {
return nil, appErr
}
return updatedTeam, nil
}
func (a *App) sendTeamEvent(team *model.Team, event string) *model.AppError {
sanitizedTeam := &model.Team{}
*sanitizedTeam = *team
sanitizedTeam.Sanitize()
message := model.NewWebSocketEvent(event, sanitizedTeam.Id, "", "", nil, "")
teamJSON, jsonErr := json.Marshal(sanitizedTeam)
if jsonErr != nil {
return model.NewAppError("sendTeamEvent", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("team", string(teamJSON))
a.Publish(message)
return nil
}
func (a *App) GetSchemeRolesForTeam(teamID string) (string, string, string, *model.AppError) {
team, err := a.GetTeam(teamID)
if err != nil {
return "", "", "", err
}
if team.SchemeId != nil && *team.SchemeId != "" {
scheme, err := a.GetScheme(*team.SchemeId)
if err != nil {
return "", "", "", err
}
return scheme.DefaultTeamGuestRole, scheme.DefaultTeamUserRole, scheme.DefaultTeamAdminRole, nil
}
return model.TeamGuestRoleId, model.TeamUserRoleId, model.TeamAdminRoleId, nil
}
func (a *App) UpdateTeamMemberRoles(teamID string, userID string, newRoles string) (*model.TeamMember, *model.AppError) {
member, nErr := a.Srv().Store().Team().GetMember(context.Background(), teamID, userID)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("UpdateTeamMemberRoles", "app.team.get_member.missing.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("UpdateTeamMemberRoles", "app.team.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if member == nil {
return nil, model.NewAppError("UpdateTeamMemberRoles", "api.team.update_member_roles.not_a_member", nil, "userId="+userID+" teamId="+teamID, http.StatusBadRequest)
}
schemeGuestRole, schemeUserRole, schemeAdminRole, err := a.GetSchemeRolesForTeam(teamID)
if err != nil {
return nil, err
}
prevSchemeGuestValue := member.SchemeGuest
var newExplicitRoles []string
member.SchemeGuest = false
member.SchemeUser = false
member.SchemeAdmin = false
for _, roleName := range strings.Fields(newRoles) {
var role *model.Role
role, err = a.GetRoleByName(context.Background(), roleName)
if err != nil {
err.StatusCode = http.StatusBadRequest
return nil, err
}
if !role.SchemeManaged {
// The role is not scheme-managed, so it's OK to apply it to the explicit roles field.
newExplicitRoles = append(newExplicitRoles, roleName)
} else {
// The role is scheme-managed, so need to check if it is part of the scheme for this channel or not.
switch roleName {
case schemeAdminRole:
member.SchemeAdmin = true
case schemeUserRole:
member.SchemeUser = true
case schemeGuestRole:
member.SchemeGuest = true
default:
// If not part of the scheme for this team, then it is not allowed to apply it as an explicit role.
return nil, model.NewAppError("UpdateTeamMemberRoles", "api.channel.update_team_member_roles.scheme_role.app_error", nil, "role_name="+roleName, http.StatusBadRequest)
}
}
}
if member.SchemeGuest && member.SchemeUser {
return nil, model.NewAppError("UpdateTeamMemberRoles", "api.team.update_team_member_roles.guest_and_user.app_error", nil, "", http.StatusBadRequest)
}
if prevSchemeGuestValue != member.SchemeGuest {
return nil, model.NewAppError("UpdateTeamMemberRoles", "api.channel.update_team_member_roles.changing_guest_role.app_error", nil, "", http.StatusBadRequest)
}
member.ExplicitRoles = strings.Join(newExplicitRoles, " ")
member, nErr = a.Srv().Store().Team().UpdateMember(member)
if nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("UpdateTeamMemberRoles", "app.team.save_member.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
a.ClearSessionCacheForUser(userID)
if appErr := a.sendUpdatedMemberRoleEvent(userID, member); appErr != nil {
return nil, appErr
}
return member, nil
}
func (a *App) UpdateTeamMemberSchemeRoles(teamID string, userID string, isSchemeGuest bool, isSchemeUser bool, isSchemeAdmin bool) (*model.TeamMember, *model.AppError) {
member, err := a.GetTeamMember(teamID, userID)
if err != nil {
return nil, err
}
member.SchemeAdmin = isSchemeAdmin
member.SchemeUser = isSchemeUser
member.SchemeGuest = isSchemeGuest
if member.SchemeUser && member.SchemeGuest {
return nil, model.NewAppError("UpdateTeamMemberSchemeRoles", "api.team.update_team_member_roles.guest_and_user.app_error", nil, "", http.StatusBadRequest)
}
// If the migration is not completed, we also need to check the default team_admin/team_user roles are not present in the roles field.
if err = a.IsPhase2MigrationCompleted(); err != nil {
member.ExplicitRoles = RemoveRoles([]string{model.TeamGuestRoleId, model.TeamUserRoleId, model.TeamAdminRoleId}, member.ExplicitRoles)
}
member, nErr := a.Srv().Store().Team().UpdateMember(member)
if nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("UpdateTeamMemberSchemeRoles", "app.team.save_member.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
a.ClearSessionCacheForUser(userID)
if appErr := a.sendUpdatedMemberRoleEvent(userID, member); appErr != nil {
return nil, appErr
}
return member, nil
}
func (a *App) sendUpdatedMemberRoleEvent(userID string, member *model.TeamMember) *model.AppError {
message := model.NewWebSocketEvent(model.WebsocketEventMemberroleUpdated, "", "", userID, nil, "")
tmJSON, jsonErr := json.Marshal(member)
if jsonErr != nil {
return model.NewAppError("sendUpdatedMemberRoleEvent", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("member", string(tmJSON))
a.Publish(message)
return nil
}
func (a *App) AddUserToTeam(c request.CTX, teamID string, userID string, userRequestorId string) (*model.Team, *model.TeamMember, *model.AppError) {
tchan := make(chan store.StoreResult, 1)
go func() {
team, err := a.Srv().Store().Team().Get(teamID)
tchan <- store.StoreResult{Data: team, NErr: err}
close(tchan)
}()
uchan := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), userID)
uchan <- store.StoreResult{Data: user, NErr: err}
close(uchan)
}()
result := <-tchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, nil, model.NewAppError("AddUserToTeam", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, nil, model.NewAppError("AddUserToTeam", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
team := result.Data.(*model.Team)
result = <-uchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, nil, model.NewAppError("AddUserToTeam", MissingAccountError, nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, nil, model.NewAppError("AddUserToTeam", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
user := result.Data.(*model.User)
teamMember, err := a.JoinUserToTeam(c, team, user, userRequestorId)
if err != nil {
return nil, nil, err
}
return team, teamMember, nil
}
func (a *App) AddUserToTeamByTeamId(c *request.Context, teamID string, user *model.User) *model.AppError {
team, err := a.Srv().Store().Team().Get(teamID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return model.NewAppError("AddUserToTeamByTeamId", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return model.NewAppError("AddUserToTeamByTeamId", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if _, err := a.JoinUserToTeam(c, team, user, ""); err != nil {
return err
}
return nil
}
func (a *App) AddUserToTeamByToken(c *request.Context, userID string, tokenID string) (*model.Team, *model.TeamMember, *model.AppError) {
token, err := a.Srv().Store().Token().GetByToken(tokenID)
if err != nil {
return nil, nil, model.NewAppError("AddUserToTeamByToken", "api.user.create_user.signup_link_invalid.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if token.Type != TokenTypeTeamInvitation && token.Type != TokenTypeGuestInvitation {
return nil, nil, model.NewAppError("AddUserToTeamByToken", "api.user.create_user.signup_link_invalid.app_error", nil, "", http.StatusBadRequest)
}
if model.GetMillis()-token.CreateAt >= InvitationExpiryTime {
a.DeleteToken(token)
return nil, nil, model.NewAppError("AddUserToTeamByToken", "api.user.create_user.signup_link_expired.app_error", nil, "", http.StatusBadRequest)
}
tokenData := model.MapFromJSON(strings.NewReader(token.Extra))
tchan := make(chan store.StoreResult, 1)
go func() {
team, err := a.Srv().Store().Team().Get(tokenData["teamId"])
tchan <- store.StoreResult{Data: team, NErr: err}
close(tchan)
}()
uchan := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), userID)
uchan <- store.StoreResult{Data: user, NErr: err}
close(uchan)
}()
result := <-tchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, nil, model.NewAppError("AddUserToTeamByToken", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, nil, model.NewAppError("AddUserToTeamByToken", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
team := result.Data.(*model.Team)
if team.IsGroupConstrained() {
return nil, nil, model.NewAppError("AddUserToTeamByToken", "app.team.invite_token.group_constrained.error", nil, "", http.StatusForbidden)
}
result = <-uchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, nil, model.NewAppError("AddUserToTeamByToken", MissingAccountError, nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, nil, model.NewAppError("AddUserToTeamByToken", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
user := result.Data.(*model.User)
if user.IsGuest() && token.Type == TokenTypeTeamInvitation {
return nil, nil, model.NewAppError("AddUserToTeamByToken", "api.user.create_user.invalid_invitation_type.app_error", nil, "", http.StatusBadRequest)
}
if !user.IsGuest() && token.Type == TokenTypeGuestInvitation {
return nil, nil, model.NewAppError("AddUserToTeamByToken", "api.user.create_user.invalid_invitation_type.app_error", nil, "", http.StatusBadRequest)
}
teamMember, appErr := a.JoinUserToTeam(c, team, user, "")
if appErr != nil {
return nil, nil, appErr
}
if token.Type == TokenTypeGuestInvitation {
channels, err := a.Srv().Store().Channel().GetChannelsByIds(strings.Split(tokenData["channels"], " "), false)
if err != nil {
return nil, nil, model.NewAppError("AddUserToTeamByToken", "app.channel.get_channels_by_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, channel := range channels {
_, err := a.AddUserToChannel(c, user, channel, false)
if err != nil {
mlog.Warn("Error adding user to channel", mlog.Err(err))
}
}
}
if err := a.DeleteToken(token); err != nil {
mlog.Warn("Error while deleting token", mlog.Err(err))
}
return team, teamMember, nil
}
func (a *App) AddUserToTeamByInviteId(c *request.Context, inviteId string, userID string) (*model.Team, *model.TeamMember, *model.AppError) {
tchan := make(chan store.StoreResult, 1)
go func() {
team, err := a.Srv().Store().Team().GetByInviteId(inviteId)
tchan <- store.StoreResult{Data: team, NErr: err}
close(tchan)
}()
uchan := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), userID)
uchan <- store.StoreResult{Data: user, NErr: err}
close(uchan)
}()
result := <-tchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, nil, model.NewAppError("AddUserToTeamByInviteId", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, nil, model.NewAppError("AddUserToTeamByInviteId", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
team := result.Data.(*model.Team)
result = <-uchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, nil, model.NewAppError("AddUserToTeamByInviteId", MissingAccountError, nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, nil, model.NewAppError("AddUserToTeamByInviteId", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
user := result.Data.(*model.User)
teamMember, err := a.JoinUserToTeam(c, team, user, "")
if err != nil {
return nil, nil, err
}
return team, teamMember, nil
}
func (a *App) JoinUserToTeam(c request.CTX, team *model.Team, user *model.User, userRequestorId string) (*model.TeamMember, *model.AppError) {
teamMember, alreadyAdded, err := a.ch.srv.teamService.JoinUserToTeam(team, user)
if err != nil {
var appErr *model.AppError
var conflictErr *store.ErrConflict
var limitExceededErr *store.ErrLimitExceeded
switch {
case errors.Is(err, teams.AcceptedDomainError):
return nil, model.NewAppError("JoinUserToTeam", "api.team.join_user_to_team.allowed_domains.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.Is(err, teams.MemberCountError):
return nil, model.NewAppError("JoinUserToTeam", "app.team.get_active_member_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
case errors.Is(err, teams.MaxMemberCountError):
return nil, model.NewAppError("JoinUserToTeam", "app.team.join_user_to_team.max_accounts.app_error", nil, "teamId="+team.Id, http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr): // in case we haven't converted to plain error.
return nil, appErr
case errors.As(err, &conflictErr):
return nil, model.NewAppError("JoinUserToTeam", "app.team.join_user_to_team.save_member.conflict.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &limitExceededErr):
return nil, model.NewAppError("JoinUserToTeam", "app.team.join_user_to_team.save_member.max_accounts.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("JoinUserToTeam", "app.team.join_user_to_team.save_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if alreadyAdded {
return teamMember, nil
}
if _, err := a.Srv().Store().User().UpdateUpdateAt(user.Id); err != nil {
return nil, model.NewAppError("JoinUserToTeam", "app.user.update_update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
if _, err := a.createInitialSidebarCategories(user.Id, opts); err != nil {
mlog.Warn(
"Encountered an issue creating default sidebar categories.",
mlog.String("user_id", user.Id),
mlog.String("team_id", team.Id),
mlog.Err(err),
)
}
shouldBeAdmin := team.Email == user.Email
if !user.IsGuest() {
// Soft error if there is an issue joining the default channels
if err := a.JoinDefaultChannels(c, team.Id, user, shouldBeAdmin, userRequestorId); err != nil {
mlog.Warn(
"Encountered an issue joining default channels.",
mlog.String("user_id", user.Id),
mlog.String("team_id", team.Id),
mlog.Err(err),
)
}
}
a.ClearSessionCacheForUser(user.Id)
a.InvalidateCacheForUser(user.Id)
a.invalidateCacheForUserTeams(user.Id)
var actor *model.User
if userRequestorId != "" {
actor, _ = a.GetUser(userRequestorId)
}
a.Srv().Go(func() {
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.UserHasJoinedTeam(pluginContext, teamMember, actor)
return true
}, plugin.UserHasJoinedTeamID)
})
message := model.NewWebSocketEvent(model.WebsocketEventAddedToTeam, "", "", user.Id, nil, "")
message.Add("team_id", team.Id)
message.Add("user_id", user.Id)
a.Publish(message)
return teamMember, nil
}
func (a *App) GetTeam(teamID string) (*model.Team, *model.AppError) {
team, err := a.ch.srv.teamService.GetTeam(teamID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetTeam", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetTeam", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return team, nil
}
func (a *App) GetTeams(teamIDs []string) ([]*model.Team, *model.AppError) {
teams, err := a.ch.srv.teamService.GetTeams(teamIDs)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetTeam", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetTeam", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return teams, nil
}
func (a *App) GetTeamByName(name string) (*model.Team, *model.AppError) {
team, err := a.Srv().Store().Team().GetByName(name)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetTeamByName", "app.team.get_by_name.missing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetTeamByName", "app.team.get_by_name.app_error", nil, "", http.StatusNotFound).Wrap(err)
}
}
return team, nil
}
func (a *App) GetTeamByInviteId(inviteId string) (*model.Team, *model.AppError) {
team, err := a.Srv().Store().Team().GetByInviteId(inviteId)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetTeamByInviteId", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetTeamByInviteId", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return team, nil
}
func (a *App) GetAllTeams() ([]*model.Team, *model.AppError) {
teams, err := a.Srv().Store().Team().GetAll()
if err != nil {
return nil, model.NewAppError("GetAllTeams", "app.team.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teams, nil
}
func (a *App) GetAllTeamsPage(offset int, limit int, opts *model.TeamSearch) ([]*model.Team, *model.AppError) {
teams, err := a.Srv().Store().Team().GetAllPage(offset, limit, opts)
if err != nil {
return nil, model.NewAppError("GetAllTeamsPage", "app.team.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teams, nil
}
func (a *App) GetAllTeamsPageWithCount(offset int, limit int, opts *model.TeamSearch) (*model.TeamsWithCount, *model.AppError) {
totalCount, err := a.Srv().Store().Team().AnalyticsTeamCount(opts)
if err != nil {
return nil, model.NewAppError("GetAllTeamsPageWithCount", "app.team.analytics_team_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
teams, err := a.Srv().Store().Team().GetAllPage(offset, limit, opts)
if err != nil {
return nil, model.NewAppError("GetAllTeamsPageWithCount", "app.team.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &model.TeamsWithCount{Teams: teams, TotalCount: totalCount}, nil
}
func (a *App) GetAllPrivateTeams() ([]*model.Team, *model.AppError) {
teams, err := a.Srv().Store().Team().GetAllPrivateTeamListing()
if err != nil {
return nil, model.NewAppError("GetAllPrivateTeams", "app.team.get_all_private_team_listing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teams, nil
}
func (a *App) GetAllPublicTeams() ([]*model.Team, *model.AppError) {
teams, err := a.Srv().Store().Team().GetAllTeamListing()
if err != nil {
return nil, model.NewAppError("GetAllPublicTeams", "app.team.get_all_team_listing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teams, nil
}
// SearchAllTeams returns a team list and the total count of the results
func (a *App) SearchAllTeams(searchOpts *model.TeamSearch) ([]*model.Team, int64, *model.AppError) {
if searchOpts.IsPaginated() {
teams, count, err := a.Srv().Store().Team().SearchAllPaged(searchOpts)
if err != nil {
return nil, 0, model.NewAppError("SearchAllTeams", "app.team.search_all_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teams, count, nil
}
results, err := a.Srv().Store().Team().SearchAll(searchOpts)
if err != nil {
return nil, 0, model.NewAppError("SearchAllTeams", "app.team.search_all_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return results, int64(len(results)), nil
}
func (a *App) SearchPublicTeams(searchOpts *model.TeamSearch) ([]*model.Team, *model.AppError) {
teams, err := a.Srv().Store().Team().SearchOpen(searchOpts)
if err != nil {
return nil, model.NewAppError("SearchPublicTeams", "app.team.search_open_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teams, nil
}
func (a *App) SearchPrivateTeams(searchOpts *model.TeamSearch) ([]*model.Team, *model.AppError) {
teams, err := a.Srv().Store().Team().SearchPrivate(searchOpts)
if err != nil {
return nil, model.NewAppError("SearchPrivateTeams", "app.team.search_private_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teams, nil
}
func (a *App) GetTeamsForUser(userID string) ([]*model.Team, *model.AppError) {
teams, err := a.Srv().Store().Team().GetTeamsByUserId(userID)
if err != nil {
return nil, model.NewAppError("GetTeamsForUser", "app.team.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teams, nil
}
func (a *App) GetTeamMember(teamID, userID string) (*model.TeamMember, *model.AppError) {
teamMember, err := a.Srv().Store().Team().GetMember(sqlstore.WithMaster(context.Background()), teamID, userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetTeamMember", "app.team.get_member.missing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetTeamMember", "app.team.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return teamMember, nil
}
func (a *App) GetTeamMembersForUser(userID string, excludeTeamID string, includeDeleted bool) ([]*model.TeamMember, *model.AppError) {
teamMembers, err := a.Srv().Store().Team().GetTeamsForUser(context.Background(), userID, excludeTeamID, includeDeleted)
if err != nil {
return nil, model.NewAppError("GetTeamMembersForUser", "app.team.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teamMembers, nil
}
func (a *App) GetTeamMembersForUserWithPagination(userID string, page, perPage int) ([]*model.TeamMember, *model.AppError) {
teamMembers, err := a.Srv().Store().Team().GetTeamsForUserWithPagination(userID, page, perPage)
if err != nil {
return nil, model.NewAppError("GetTeamMembersForUserWithPagination", "app.team.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teamMembers, nil
}
func (a *App) GetTeamMembers(teamID string, offset int, limit int, teamMembersGetOptions *model.TeamMembersGetOptions) ([]*model.TeamMember, *model.AppError) {
teamMembers, err := a.Srv().Store().Team().GetMembers(teamID, offset, limit, teamMembersGetOptions)
if err != nil {
return nil, model.NewAppError("GetTeamMembers", "app.team.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teamMembers, nil
}
func (a *App) GetTeamMembersByIds(teamID string, userIDs []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, *model.AppError) {
teamMembers, err := a.Srv().Store().Team().GetMembersByIds(teamID, userIDs, restrictions)
if err != nil {
return nil, model.NewAppError("GetTeamMembersByIds", "app.team.get_members_by_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teamMembers, nil
}
func (a *App) GetCommonTeamIDsForTwoUsers(userID, otherUserID string) ([]string, *model.AppError) {
teamIDs, err := a.Srv().Store().Team().GetCommonTeamIDsForTwoUsers(userID, otherUserID)
if err != nil {
return nil, model.NewAppError("GetCommonTeamIDsForUsers", "app.team.get_common_team_ids_for_users.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teamIDs, nil
}
func (a *App) AddTeamMember(c request.CTX, teamID, userID string) (*model.TeamMember, *model.AppError) {
_, teamMember, err := a.AddUserToTeam(c, teamID, userID, "")
if err != nil {
return nil, err
}
message := model.NewWebSocketEvent(model.WebsocketEventAddedToTeam, "", "", userID, nil, "")
message.Add("team_id", teamID)
message.Add("user_id", userID)
a.Publish(message)
return teamMember, nil
}
func (a *App) AddTeamMembers(c *request.Context, teamID string, userIDs []string, userRequestorId string, graceful bool) ([]*model.TeamMemberWithError, *model.AppError) {
var membersWithErrors []*model.TeamMemberWithError
for _, userID := range userIDs {
_, teamMember, err := a.AddUserToTeam(c, teamID, userID, userRequestorId)
if err != nil {
if graceful {
membersWithErrors = append(membersWithErrors, &model.TeamMemberWithError{
UserId: userID,
Error: err,
})
continue
}
return nil, err
}
membersWithErrors = append(membersWithErrors, &model.TeamMemberWithError{
UserId: userID,
Member: teamMember,
})
message := model.NewWebSocketEvent(model.WebsocketEventAddedToTeam, "", "", userID, nil, "")
message.Add("team_id", teamID)
message.Add("user_id", userID)
a.Publish(message)
}
return membersWithErrors, nil
}
func (a *App) AddTeamMemberByToken(c *request.Context, userID, tokenID string) (*model.TeamMember, *model.AppError) {
_, teamMember, err := a.AddUserToTeamByToken(c, userID, tokenID)
if err != nil {
return nil, err
}
return teamMember, nil
}
func (a *App) AddTeamMemberByInviteId(c *request.Context, inviteId, userID string) (*model.TeamMember, *model.AppError) {
team, teamMember, err := a.AddUserToTeamByInviteId(c, inviteId, userID)
if err != nil {
return nil, err
}
if team.IsGroupConstrained() {
return nil, model.NewAppError("AddTeamMemberByInviteId", "app.team.invite_id.group_constrained.error", nil, "", http.StatusForbidden)
}
return teamMember, nil
}
func (a *App) GetTeamUnread(teamID, userID string) (*model.TeamUnread, *model.AppError) {
channelUnreads, err := a.Srv().Store().Team().GetChannelUnreadsForTeam(teamID, userID)
if err != nil {
return nil, model.NewAppError("GetTeamUnread", "app.team.get_unread.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
var teamUnread = &model.TeamUnread{
MsgCount: 0,
MentionCount: 0,
MentionCountRoot: 0,
MsgCountRoot: 0,
TeamId: teamID,
}
for _, cu := range channelUnreads {
teamUnread.MentionCount += cu.MentionCount
teamUnread.MentionCountRoot += cu.MentionCountRoot
if cu.NotifyProps[model.MarkUnreadNotifyProp] != model.ChannelMarkUnreadMention {
teamUnread.MsgCount += cu.MsgCount
teamUnread.MsgCountRoot += cu.MsgCountRoot
}
}
return teamUnread, nil
}
func (a *App) RemoveUserFromTeam(c request.CTX, teamID string, userID string, requestorId string) *model.AppError {
tchan := make(chan store.StoreResult, 1)
go func() {
team, err := a.Srv().Store().Team().Get(teamID)
tchan <- store.StoreResult{Data: team, NErr: err}
close(tchan)
}()
uchan := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), userID)
uchan <- store.StoreResult{Data: user, NErr: err}
close(uchan)
}()
result := <-tchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return model.NewAppError("RemoveUserFromTeam", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return model.NewAppError("RemoveUserFromTeam", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
team := result.Data.(*model.Team)
result = <-uchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return model.NewAppError("RemoveUserFromTeam", MissingAccountError, nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return model.NewAppError("RemoveUserFromTeam", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
user := result.Data.(*model.User)
if err := a.LeaveTeam(c, team, user, requestorId); err != nil {
return err
}
return nil
}
func (a *App) postProcessTeamMemberLeave(c request.CTX, teamMember *model.TeamMember, requestorId string) *model.AppError {
var actor *model.User
if requestorId != "" {
actor, _ = a.GetUser(requestorId)
}
a.Srv().Go(func() {
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.UserHasLeftTeam(pluginContext, teamMember, actor)
return true
}, plugin.UserHasLeftTeamID)
})
user, nErr := a.Srv().Store().User().Get(context.Background(), teamMember.UserId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return model.NewAppError("postProcessTeamMemberLeave", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return model.NewAppError("postProcessTeamMemberLeave", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if _, err := a.Srv().Store().User().UpdateUpdateAt(user.Id); err != nil {
return model.NewAppError("postProcessTeamMemberLeave", "app.user.update_update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Channel().ClearSidebarOnTeamLeave(user.Id, teamMember.TeamId); err != nil {
return model.NewAppError("postProcessTeamMemberLeave", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// delete the preferences that set the last channel used in the team and other team specific preferences
if err := a.Srv().Store().Preference().DeleteCategory(user.Id, teamMember.TeamId); err != nil {
return model.NewAppError("postProcessTeamMemberLeave", "app.preference.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.ClearSessionCacheForUser(user.Id)
a.InvalidateCacheForUser(user.Id)
a.invalidateCacheForUserTeams(user.Id)
return nil
}
func (a *App) LeaveTeam(c request.CTX, team *model.Team, user *model.User, requestorId string) *model.AppError {
teamMember, err := a.GetTeamMember(team.Id, user.Id)
if err != nil {
return model.NewAppError("LeaveTeam", "api.team.remove_user_from_team.missing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
var channelList model.ChannelList
var nErr error
if channelList, nErr = a.Srv().Store().Channel().GetChannels(team.Id, user.Id, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 0,
}); nErr != nil {
var nfErr *store.ErrNotFound
if errors.As(nErr, &nfErr) {
channelList = model.ChannelList{}
} else {
return model.NewAppError("LeaveTeam", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
for _, channel := range channelList {
if !channel.IsGroupOrDirect() {
a.invalidateCacheForChannelMembers(channel.Id)
if nErr = a.Srv().Store().Channel().RemoveMember(channel.Id, user.Id); nErr != nil {
return model.NewAppError("LeaveTeam", "app.channel.remove_member.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
}
if *a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages {
channel, cErr := a.Srv().Store().Channel().GetByName(team.Id, model.DefaultChannelName, false)
if cErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(cErr, &nfErr):
return model.NewAppError("LeaveTeam", "app.channel.get_by_name.missing.app_error", nil, "", http.StatusNotFound).Wrap(cErr)
default:
return model.NewAppError("LeaveTeam", "app.channel.get_by_name.existing.app_error", nil, "", http.StatusInternalServerError).Wrap(cErr)
}
}
if requestorId == user.Id {
if err = a.postLeaveTeamMessage(c, user, channel); err != nil {
c.Logger().Warn("Failed to post join/leave message", mlog.Err(err))
}
} else {
if err = a.postRemoveFromTeamMessage(c, user, channel); err != nil {
c.Logger().Warn("Failed to post join/leave message", mlog.Err(err))
}
}
}
if err := a.ch.srv.teamService.RemoveTeamMember(teamMember); err != nil {
return model.NewAppError("RemoveTeamMemberFromTeam", "app.team.save_member.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.postProcessTeamMemberLeave(c, teamMember, requestorId); err != nil {
return err
}
return nil
}
func (a *App) postLeaveTeamMessage(c request.CTX, user *model.User, channel *model.Channel) *model.AppError {
post := &model.Post{
ChannelId: channel.Id,
Message: fmt.Sprintf(i18n.T("api.team.leave.left"), user.Username),
Type: model.PostTypeLeaveTeam,
UserId: user.Id,
Props: model.StringInterface{
"username": user.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("postRemoveFromChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) postRemoveFromTeamMessage(c request.CTX, user *model.User, channel *model.Channel) *model.AppError {
post := &model.Post{
ChannelId: channel.Id,
Message: fmt.Sprintf(i18n.T("api.team.remove_user_from_team.removed"), user.Username),
Type: model.PostTypeRemoveFromTeam,
UserId: user.Id,
Props: model.StringInterface{
"username": user.Username,
},
}
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
return model.NewAppError("postRemoveFromTeamMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) prepareInviteNewUsersToTeam(teamID, senderId string, channelIds []string) (*model.User, *model.Team, []*model.Channel, *model.AppError) {
tchan := make(chan store.StoreResult, 1)
go func() {
team, err := a.Srv().Store().Team().Get(teamID)
tchan <- store.StoreResult{Data: team, NErr: err}
close(tchan)
}()
uchan := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), senderId)
uchan <- store.StoreResult{Data: user, NErr: err}
close(uchan)
}()
var channels []*model.Channel
var err error
if len(channelIds) > 0 {
channels, err = a.Srv().Store().Channel().GetChannelsByIds(channelIds, false)
if err != nil {
return nil, nil, nil, model.NewAppError("prepareInviteNewUsersToTeam", "app.channel.get_channels_by_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
result := <-tchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, nil, nil, model.NewAppError("prepareInviteNewUsersToTeam", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, nil, nil, model.NewAppError("prepareInviteNewUsersToTeam", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
team := result.Data.(*model.Team)
result = <-uchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, nil, nil, model.NewAppError("prepareInviteNewUsersToTeam", MissingAccountError, nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, nil, nil, model.NewAppError("prepareInviteNewUsersToTeam", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
user := result.Data.(*model.User)
for _, channel := range channels {
if channel.TeamId != teamID {
return nil, nil, nil, model.NewAppError("prepareInviteGuestsToChannels", "api.team.invite_guests.channel_in_invalid_team.app_error", nil, "", http.StatusBadRequest)
}
}
return user, team, channels, nil
}
func (a *App) InviteNewUsersToTeamGracefully(memberInvite *model.MemberInvite, teamID, senderId string, reminderInterval string) ([]*model.EmailInviteWithError, *model.AppError) {
if !*a.Config().ServiceSettings.EnableEmailInvitations {
return nil, model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.disabled.app_error", nil, "", http.StatusNotImplemented)
}
emailList := memberInvite.Emails
if len(emailList) == 0 {
err := model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.no_one.app_error", nil, "", http.StatusBadRequest)
return nil, err
}
user, team, channels, err := a.prepareInviteNewUsersToTeam(teamID, senderId, memberInvite.ChannelIds)
if err != nil {
return nil, err
}
allowedDomains := a.ch.srv.teamService.GetAllowedDomains(user, team)
var inviteListWithErrors []*model.EmailInviteWithError
var goodEmails []string
for _, email := range emailList {
invite := &model.EmailInviteWithError{
Email: email,
Error: nil,
}
if !teams.IsEmailAddressAllowed(email, allowedDomains) {
invite.Error = model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": email}, "", http.StatusBadRequest)
} else {
goodEmails = append(goodEmails, email)
}
inviteListWithErrors = append(inviteListWithErrors, invite)
}
var reminderData *model.TeamInviteReminderData
if reminderInterval != "" {
reminderData = &model.TeamInviteReminderData{Interval: reminderInterval}
}
if len(goodEmails) > 0 {
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
senderProfileImage, _, err := a.GetProfileImage(user)
if err != nil {
a.Log().Warn("Unable to get the sender user profile image.", mlog.String("user_id", user.Id), mlog.String("team_id", team.Id), mlog.Err(err))
}
userIsFirstAdmin := a.UserIsFirstAdmin(user)
var eErr error
var invitesWithErrors2 []*model.EmailInviteWithError
if len(channels) > 0 {
invitesWithErrors2, eErr = a.Srv().EmailService.SendInviteEmailsToTeamAndChannels(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, goodEmails, a.GetSiteURL(), reminderData, memberInvite.Message, true, user.IsSystemAdmin(), userIsFirstAdmin)
inviteListWithErrors = append(inviteListWithErrors, invitesWithErrors2...)
} else {
eErr = a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, goodEmails, a.GetSiteURL(), reminderData, true, user.IsSystemAdmin(), userIsFirstAdmin)
}
if eErr != nil {
switch {
case errors.Is(eErr, email.SendMailError):
for i := range inviteListWithErrors {
if inviteListWithErrors[i].Error == nil {
if *a.Config().EmailSettings.SMTPServer == model.EmailSMTPDefaultServer && *a.Config().EmailSettings.SMTPPort == model.EmailSMTPDefaultPort {
inviteListWithErrors[i].Error = model.NewAppError("InviteNewUsersToTeamGracefully", "api.team.invite_members.unable_to_send_email_with_defaults.app_error", nil, "", http.StatusInternalServerError)
} else {
inviteListWithErrors[i].Error = model.NewAppError("InviteNewUsersToTeamGracefully", "api.team.invite_members.unable_to_send_email.app_error", nil, "", http.StatusInternalServerError)
}
}
}
case errors.Is(eErr, email.NoRateLimiterError):
return nil, model.NewAppError("InviteNewUsersToTeamGracefully", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s", user.Id, team.Id), http.StatusInternalServerError)
case errors.Is(eErr, email.SetupRateLimiterError):
return nil, model.NewAppError("InviteNewUsersToTeamGracefully", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusInternalServerError)
default:
return nil, model.NewAppError("InviteNewUsersToTeamGracefully", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusRequestEntityTooLarge)
}
}
}
return inviteListWithErrors, nil
}
func (a *App) prepareInviteGuestsToChannels(teamID string, guestsInvite *model.GuestsInvite, senderId string) (*model.User, *model.Team, []*model.Channel, *model.AppError) {
if err := guestsInvite.IsValid(); err != nil {
return nil, nil, nil, err
}
tchan := make(chan store.StoreResult, 1)
go func() {
team, err := a.Srv().Store().Team().Get(teamID)
tchan <- store.StoreResult{Data: team, NErr: err}
close(tchan)
}()
cchan := make(chan store.StoreResult, 1)
go func() {
channels, err := a.Srv().Store().Channel().GetChannelsByIds(guestsInvite.Channels, false)
cchan <- store.StoreResult{Data: channels, NErr: err}
close(cchan)
}()
uchan := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), senderId)
uchan <- store.StoreResult{Data: user, NErr: err}
close(uchan)
}()
result := <-cchan
if result.NErr != nil {
return nil, nil, nil, model.NewAppError("prepareInviteGuestsToChannels", "app.channel.get_channels_by_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
channels := result.Data.([]*model.Channel)
result = <-uchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, nil, nil, model.NewAppError("prepareInviteGuestsToChannels", MissingAccountError, nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, nil, nil, model.NewAppError("prepareInviteGuestsToChannels", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
user := result.Data.(*model.User)
result = <-tchan
if result.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result.NErr, &nfErr):
return nil, nil, nil, model.NewAppError("prepareInviteGuestsToChannels", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusNotFound).Wrap(result.NErr)
default:
return nil, nil, nil, model.NewAppError("prepareInviteGuestsToChannels", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
team := result.Data.(*model.Team)
for _, channel := range channels {
if channel.TeamId != teamID {
return nil, nil, nil, model.NewAppError("prepareInviteGuestsToChannels", "api.team.invite_guests.channel_in_invalid_team.app_error", nil, "", http.StatusBadRequest)
}
}
return user, team, channels, nil
}
func (a *App) InviteGuestsToChannelsGracefully(teamID string, guestsInvite *model.GuestsInvite, senderId string) ([]*model.EmailInviteWithError, *model.AppError) {
if !*a.Config().ServiceSettings.EnableEmailInvitations {
return nil, model.NewAppError("InviteGuestsToChannelsGracefully", "api.team.invite_members.disabled.app_error", nil, "", http.StatusNotImplemented)
}
user, team, channels, err := a.prepareInviteGuestsToChannels(teamID, guestsInvite, senderId)
if err != nil {
return nil, err
}
var inviteListWithErrors []*model.EmailInviteWithError
var goodEmails []string
for _, email := range guestsInvite.Emails {
invite := &model.EmailInviteWithError{
Email: email,
Error: nil,
}
if !users.CheckEmailDomain(email, *a.Config().GuestAccountsSettings.RestrictCreationToDomains) {
invite.Error = model.NewAppError("InviteGuestsToChannelsGracefully", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": email}, "", http.StatusBadRequest)
} else {
goodEmails = append(goodEmails, email)
}
inviteListWithErrors = append(inviteListWithErrors, invite)
}
if len(goodEmails) > 0 {
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
senderProfileImage, _, err := a.GetProfileImage(user)
if err != nil {
a.Log().Warn("Unable to get the sender user profile image.", mlog.String("user_id", user.Id), mlog.String("team_id", team.Id), mlog.Err(err))
}
eErr := a.Srv().EmailService.SendGuestInviteEmails(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, goodEmails, a.GetSiteURL(), guestsInvite.Message, true, user.IsSystemAdmin(), a.UserIsFirstAdmin(user))
if eErr != nil {
switch {
case errors.Is(eErr, email.SendMailError):
for i := range inviteListWithErrors {
if inviteListWithErrors[i].Error == nil {
if *a.Config().EmailSettings.SMTPServer == model.EmailSMTPDefaultServer && *a.Config().EmailSettings.SMTPPort == model.EmailSMTPDefaultPort {
inviteListWithErrors[i].Error = model.NewAppError("InviteGuestsToChannelsGracefully", "api.team.invite_members.unable_to_send_email_with_defaults.app_error", nil, "", http.StatusInternalServerError)
} else {
inviteListWithErrors[i].Error = model.NewAppError("InviteGuestsToChannelsGracefully", "api.team.invite_members.unable_to_send_email.app_error", nil, "", http.StatusInternalServerError)
}
}
}
case errors.Is(eErr, email.NoRateLimiterError):
return nil, model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s", user.Id, team.Id), http.StatusInternalServerError)
case errors.Is(eErr, email.SetupRateLimiterError):
return nil, model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusInternalServerError)
default:
return nil, model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusRequestEntityTooLarge)
}
}
}
return inviteListWithErrors, nil
}
func (a *App) InviteNewUsersToTeam(emailList []string, teamID, senderId string) *model.AppError {
if !*a.Config().ServiceSettings.EnableEmailInvitations {
return model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.disabled.app_error", nil, "", http.StatusNotImplemented)
}
if len(emailList) == 0 {
err := model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.no_one.app_error", nil, "", http.StatusBadRequest)
return err
}
user, team, _, err := a.prepareInviteNewUsersToTeam(teamID, senderId, []string{})
if err != nil {
return err
}
allowedDomains := a.ch.srv.teamService.GetAllowedDomains(user, team)
var invalidEmailList []string
for _, email := range emailList {
if !teams.IsEmailAddressAllowed(email, allowedDomains) {
invalidEmailList = append(invalidEmailList, email)
}
}
if len(invalidEmailList) > 0 {
s := strings.Join(invalidEmailList, ", ")
return model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": s}, "", http.StatusBadRequest)
}
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
eErr := a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, emailList, a.GetSiteURL(), nil, false, user.IsSystemAdmin(), a.UserIsFirstAdmin(user))
if eErr != nil {
switch {
case errors.Is(eErr, email.NoRateLimiterError):
return model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s", user.Id, team.Id), http.StatusInternalServerError)
case errors.Is(eErr, email.SetupRateLimiterError):
return model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusInternalServerError)
default:
return model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusRequestEntityTooLarge)
}
}
return nil
}
func (a *App) InviteGuestsToChannels(teamID string, guestsInvite *model.GuestsInvite, senderId string) *model.AppError {
if !*a.Config().ServiceSettings.EnableEmailInvitations {
return model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.disabled.app_error", nil, "", http.StatusNotImplemented)
}
user, team, channels, err := a.prepareInviteGuestsToChannels(teamID, guestsInvite, senderId)
if err != nil {
return err
}
var invalidEmailList []string
for _, email := range guestsInvite.Emails {
if !users.CheckEmailDomain(email, *a.Config().GuestAccountsSettings.RestrictCreationToDomains) {
invalidEmailList = append(invalidEmailList, email)
}
}
if len(invalidEmailList) > 0 {
s := strings.Join(invalidEmailList, ", ")
return model.NewAppError("InviteGuestsToChannels", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": s}, "", http.StatusBadRequest)
}
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
senderProfileImage, _, err := a.GetProfileImage(user)
if err != nil {
a.Log().Warn("Unable to get the sender user profile image.", mlog.String("user_id", user.Id), mlog.String("team_id", team.Id), mlog.Err(err))
}
eErr := a.Srv().EmailService.SendGuestInviteEmails(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, guestsInvite.Emails, a.GetSiteURL(), guestsInvite.Message, false, user.IsSystemAdmin(), a.UserIsFirstAdmin(user))
if eErr != nil {
switch {
case errors.Is(eErr, email.NoRateLimiterError):
return model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s", user.Id, team.Id), http.StatusInternalServerError)
case errors.Is(eErr, email.SetupRateLimiterError):
return model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, err), http.StatusInternalServerError)
default:
return model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, err), http.StatusRequestEntityTooLarge)
}
}
return nil
}
func (a *App) FindTeamByName(name string) bool {
if _, err := a.Srv().Store().Team().GetByName(name); err != nil {
return false
}
return true
}
func (a *App) GetTeamsUnreadForUser(excludeTeamId string, userID string, includeCollapsedThreads bool) ([]*model.TeamUnread, *model.AppError) {
data, err := a.Srv().Store().Team().GetChannelUnreadsForAllTeams(excludeTeamId, userID)
if err != nil {
return nil, model.NewAppError("GetTeamsUnreadForUser", "app.team.get_unread.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
members := []*model.TeamUnread{}
membersMap := make(map[string]*model.TeamUnread)
unreads := func(cu *model.ChannelUnread, tu *model.TeamUnread) *model.TeamUnread {
tu.MentionCount += cu.MentionCount
tu.MentionCountRoot += cu.MentionCountRoot
if cu.NotifyProps[model.MarkUnreadNotifyProp] != model.ChannelMarkUnreadMention {
tu.MsgCount += cu.MsgCount
tu.MsgCountRoot += cu.MsgCountRoot
}
return tu
}
teamIDs := make([]string, 0, len(data))
for i := range data {
id := data[i].TeamId
if mu, ok := membersMap[id]; ok {
membersMap[id] = unreads(data[i], mu)
} else {
teamIDs = append(teamIDs, id)
membersMap[id] = unreads(data[i], &model.TeamUnread{
MsgCount: 0,
MentionCount: 0,
MentionCountRoot: 0,
MsgCountRoot: 0,
ThreadCount: 0,
ThreadMentionCount: 0,
ThreadUrgentMentionCount: 0,
TeamId: id,
})
}
}
includeCollapsedThreads = includeCollapsedThreads && *a.Config().ServiceSettings.CollapsedThreads != model.CollapsedThreadsDisabled
if includeCollapsedThreads {
teamUnreads, err := a.Srv().Store().Thread().GetTeamsUnreadForUser(userID, teamIDs, a.isPostPriorityEnabled())
if err != nil {
return nil, model.NewAppError("GetTeamsUnreadForUser", "app.team.get_unread.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for teamID, member := range membersMap {
if _, ok := teamUnreads[teamID]; ok {
member.ThreadCount = teamUnreads[teamID].ThreadCount
member.ThreadMentionCount = teamUnreads[teamID].ThreadMentionCount
member.ThreadUrgentMentionCount = teamUnreads[teamID].ThreadUrgentMentionCount
}
}
}
for _, member := range membersMap {
members = append(members, member)
}
return members, nil
}
func (a *App) PermanentDeleteTeamId(c request.CTX, teamID string) *model.AppError {
team, err := a.GetTeam(teamID)
if err != nil {
return err
}
return a.PermanentDeleteTeam(c, team)
}
func (a *App) PermanentDeleteTeam(c request.CTX, team *model.Team) *model.AppError {
team.DeleteAt = model.GetMillis()
if _, err := a.Srv().Store().Team().Update(team); err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &invErr):
return model.NewAppError("PermanentDeleteTeam", "app.team.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr):
return appErr
default:
return model.NewAppError("PermanentDeleteTeam", "app.team.update.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if channels, err := a.Srv().Store().Channel().GetTeamChannels(team.Id); err != nil {
var nfErr *store.ErrNotFound
if !errors.As(err, &nfErr) {
return model.NewAppError("PermanentDeleteTeam", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
} else {
for _, ch := range channels {
a.PermanentDeleteChannel(c, ch)
}
}
if err := a.Srv().Store().Team().RemoveAllMembersByTeam(team.Id); err != nil {
return model.NewAppError("PermanentDeleteTeam", "app.team.remove_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Command().PermanentDeleteByTeam(team.Id); err != nil {
return model.NewAppError("PermanentDeleteTeam", "app.team.permanentdeleteteam.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Team().PermanentDelete(team.Id); err != nil {
return model.NewAppError("PermanentDeleteTeam", "app.team.permanent_delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if appErr := a.sendTeamEvent(team, model.WebsocketEventDeleteTeam); appErr != nil {
return appErr
}
return nil
}
func (a *App) SoftDeleteTeam(teamID string) *model.AppError {
team, err := a.GetTeam(teamID)
if err != nil {
return err
}
team.DeleteAt = model.GetMillis()
team, nErr := a.Srv().Store().Team().Update(team)
if nErr != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(nErr, &invErr):
return model.NewAppError("SoftDeleteTeam", "app.team.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &appErr):
return appErr
default:
return model.NewAppError("SoftDeleteTeam", "app.team.update.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if appErr := a.sendTeamEvent(team, model.WebsocketEventDeleteTeam); appErr != nil {
return appErr
}
return nil
}
func (a *App) RestoreTeam(teamID string) *model.AppError {
team, err := a.GetTeam(teamID)
if err != nil {
return err
}
team.DeleteAt = 0
team, nErr := a.Srv().Store().Team().Update(team)
if nErr != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(nErr, &invErr):
return model.NewAppError("RestoreTeam", "app.team.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &appErr):
return appErr
default:
return model.NewAppError("RestoreTeam", "app.team.update.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if appErr := a.sendTeamEvent(team, model.WebsocketEventRestoreTeam); appErr != nil {
return appErr
}
return nil
}
func (a *App) GetTeamStats(teamID string, restrictions *model.ViewUsersRestrictions) (*model.TeamStats, *model.AppError) {
tchan := make(chan store.StoreResult, 1)
go func() {
totalMemberCount, err := a.Srv().Store().Team().GetTotalMemberCount(teamID, restrictions)
tchan <- store.StoreResult{Data: totalMemberCount, NErr: err}
close(tchan)
}()
achan := make(chan store.StoreResult, 1)
go func() {
memberCount, err := a.Srv().Store().Team().GetActiveMemberCount(teamID, restrictions)
achan <- store.StoreResult{Data: memberCount, NErr: err}
close(achan)
}()
stats := &model.TeamStats{}
stats.TeamId = teamID
result := <-tchan
if result.NErr != nil {
return nil, model.NewAppError("GetTeamStats", "app.team.get_member_count.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
stats.TotalMemberCount = result.Data.(int64)
result = <-achan
if result.NErr != nil {
return nil, model.NewAppError("GetTeamStats", "app.team.get_active_member_count.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
stats.ActiveMemberCount = result.Data.(int64)
return stats, nil
}
func (a *App) GetTeamIdFromQuery(query url.Values) (string, *model.AppError) {
tokenID := query.Get("t")
inviteId := query.Get("id")
if tokenID != "" {
token, err := a.Srv().Store().Token().GetByToken(tokenID)
if err != nil {
return "", model.NewAppError("GetTeamIdFromQuery", "api.oauth.singup_with_oauth.invalid_link.app_error", nil, "", http.StatusBadRequest)
}
if token.Type != TokenTypeTeamInvitation && token.Type != TokenTypeGuestInvitation {
return "", model.NewAppError("GetTeamIdFromQuery", "api.oauth.singup_with_oauth.invalid_link.app_error", nil, "", http.StatusBadRequest)
}
if model.GetMillis()-token.CreateAt >= InvitationExpiryTime {
a.DeleteToken(token)
return "", model.NewAppError("GetTeamIdFromQuery", "api.oauth.singup_with_oauth.expired_link.app_error", nil, "", http.StatusBadRequest)
}
tokenData := model.MapFromJSON(strings.NewReader(token.Extra))
return tokenData["teamId"], nil
}
if inviteId != "" {
team, err := a.Srv().Store().Team().GetByInviteId(inviteId)
if err == nil {
return team.Id, nil
}
// soft fail, so we still create user but don't auto-join team
mlog.Warn("Error getting team by inviteId.", mlog.String("invite_id", inviteId), mlog.Err(err))
}
return "", nil
}
func (a *App) SanitizeTeam(session model.Session, team *model.Team) *model.Team {
if a.SessionHasPermissionToTeam(session, team.Id, model.PermissionManageTeam) {
return team
}
if a.SessionHasPermissionToTeam(session, team.Id, model.PermissionInviteUser) {
inviteId := team.InviteId
team.Sanitize()
team.InviteId = inviteId
return team
}
team.Sanitize()
return team
}
func (a *App) SanitizeTeams(session model.Session, teams []*model.Team) []*model.Team {
for _, team := range teams {
a.SanitizeTeam(session, team)
}
return teams
}
func (a *App) GetTeamIcon(team *model.Team) ([]byte, *model.AppError) {
if *a.Config().FileSettings.DriverName == "" {
return nil, model.NewAppError("GetTeamIcon", "api.team.get_team_icon.filesettings_no_driver.app_error", nil, "", http.StatusNotImplemented)
}
path := "teams/" + team.Id + "/teamIcon.png"
data, err := a.ReadFile(path)
if err != nil {
return nil, model.NewAppError("GetTeamIcon", "api.team.get_team_icon.read_file.app_error", nil, "", http.StatusNotFound).Wrap(err)
}
return data, nil
}
func (a *App) SetTeamIcon(teamID string, imageData *multipart.FileHeader) *model.AppError {
file, err := imageData.Open()
if err != nil {
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.open.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
defer file.Close()
return a.SetTeamIconFromMultiPartFile(teamID, file)
}
func (a *App) SetTeamIconFromMultiPartFile(teamID string, file multipart.File) *model.AppError {
team, getTeamErr := a.GetTeam(teamID)
if getTeamErr != nil {
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.get_team.app_error", nil, "", http.StatusBadRequest).Wrap(getTeamErr)
}
if *a.Config().FileSettings.DriverName == "" {
return model.NewAppError("setTeamIcon", "api.team.set_team_icon.storage.app_error", nil, "", http.StatusNotImplemented)
}
if limitErr := checkImageLimits(file, *a.Config().FileSettings.MaxImageResolution); limitErr != nil {
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.check_image_limits.app_error",
nil, "", http.StatusBadRequest).Wrap(limitErr)
}
return a.SetTeamIconFromFile(team, file)
}
func (a *App) SetTeamIconFromFile(team *model.Team, file io.Reader) *model.AppError {
// Decode image into Image object
img, _, err := image.Decode(file)
if err != nil {
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.decode.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
orientation, _ := imaging.GetImageOrientation(file)
img = imaging.MakeImageUpright(img, orientation)
// Scale team icon
teamIconWidthAndHeight := 128
img = imaging.FillCenter(img, teamIconWidthAndHeight, teamIconWidthAndHeight)
buf := new(bytes.Buffer)
err = a.ch.imgEncoder.EncodePNG(buf, img)
if err != nil {
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.encode.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
path := "teams/" + team.Id + "/teamIcon.png"
if _, err := a.WriteFile(buf, path); err != nil {
return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.write_file.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
curTime := model.GetMillis()
if err := a.Srv().Store().Team().UpdateLastTeamIconUpdate(team.Id, curTime); err != nil {
return model.NewAppError("SetTeamIcon", "api.team.team_icon.update.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
// manually set time to avoid possible cluster inconsistencies
team.LastTeamIconUpdate = curTime
if appErr := a.sendTeamEvent(team, model.WebsocketEventUpdateTeam); appErr != nil {
return appErr
}
return nil
}
func (a *App) RemoveTeamIcon(teamID string) *model.AppError {
team, err := a.GetTeam(teamID)
if err != nil {
return model.NewAppError("RemoveTeamIcon", "api.team.remove_team_icon.get_team.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if err := a.Srv().Store().Team().UpdateLastTeamIconUpdate(teamID, 0); err != nil {
return model.NewAppError("RemoveTeamIcon", "api.team.team_icon.update.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
team.LastTeamIconUpdate = 0
if appErr := a.sendTeamEvent(team, model.WebsocketEventUpdateTeam); appErr != nil {
return appErr
}
return nil
}
func (a *App) InvalidateAllEmailInvites() *model.AppError {
if err := a.Srv().Store().Token().RemoveAllTokensByType(TokenTypeTeamInvitation); err != nil {
return model.NewAppError("InvalidateAllEmailInvites", "api.team.invalidate_all_email_invites.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Token().RemoveAllTokensByType(TokenTypeGuestInvitation); err != nil {
return model.NewAppError("InvalidateAllEmailInvites", "api.team.invalidate_all_email_invites.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.InvalidateAllResendInviteEmailJobs(); err != nil {
return model.NewAppError("InvalidateAllEmailInvites", "api.team.invalidate_all_email_invites.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) InvalidateAllResendInviteEmailJobs() *model.AppError {
jobs, appErr := a.Srv().Jobs.GetJobsByTypeAndStatus(model.JobTypeResendInvitationEmail, model.JobStatusPending)
if appErr != nil {
return appErr
}
for _, j := range jobs {
a.Srv().Jobs.SetJobCanceled(j)
// clean up any system values this job was using
a.Srv().Store().System().PermanentDeleteByName(j.Id)
}
return nil
}
func (a *App) ClearTeamMembersCache(teamID string) error {
perPage := 100
page := 0
for {
teamMembers, err := a.Srv().Store().Team().GetMembers(teamID, page*perPage, perPage, nil)
if err != nil {
return fmt.Errorf("failed to get team members: %v", err)
}
for _, teamMember := range teamMembers {
a.ClearSessionCacheForUser(teamMember.UserId)
message := model.NewWebSocketEvent(model.WebsocketEventMemberroleUpdated, "", "", teamMember.UserId, nil, "")
tmJSON, jsonErr := json.Marshal(teamMember)
if jsonErr != nil {
return jsonErr
}
message.Add("member", string(tmJSON))
a.Publish(message)
}
length := len(teamMembers)
if length < perPage {
break
}
page++
}
return nil
}
func (a *App) GetNewTeamMembersSince(c request.CTX, teamID string, opts *model.InsightsOpts) (*model.NewTeamMembersList, int64, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, 0, model.NewAppError("GetNewTeamMembersSince", "app.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
ntms, count, err := a.Srv().Store().Team().GetNewTeamMembersSince(teamID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, 0, model.NewAppError("GetNewTeamMembersSince", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return ntms, count, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package teams
import "errors"
var (
AcceptedDomainError = errors.New("the user cannot be added as the domain associated with the account is not permitted")
MemberCountError = errors.New("unable to count the team members")
MaxMemberCountError = errors.New("reached to the maximum number of allowed accounts")
)
type DomainError struct {
Domain string
}
func (DomainError) Error() string {
return "restricting team to the domain, it is not allowed by the system config"
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package teams
import (
"errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type TeamService struct {
store store.TeamStore
groupStore store.GroupStore
channelStore store.ChannelStore // TODO: replace this with ChannelService in the future
users Users
wh WebHub
config func() *model.Config
license func() *model.License
}
// ServiceConfig is used to initialize the TeamService.
type ServiceConfig struct {
// Mandatory fields
TeamStore store.TeamStore
GroupStore store.GroupStore
ChannelStore store.ChannelStore
Users Users
WebHub WebHub
ConfigFn func() *model.Config
LicenseFn func() *model.License
}
// Users is a subset of UserService interface
type Users interface {
GetUser(userID string) (*model.User, error)
}
// WebHub is used to publish events, the name should be given appropriately
// while developing the websocket or clustering service
type WebHub interface {
Publish(message *model.WebSocketEvent)
}
func New(c ServiceConfig) (*TeamService, error) {
if err := c.validate(); err != nil {
return nil, err
}
return &TeamService{
store: c.TeamStore,
groupStore: c.GroupStore,
channelStore: c.ChannelStore,
users: c.Users,
config: c.ConfigFn,
license: c.LicenseFn,
wh: c.WebHub,
}, nil
}
func (c *ServiceConfig) validate() error {
if c.ConfigFn == nil || c.TeamStore == nil || c.LicenseFn == nil || c.Users == nil || c.ChannelStore == nil || c.GroupStore == nil || c.WebHub == nil {
return errors.New("required parameters are not provided")
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package teams
import (
"context"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
func (ts *TeamService) CreateTeam(team *model.Team) (*model.Team, error) {
team.InviteId = ""
rteam, err := ts.store.Save(team)
if err != nil {
return nil, err
}
if _, err := ts.createDefaultChannels(rteam.Id); err != nil {
return nil, err
}
return rteam, nil
}
func (ts *TeamService) GetTeam(teamID string) (*model.Team, error) {
team, err := ts.store.Get(teamID)
if err != nil {
return nil, err
}
return team, nil
}
func (ts *TeamService) GetTeams(teamIDs []string) ([]*model.Team, error) {
teams, err := ts.store.GetMany(teamIDs)
if err != nil {
return nil, err
}
return teams, nil
}
// CreateDefaultChannels creates channels in the given team for each channel returned by (*App).DefaultChannelNames.
func (ts *TeamService) createDefaultChannels(teamID string) ([]*model.Channel, error) {
displayNames := map[string]string{
"town-square": i18n.T("api.channel.create_default_channels.town_square"),
"off-topic": i18n.T("api.channel.create_default_channels.off_topic"),
}
channels := []*model.Channel{}
defaultChannelNames := ts.DefaultChannelNames()
for _, name := range defaultChannelNames {
displayName := i18n.TDefault(displayNames[name], name)
channel := &model.Channel{DisplayName: displayName, Name: name, Type: model.ChannelTypeOpen, TeamId: teamID}
// We should use the channel service here (coming soon). Ideally, we should just emit an event
// and let the subscribers do the job, in this case it would be the channels service.
// Currently we are adding services to the server and because of that we are using
// the channel store here. This should be replaced in the future.
if _, err := ts.channelStore.Save(channel, *ts.config().TeamSettings.MaxChannelsPerTeam); err != nil {
return nil, err
}
channels = append(channels, channel)
}
return channels, nil
}
type UpdateOptions struct {
Sanitized bool
Imported bool
}
func (ts *TeamService) UpdateTeam(team *model.Team, opts UpdateOptions) (*model.Team, error) {
oldTeam := team
var err error
if !opts.Imported {
oldTeam, err = ts.store.Get(team.Id)
if err != nil {
return nil, err
}
if err = ts.checkValidDomains(team); err != nil {
return nil, err
}
}
if opts.Sanitized {
oldTeam.DisplayName = team.DisplayName
oldTeam.Description = team.Description
oldTeam.AllowOpenInvite = team.AllowOpenInvite
oldTeam.CompanyName = team.CompanyName
oldTeam.AllowedDomains = team.AllowedDomains
oldTeam.LastTeamIconUpdate = team.LastTeamIconUpdate
oldTeam.GroupConstrained = team.GroupConstrained
}
oldTeam, err = ts.store.Update(oldTeam)
if err != nil {
return team, err
}
return oldTeam, nil
}
func (ts *TeamService) PatchTeam(teamID string, patch *model.TeamPatch) (*model.Team, error) {
team, err := ts.store.Get(teamID)
if err != nil {
return nil, err
}
team.Patch(patch)
if patch.AllowOpenInvite != nil && !*patch.AllowOpenInvite {
team.InviteId = model.NewId()
}
if err = ts.checkValidDomains(team); err != nil {
return nil, err
}
team, err = ts.store.Update(team)
if err != nil {
return team, err
}
return team, nil
}
// JoinUserToTeam adds a user to the team and it returns three values:
// 1. a pointer to the team member, if successful
// 2. a boolean: true if the user has a non-deleted team member for that team already, otherwise false.
// 3. a pointer to an AppError if something went wrong.
func (ts *TeamService) JoinUserToTeam(team *model.Team, user *model.User) (*model.TeamMember, bool, error) {
if !ts.IsTeamEmailAllowed(user, team) {
return nil, false, AcceptedDomainError
}
tm := &model.TeamMember{
TeamId: team.Id,
UserId: user.Id,
SchemeGuest: user.IsGuest(),
SchemeUser: !user.IsGuest(),
CreateAt: model.GetMillis(),
}
if !user.IsGuest() {
userShouldBeAdmin, err := ts.userIsInAdminRoleGroup(user.Id, team.Id, model.GroupSyncableTypeTeam)
if err != nil {
return nil, false, err
}
tm.SchemeAdmin = userShouldBeAdmin
}
if team.Email == user.Email {
tm.SchemeAdmin = true
}
rtm, err := ts.store.GetMember(context.Background(), team.Id, user.Id)
if err != nil {
// Membership appears to be missing. Lets try to add.
tmr, nErr := ts.store.SaveMember(tm, *ts.config().TeamSettings.MaxUsersPerTeam)
if nErr != nil {
return nil, false, nErr
}
return tmr, false, nil
}
// Membership already exists. Check if deleted and update, otherwise do nothing
// Do nothing if already added
if rtm.DeleteAt == 0 {
return rtm, true, nil
}
membersCount, err := ts.store.GetActiveMemberCount(tm.TeamId, nil)
if err != nil {
return nil, false, MemberCountError
}
if membersCount >= int64(*ts.config().TeamSettings.MaxUsersPerTeam) {
return nil, false, MaxMemberCountError
}
member, nErr := ts.store.UpdateMember(tm)
if nErr != nil {
return nil, false, nErr
}
return member, false, nil
}
// RemoveTeamMember removes the team member from the team. This method sends
// the websocket message before actually removing so the user being removed gets it.
func (ts *TeamService) RemoveTeamMember(teamMember *model.TeamMember) error {
/*
MM-43850: send leave_team event to user using `ReliableClusterSend` to improve safety
*/
// message for other team members
omitUsers := make(map[string]bool, 1)
omitUsers[teamMember.UserId] = true
messageTeam := model.NewWebSocketEvent(model.WebsocketEventLeaveTeam, teamMember.TeamId, "", "", omitUsers, "")
messageTeam.Add("user_id", teamMember.UserId)
messageTeam.Add("team_id", teamMember.TeamId)
ts.wh.Publish(messageTeam)
// message for teamMember.UserId
messageUser := model.NewWebSocketEvent(model.WebsocketEventLeaveTeam, "", "", teamMember.UserId, nil, "")
messageUser.Add("user_id", teamMember.UserId)
messageUser.Add("team_id", teamMember.TeamId)
ts.wh.Publish(messageUser)
// delete team member
teamMember.Roles = ""
teamMember.DeleteAt = model.GetMillis()
if _, nErr := ts.store.UpdateMember(teamMember); nErr != nil {
return nErr
}
return nil
}
// GetMember return the team member from the team.
func (ts *TeamService) GetMember(teamID string, userID string) (*model.TeamMember, error) {
member, err := ts.store.GetMember(context.Background(), teamID, userID)
if err != nil {
return nil, err
}
return member, err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package teams
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
)
// By default the list will be (not necessarily in this order):
//
// ['town-square', 'off-topic']
//
// However, if TeamSettings.ExperimentalDefaultChannels contains a list of channels then that list will replace
// 'off-topic' and be included in the return results in addition to 'town-square'. For example:
//
// ['town-square', 'game-of-thrones', 'wow']
func (ts *TeamService) DefaultChannelNames() []string {
names := []string{"town-square"}
if len(ts.config().TeamSettings.ExperimentalDefaultChannels) == 0 {
names = append(names, "off-topic")
} else {
seenChannels := map[string]bool{"town-square": true}
for _, channelName := range ts.config().TeamSettings.ExperimentalDefaultChannels {
if !seenChannels[channelName] {
names = append(names, channelName)
seenChannels[channelName] = true
}
}
}
return names
}
func IsEmailAddressAllowed(email string, allowedDomains []string) bool {
for _, restriction := range allowedDomains {
domains := normalizeDomains(restriction)
if len(domains) <= 0 {
continue
}
matched := false
for _, d := range domains {
if strings.HasSuffix(email, "@"+d) {
matched = true
break
}
}
if !matched {
return false
}
}
return true
}
func (ts *TeamService) IsTeamEmailAllowed(user *model.User, team *model.Team) bool {
if user.IsBot {
return true
}
email := strings.ToLower(user.Email)
allowedDomains := ts.GetAllowedDomains(user, team)
return IsEmailAddressAllowed(email, allowedDomains)
}
func (ts *TeamService) GetAllowedDomains(user *model.User, team *model.Team) []string {
if user.IsGuest() {
return []string{*ts.config().GuestAccountsSettings.RestrictCreationToDomains}
}
// First check per team allowedDomains, then app wide restrictions
return []string{team.AllowedDomains, *ts.config().TeamSettings.RestrictCreationToDomains}
}
func (ts *TeamService) checkValidDomains(team *model.Team) error {
validDomains := normalizeDomains(*ts.config().TeamSettings.RestrictCreationToDomains)
if len(validDomains) > 0 {
for _, domain := range normalizeDomains(team.AllowedDomains) {
matched := false
for _, d := range validDomains {
if domain == d {
matched = true
break
}
}
if !matched {
return &DomainError{Domain: domain}
}
}
}
return nil
}
func normalizeDomains(domains string) []string {
// commas and @ signs are optional
// can be in the form of "@corp.mattermost.com, mattermost.com mattermost.org" -> corp.mattermost.com mattermost.com mattermost.org
return strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(domains, "@", " ", -1), ",", " ", -1))))
}
// UserIsInAdminRoleGroup returns true at least one of the user's groups are configured to set the members as
// admins in the given syncable.
func (ts *TeamService) userIsInAdminRoleGroup(userID, syncableID string, syncableType model.GroupSyncableType) (bool, error) {
groupIDs, err := ts.groupStore.AdminRoleGroupsForSyncableMember(userID, syncableID, syncableType)
if err != nil {
return false, err
}
if len(groupIDs) == 0 {
return false, nil
}
return true, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import "github.com/mattermost/mattermost-server/v6/server/platform/services/telemetry"
func (s *Server) GetTelemetryService() *telemetry.TelemetryService {
return s.telemetryService
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func (a *App) CreateTermsOfService(text, userID string) (*model.TermsOfService, *model.AppError) {
termsOfService := &model.TermsOfService{
Text: text,
UserId: userID,
}
if _, appErr := a.GetUser(userID); appErr != nil {
return nil, appErr
}
var err error
if termsOfService, err = a.Srv().Store().TermsOfService().Save(termsOfService); err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateTermsOfService", "app.terms_of_service.create.existing.app_error", nil, "id="+termsOfService.Id, http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CreateTermsOfService", "app.terms_of_service.create.app_error", nil, "terms_of_service_id="+termsOfService.Id, http.StatusInternalServerError).Wrap(err)
}
}
return termsOfService, nil
}
func (a *App) GetLatestTermsOfService() (*model.TermsOfService, *model.AppError) {
termsOfService, err := a.Srv().Store().TermsOfService().GetLatest(true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetLatestTermsOfService", "app.terms_of_service.get.no_rows.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetLatestTermsOfService", "app.terms_of_service.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return termsOfService, nil
}
func (a *App) GetTermsOfService(id string) (*model.TermsOfService, *model.AppError) {
termsOfService, err := a.Srv().Store().TermsOfService().Get(id, true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetTermsOfService", "app.terms_of_service.get.no_rows.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetTermsOfService", "app.terms_of_service.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return termsOfService, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"errors"
"net/http"
"os"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/services/telemetry"
)
func pluginActivated(pluginStates map[string]*model.PluginState, pluginId string) bool {
state, ok := pluginStates[pluginId]
if !ok {
return false
}
return state.Enable
}
func (a *App) getMarketplacePlugins() ([]string, error) {
ts := a.Srv().telemetryService
config := a.Srv().Config()
marketplacePlugins, err := ts.GetAllMarketplacePlugins(model.PluginSettingsDefaultMarketplaceURL)
if err != nil {
return nil, err
}
activePlugins := []string{}
for _, p := range marketplacePlugins {
id := p.Manifest.Id
if pluginActivated(config.PluginSettings.PluginStates, id) {
activePlugins = append(activePlugins, id)
}
}
return activePlugins, nil
}
func (a *App) getTrueUpProfile() (*model.TrueUpReviewProfile, error) {
license := a.Channels().License()
if license == nil {
return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.license_required", nil, "Could not get the total active users count", http.StatusInternalServerError)
}
// Customer Info & Usage Analytics
activeUserCount, err := a.Srv().Store().Status().GetTotalActiveUsersCount()
if err != nil {
return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.user_count_fail", nil, "Could not get the total active users count", http.StatusInternalServerError)
}
// Webhook, calls, boards, and playbook counts
incomingWebhookCount, err := a.Srv().Store().Webhook().AnalyticsIncomingCount("")
if err != nil {
return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.webhook_in_count_fail", nil, "Could not get the total incoming webhook count", http.StatusInternalServerError)
}
outgoingWebhookCount, err := a.Srv().Store().Webhook().AnalyticsOutgoingCount("")
if err != nil {
return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.webhook_out_count_fail", nil, "Could not get the total outgoing webhook count", http.StatusInternalServerError)
}
// Plugin Data
trueUpReviewPlugins := model.TrueUpReviewPlugins{
PluginNames: []string{},
}
if plugins, err := a.getMarketplacePlugins(); err == nil {
trueUpReviewPlugins.PluginNames = plugins
trueUpReviewPlugins.TotalPlugins = len(plugins)
}
// Authentication Features
config := a.Config()
mfaUsed := config.ServiceSettings.EnforceMultifactorAuthentication
ldapUsed := config.LdapSettings.Enable
samlUsed := config.SamlSettings.Enable
openIdUsed := config.OpenIdSettings.Enable
guestAccessAllowed := config.GuestAccountsSettings.Enable
authFeatures := map[string]*bool{
model.TrueUpReviewAuthFeaturesMfa: mfaUsed,
model.TrueUpReviewAuthFeaturesADLdap: ldapUsed,
model.TrueUpReviewAuthFeaturesSaml: samlUsed,
model.TrueUpReviewAuthFeatureOpenId: openIdUsed,
model.TrueUpReviewAuthFeatureGuestAccess: guestAccessAllowed,
}
authFeatureList := []string{}
for feature, used := range authFeatures {
if used != nil && *used {
authFeatureList = append(authFeatureList, feature)
}
}
reviewProfile := model.TrueUpReviewProfile{
ServerId: a.TelemetryId(),
ServerVersion: model.CurrentVersion,
ServerInstallationType: os.Getenv(telemetry.EnvVarInstallType),
LicenseId: license.Id,
LicensedSeats: *license.Features.Users,
LicensePlan: license.SkuName,
CustomerName: license.Customer.Name,
ActiveUsers: activeUserCount,
TotalIncomingWebhooks: incomingWebhookCount,
TotalOutgoingWebhooks: outgoingWebhookCount,
Plugins: trueUpReviewPlugins,
AuthenticationFeatures: authFeatureList,
}
return &reviewProfile, nil
}
func (a *App) GetTrueUpProfile() (map[string]any, error) {
profile, err := a.getTrueUpProfile()
if err != nil {
return nil, err
}
profileJson, err := json.Marshal(profile)
if err != nil {
return nil, err
}
telemetryProperties := map[string]any{}
json.Unmarshal(profileJson, &telemetryProperties)
delete(telemetryProperties, "plugins")
plugins := profile.Plugins.ToMap()
for key, pluginValue := range plugins {
telemetryProperties[key] = pluginValue
}
delete(telemetryProperties, "authentication_features")
telemetryProperties["authentication_features"] = strings.Join(profile.AuthenticationFeatures, ",")
return telemetryProperties, nil
}
func (a *App) GetOrCreateTrueUpReviewStatus() (*model.TrueUpReviewStatus, *model.AppError) {
nextDueDate := utils.GetNextTrueUpReviewDueDate(time.Now())
status, err := a.Srv().Store().TrueUpReview().GetTrueUpReviewStatus(nextDueDate.UnixMilli())
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
a.Log().Warn("Could not find true up review status")
default:
return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.get_status_error", nil, "Could not get true up status records", http.StatusInternalServerError).Wrap(err)
}
status, err = a.Srv().Store().TrueUpReview().CreateTrueUpReviewStatusRecord(&model.TrueUpReviewStatus{DueDate: nextDueDate.UnixMilli(), Completed: false})
if err != nil {
return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.create_error", nil, "Could not create true up status record", http.StatusInternalServerError)
}
}
return status, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"io"
"mime"
"net/http"
"path/filepath"
"strings"
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const minFirstPartSize = 5 * 1024 * 1024 // 5MB
func (a *App) genFileInfoFromReader(name string, file io.ReadSeeker, size int64) (*model.FileInfo, error) {
ext := strings.ToLower(filepath.Ext(name))
info := &model.FileInfo{
Name: name,
MimeType: mime.TypeByExtension(ext),
Size: size,
Extension: ext,
}
if ext != "" {
// The client expects a file extension without the leading period
info.Extension = ext[1:]
}
if info.IsImage() {
config, _, err := a.ch.imgDecoder.DecodeConfig(file)
if err != nil {
return nil, err
}
info.Width = config.Width
info.Height = config.Height
}
return info, nil
}
func (a *App) runPluginsHook(c request.CTX, info *model.FileInfo, file io.Reader) *model.AppError {
filePath := info.Path
// using a pipe to avoid loading the whole file content in memory.
r, w := io.Pipe()
errChan := make(chan *model.AppError, 1)
hookHasRunCh := make(chan struct{})
go func() {
defer w.Close()
defer close(hookHasRunCh)
defer close(errChan)
var rejErr *model.AppError
var once sync.Once
pluginContext := pluginContext(c)
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
once.Do(func() {
hookHasRunCh <- struct{}{}
})
newInfo, rejStr := hooks.FileWillBeUploaded(pluginContext, info, file, w)
if rejStr != "" {
rejErr = model.NewAppError("runPluginsHook", "app.upload.run_plugins_hook.rejected",
map[string]any{"Filename": info.Name, "Reason": rejStr}, "", http.StatusBadRequest)
return false
}
if newInfo != nil {
info = newInfo
}
return true
}, plugin.FileWillBeUploadedID)
if rejErr != nil {
errChan <- rejErr
}
}()
// If the plugin hook has not run we can return early.
if _, ok := <-hookHasRunCh; !ok {
return nil
}
tmpPath := filePath + ".tmp"
written, err := a.WriteFile(r, tmpPath)
if err != nil {
if fileErr := a.RemoveFile(tmpPath); fileErr != nil {
mlog.Warn("Failed to remove file", mlog.Err(fileErr))
}
r.CloseWithError(err) // always returns nil
return err
}
if err = <-errChan; err != nil {
if fileErr := a.RemoveFile(info.Path); fileErr != nil {
mlog.Warn("Failed to remove file", mlog.Err(fileErr))
}
if fileErr := a.RemoveFile(tmpPath); fileErr != nil {
mlog.Warn("Failed to remove file", mlog.Err(fileErr))
}
return err
}
if written > 0 {
info.Size = written
if fileErr := a.MoveFile(tmpPath, info.Path); fileErr != nil {
return model.NewAppError("runPluginsHook", "app.upload.run_plugins_hook.move_fail",
nil, "", http.StatusInternalServerError).Wrap(fileErr)
}
} else {
if fileErr := a.RemoveFile(tmpPath); fileErr != nil {
mlog.Warn("Failed to remove file", mlog.Err(fileErr))
}
}
return nil
}
func (a *App) CreateUploadSession(c request.CTX, us *model.UploadSession) (*model.UploadSession, *model.AppError) {
us.FileOffset = 0
now := time.Now()
us.CreateAt = model.GetMillisForTime(now)
if us.Type == model.UploadTypeAttachment {
us.Path = now.Format("20060102") + "/teams/noteam/channels/" + us.ChannelId + "/users/" + us.UserId + "/" + us.Id + "/" + filepath.Base(us.Filename)
} else if us.Type == model.UploadTypeImport {
us.Path = filepath.Clean(*a.Config().ImportSettings.Directory) + "/" + us.Id + "_" + filepath.Base(us.Filename)
}
if err := us.IsValid(); err != nil {
return nil, err
}
if us.Type == model.UploadTypeAttachment {
channel, err := a.GetChannel(c, us.ChannelId)
if err != nil {
return nil, model.NewAppError("CreateUploadSession", "app.upload.create.incorrect_channel_id.app_error",
map[string]any{"channelId": us.ChannelId}, "", http.StatusBadRequest)
}
if channel.DeleteAt != 0 {
return nil, model.NewAppError("CreateUploadSession", "app.upload.create.cannot_upload_to_deleted_channel.app_error",
map[string]any{"channelId": us.ChannelId}, "", http.StatusBadRequest)
}
}
us, storeErr := a.Srv().Store().UploadSession().Save(us)
if storeErr != nil {
return nil, model.NewAppError("CreateUploadSession", "app.upload.create.save.app_error", nil, "", http.StatusInternalServerError).Wrap(storeErr)
}
return us, nil
}
func (a *App) GetUploadSession(c request.CTX, uploadId string) (*model.UploadSession, *model.AppError) {
us, err := a.Srv().Store().UploadSession().Get(c.Context(), uploadId)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetUpload", "app.upload.get.app_error",
nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetUpload", "app.upload.get.app_error",
nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return us, nil
}
func (a *App) GetUploadSessionsForUser(userID string) ([]*model.UploadSession, *model.AppError) {
uss, err := a.Srv().Store().UploadSession().GetForUser(userID)
if err != nil {
return nil, model.NewAppError("GetUploadsForUser", "app.upload.get_for_user.app_error",
nil, "", http.StatusInternalServerError).Wrap(err)
}
return uss, nil
}
func (a *App) UploadData(c request.CTX, us *model.UploadSession, rd io.Reader) (*model.FileInfo, *model.AppError) {
// prevent more than one caller to upload data at the same time for a given upload session.
// This is to avoid possible inconsistencies.
a.ch.uploadLockMapMut.Lock()
locked := a.ch.uploadLockMap[us.Id]
if locked {
// session lock is already taken, return error.
a.ch.uploadLockMapMut.Unlock()
return nil, model.NewAppError("UploadData", "app.upload.upload_data.concurrent.app_error",
nil, "", http.StatusBadRequest)
}
// grab the session lock.
a.ch.uploadLockMap[us.Id] = true
a.ch.uploadLockMapMut.Unlock()
// reset the session lock on exit.
defer func() {
a.ch.uploadLockMapMut.Lock()
delete(a.ch.uploadLockMap, us.Id)
a.ch.uploadLockMapMut.Unlock()
}()
// fetch the session from store to check for inconsistencies.
c.SetContext(WithMaster(c.Context()))
if storedSession, err := a.GetUploadSession(c, us.Id); err != nil {
return nil, err
} else if us.FileOffset != storedSession.FileOffset {
return nil, model.NewAppError("UploadData", "app.upload.upload_data.concurrent.app_error",
nil, "FileOffset mismatch", http.StatusBadRequest)
}
uploadPath := us.Path
if us.Type == model.UploadTypeImport {
uploadPath += model.IncompleteUploadSuffix
}
// make sure it's not possible to upload more data than what is expected.
lr := &io.LimitedReader{
R: rd,
N: us.FileSize - us.FileOffset,
}
var err *model.AppError
var written int64
if us.FileOffset == 0 {
// new upload
written, err = a.WriteFile(lr, uploadPath)
if err != nil && written == 0 {
return nil, err
}
if written < minFirstPartSize && written != us.FileSize {
a.RemoveFile(uploadPath)
var errStr string
if err != nil {
errStr = err.Error()
}
return nil, model.NewAppError("UploadData", "app.upload.upload_data.first_part_too_small.app_error",
map[string]any{"Size": minFirstPartSize}, errStr, http.StatusBadRequest)
}
} else if us.FileOffset < us.FileSize {
// resume upload
written, err = a.AppendFile(lr, uploadPath)
}
if written > 0 {
us.FileOffset += written
if storeErr := a.Srv().Store().UploadSession().Update(us); storeErr != nil {
return nil, model.NewAppError("UploadData", "app.upload.upload_data.update.app_error", nil, "", http.StatusInternalServerError).Wrap(storeErr)
}
}
if err != nil {
return nil, err
}
// upload is incomplete
if us.FileOffset != us.FileSize {
return nil, nil
}
// upload is done, create FileInfo
file, err := a.FileReader(uploadPath)
if err != nil {
return nil, model.NewAppError("UploadData", "app.upload.upload_data.read_file.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// generate file info
info, genErr := a.genFileInfoFromReader(us.Filename, file, us.FileSize)
file.Close()
if genErr != nil {
return nil, model.NewAppError("UploadData", "app.upload.upload_data.gen_info.app_error", nil, "", http.StatusInternalServerError).Wrap(genErr)
}
info.CreatorId = us.UserId
info.Path = us.Path
info.RemoteId = model.NewString(us.RemoteId)
if us.ReqFileId != "" {
info.Id = us.ReqFileId
}
// run plugins upload hook
if err := a.runPluginsHook(c, info, file); err != nil {
return nil, err
}
// image post-processing
if info.IsImage() && !info.IsSvg() {
if limitErr := checkImageResolutionLimit(info.Width, info.Height, *a.Config().FileSettings.MaxImageResolution); limitErr != nil {
return nil, model.NewAppError("uploadData", "app.upload.upload_data.large_image.app_error",
map[string]any{"Filename": us.Filename, "Width": info.Width, "Height": info.Height}, "", http.StatusBadRequest)
}
nameWithoutExtension := info.Name[:strings.LastIndex(info.Name, ".")]
info.PreviewPath = filepath.Dir(info.Path) + "/" + nameWithoutExtension + "_preview." + getFileExtFromMimeType(info.MimeType)
info.ThumbnailPath = filepath.Dir(info.Path) + "/" + nameWithoutExtension + "_thumb." + getFileExtFromMimeType(info.MimeType)
imgData, fileErr := a.ReadFile(uploadPath)
if fileErr != nil {
return nil, fileErr
}
a.HandleImages([]string{info.PreviewPath}, []string{info.ThumbnailPath}, [][]byte{imgData})
}
if us.Type == model.UploadTypeImport {
if err := a.MoveFile(uploadPath, us.Path); err != nil {
return nil, model.NewAppError("UploadData", "app.upload.upload_data.move_file.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
var storeErr error
if info, storeErr = a.Srv().Store().FileInfo().Save(info); storeErr != nil {
var appErr *model.AppError
switch {
case errors.As(storeErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("uploadData", "app.upload.upload_data.save.app_error", nil, "", http.StatusInternalServerError).Wrap(storeErr)
}
}
if *a.Config().FileSettings.ExtractContent {
infoCopy := *info
a.Srv().Go(func() {
err := a.ExtractContentFromFileInfo(&infoCopy)
if err != nil {
mlog.Error("Failed to extract file content", mlog.Err(err), mlog.String("fileInfoId", infoCopy.Id))
}
})
}
// delete upload session
if storeErr := a.Srv().Store().UploadSession().Delete(us.Id); storeErr != nil {
mlog.Warn("Failed to delete UploadSession", mlog.Err(storeErr))
}
return info, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
// GetPostsUsage returns the total posts count rounded down to the most
// significant digit
func (a *App) GetPostsUsage() (int64, *model.AppError) {
count, err := a.Srv().Store().Post().AnalyticsPostCount(&model.PostCountOptions{ExcludeDeleted: true, UsersPostsOnly: true, AllowFromCache: true})
if err != nil {
return 0, model.NewAppError("GetPostsUsage", "app.post.analytics_posts_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return utils.RoundOffToZeroesResolution(float64(count), 3), nil
}
// GetStorageUsage returns the sum of files' sizes stored on this instance
func (a *App) GetStorageUsage() (int64, *model.AppError) {
usage, err := a.Srv().Store().FileInfo().GetStorageUsage(true, false)
if err != nil {
return 0, model.NewAppError("GetStorageUsage", "app.usage.get_storage_usage.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return usage, nil
}
func (a *App) GetTeamsUsage() (*model.TeamsUsage, *model.AppError) {
usage := &model.TeamsUsage{}
includeDeleted := false
teamCount, err := a.Srv().Store().Team().AnalyticsTeamCount(&model.TeamSearch{IncludeDeleted: &includeDeleted})
if err != nil {
return nil, model.NewAppError("GetTeamsUsage", "app.post.analytics_teams_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
usage.Active = teamCount
allTeams, appErr := a.GetAllTeams()
if appErr != nil {
return nil, appErr
}
cloudArchivedTeamCount := 0
for _, team := range allTeams {
if team.DeleteAt > 0 && team.CloudLimitsArchived {
cloudArchivedTeamCount += 1
}
}
usage.CloudArchived = int64(cloudArchivedTeamCount)
return usage, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"path/filepath"
"strconv"
"strings"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/app/email"
"github.com/mattermost/mattermost-server/v6/server/channels/app/imaging"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/app/users"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mfa"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
TokenTypePasswordRecovery = "password_recovery"
TokenTypeVerifyEmail = "verify_email"
TokenTypeTeamInvitation = "team_invitation"
TokenTypeGuestInvitation = "guest_invitation"
TokenTypeCWSAccess = "cws_access_token"
PasswordRecoverExpiryTime = 1000 * 60 * 60 * 24 // 24 hours
InvitationExpiryTime = 1000 * 60 * 60 * 48 // 48 hours
ImageProfilePixelDimension = 128
)
func (a *App) CreateUserWithToken(c request.CTX, user *model.User, token *model.Token) (*model.User, *model.AppError) {
if err := a.IsUserSignUpAllowed(); err != nil {
return nil, err
}
if token.Type != TokenTypeTeamInvitation && token.Type != TokenTypeGuestInvitation {
return nil, model.NewAppError("CreateUserWithToken", "api.user.create_user.signup_link_invalid.app_error", nil, "", http.StatusBadRequest)
}
if model.GetMillis()-token.CreateAt >= InvitationExpiryTime {
a.DeleteToken(token)
return nil, model.NewAppError("CreateUserWithToken", "api.user.create_user.signup_link_expired.app_error", nil, "", http.StatusBadRequest)
}
tokenData := model.MapFromJSON(strings.NewReader(token.Extra))
team, nErr := a.Srv().Store().Team().Get(tokenData["teamId"])
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("CreateUserWithToken", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("CreateUserWithToken", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
// find the sender id and grab the channels in order to validate
// the sender id still belongs to team and to private channels
senderId := tokenData["senderId"]
channelIds := strings.Split(tokenData["channels"], " ")
// filter the channels the original inviter has still permissions over
channelIds = a.ValidateUserPermissionsOnChannels(c, senderId, channelIds)
channels, nErr := a.Srv().Store().Channel().GetChannelsByIds(channelIds, false)
if nErr != nil {
return nil, model.NewAppError("CreateUserWithToken", "app.channel.get_channels_by_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
emailFromToken := tokenData["email"]
if emailFromToken != user.Email {
return nil, model.NewAppError("CreateUserWithToken", "api.user.create_user.bad_token_email_data.app_error", nil, "", http.StatusBadRequest)
}
user.Email = tokenData["email"]
user.EmailVerified = true
var ruser *model.User
var err *model.AppError
if token.Type == TokenTypeTeamInvitation {
ruser, err = a.CreateUser(c, user)
} else {
ruser, err = a.CreateGuest(c, user)
}
if err != nil {
return nil, err
}
if _, err := a.JoinUserToTeam(c, team, ruser, ""); err != nil {
return nil, err
}
a.AddDirectChannels(c, team.Id, ruser)
if token.Type == TokenTypeGuestInvitation || (token.Type == TokenTypeTeamInvitation && len(channels) > 0) {
for _, channel := range channels {
_, err := a.AddChannelMember(c, ruser.Id, channel, ChannelMemberOpts{})
if err != nil {
c.Logger().Warn("Failed to add channel member", mlog.Err(err))
}
}
}
if err := a.DeleteToken(token); err != nil {
c.Logger().Warn("Error while deleting token", mlog.Err(err))
}
return ruser, nil
}
func (a *App) CreateUserWithInviteId(c request.CTX, user *model.User, inviteId, redirect string) (*model.User, *model.AppError) {
if err := a.IsUserSignUpAllowed(); err != nil {
return nil, err
}
team, nErr := a.Srv().Store().Team().GetByInviteId(inviteId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("CreateUserWithInviteId", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("CreateUserWithInviteId", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if team.IsGroupConstrained() {
return nil, model.NewAppError("CreateUserWithInviteId", "app.team.invite_id.group_constrained.error", nil, "", http.StatusForbidden)
}
if !users.CheckUserDomain(user, team.AllowedDomains) {
return nil, model.NewAppError("CreateUserWithInviteId", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": team.AllowedDomains}, "", http.StatusForbidden)
}
user.EmailVerified = false
ruser, err := a.CreateUser(c, user)
if err != nil {
return nil, err
}
if _, err := a.JoinUserToTeam(c, team, ruser, ""); err != nil {
return nil, err
}
a.AddDirectChannels(c, team.Id, ruser)
if err := a.Srv().EmailService.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
c.Logger().Warn("Failed to send welcome email on create user with inviteId", mlog.Err(err))
}
return ruser, nil
}
func (a *App) CreateUserAsAdmin(c request.CTX, user *model.User, redirect string) (*model.User, *model.AppError) {
ruser, err := a.CreateUser(c, user)
if err != nil {
return nil, err
}
if err := a.Srv().EmailService.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
c.Logger().Warn("Failed to send welcome email to the new user, created by system admin", mlog.Err(err))
}
return ruser, nil
}
func (a *App) CreateUserFromSignup(c request.CTX, user *model.User, redirect string) (*model.User, *model.AppError) {
if err := a.IsUserSignUpAllowed(); err != nil {
return nil, err
}
if !a.IsFirstUserAccount() && !*a.Config().TeamSettings.EnableOpenServer {
err := model.NewAppError("CreateUserFromSignup", "api.user.create_user.no_open_server", nil, "email="+user.Email, http.StatusForbidden)
return nil, err
}
user.EmailVerified = false
ruser, err := a.CreateUser(c, user)
if err != nil {
return nil, err
}
if err := a.Srv().EmailService.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
c.Logger().Warn("Failed to send welcome email on create user from signup", mlog.Err(err))
}
return ruser, nil
}
func (a *App) IsUserSignUpAllowed() *model.AppError {
if !*a.Config().EmailSettings.EnableSignUpWithEmail || !*a.Config().TeamSettings.EnableUserCreation {
err := model.NewAppError("IsUserSignUpAllowed", "api.user.create_user.signup_email_disabled.app_error", nil, "", http.StatusNotImplemented)
return err
}
return nil
}
func (a *App) IsFirstUserAccount() bool {
return a.ch.srv.platform.IsFirstUserAccount()
}
func (a *App) IsFirstAdmin(user *model.User) bool {
if !user.IsSystemAdmin() {
return false
}
adminID, err := a.Srv().Store().User().GetFirstSystemAdminID()
if err != nil {
return false
}
return adminID == user.Id
}
// CreateUser creates a user and sets several fields of the returned User struct to
// their zero values.
func (a *App) CreateUser(c request.CTX, user *model.User) (*model.User, *model.AppError) {
return a.createUserOrGuest(c, user, false)
}
// CreateGuest creates a guest and sets several fields of the returned User struct to
// their zero values.
func (a *App) CreateGuest(c request.CTX, user *model.User) (*model.User, *model.AppError) {
return a.createUserOrGuest(c, user, true)
}
func (a *App) createUserOrGuest(c request.CTX, user *model.User, guest bool) (*model.User, *model.AppError) {
if err := a.isUniqueToGroupNames(user.Username); err != nil {
err.Where = "createUserOrGuest"
return nil, err
}
ruser, nErr := a.ch.srv.userService.CreateUser(user, users.UserCreateOptions{Guest: guest})
if nErr != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
var nfErr *users.ErrInvalidPassword
switch {
case errors.As(nErr, &appErr):
return nil, appErr
case errors.Is(nErr, users.AcceptedDomainError):
return nil, model.NewAppError("createUserOrGuest", "api.user.create_user.accepted_domain.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("createUserOrGuest", "api.user.check_user_password.invalid.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.Is(nErr, users.UserStoreIsEmptyError):
return nil, model.NewAppError("createUserOrGuest", "app.user.store_is_empty.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
case errors.As(nErr, &invErr):
switch invErr.Field {
case "email":
return nil, model.NewAppError("createUserOrGuest", "app.user.save.email_exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case "username":
return nil, model.NewAppError("createUserOrGuest", "app.user.save.username_exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
default:
return nil, model.NewAppError("createUserOrGuest", "app.user.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
}
default:
return nil, model.NewAppError("createUserOrGuest", "app.user.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if user.EmailVerified {
a.InvalidateCacheForUser(ruser.Id)
nUser, err := a.ch.srv.userService.GetUser(ruser.Id)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("createUserOrGuest", MissingAccountError, nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("createUserOrGuest", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
a.sendUpdatedUserEvent(*nUser)
}
recommendedNextStepsPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceRecommendedNextSteps, Name: "hide", Value: "false"}
tutorialStepPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceCategoryTutorialSteps, Name: ruser.Id, Value: "0"}
preferences := model.Preferences{recommendedNextStepsPref, tutorialStepPref}
if a.Config().FeatureFlags.InsightsEnabled {
// We don't want to show the insights intro modal for new users
preferences = append(preferences, model.Preference{UserId: ruser.Id, Category: model.PreferenceCategoryInsights, Name: model.PreferenceNameInsights, Value: "{\"insights_modal_viewed\":true}"})
} else {
preferences = append(preferences, model.Preference{UserId: ruser.Id, Category: model.PreferenceCategoryInsights, Name: model.PreferenceNameInsights, Value: "{\"insights_modal_viewed\":false}"})
}
if err := a.Srv().Store().Preference().Save(preferences); err != nil {
c.Logger().Warn("Encountered error saving user preferences", mlog.Err(err))
}
go a.UpdateViewedProductNoticesForNewUser(ruser.Id)
// This message goes to everyone, so the teamID, channelID and userID are irrelevant
message := model.NewWebSocketEvent(model.WebsocketEventNewUser, "", "", "", nil, "")
message.Add("user_id", ruser.Id)
a.Publish(message)
pluginContext := pluginContext(c)
a.Srv().Go(func() {
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.UserHasBeenCreated(pluginContext, ruser)
return true
}, plugin.UserHasBeenCreatedID)
})
// For cloud yearly subscriptions, if the current user count of the workspace exceeds the number of seats initially purchased
// (plus the “threshold” of 10%), then a subscriptionHistoryEvent object would need to be created and added to the subscriptionHistory
// table in CWS. This is then used to calculate how much the customers have to pay in addition for the extra users. If the
// workspace is currently on a monthly plan, then this function will not do anything.
if a.Channels().License().IsCloud() {
go func(userId string) {
_, err := a.SendSubscriptionHistoryEvent(userId)
if err != nil {
c.Logger().Error("Failed to create/update the SubscriptionHistoryEvent", mlog.Err(err))
}
}(ruser.Id)
}
return ruser, nil
}
func (a *App) CreateOAuthUser(c *request.Context, service string, userData io.Reader, teamID string, tokenUser *model.User) (*model.User, *model.AppError) {
if !*a.Config().TeamSettings.EnableUserCreation {
return nil, model.NewAppError("CreateOAuthUser", "api.user.create_user.disabled.app_error", nil, "", http.StatusNotImplemented)
}
provider, e := a.getSSOProvider(service)
if e != nil {
return nil, e
}
user, err1 := provider.GetUserFromJSON(userData, tokenUser)
if err1 != nil {
return nil, model.NewAppError("CreateOAuthUser", "api.user.create_oauth_user.create.app_error", map[string]any{"Service": service}, "", http.StatusInternalServerError).Wrap(err1)
}
if user.AuthService == "" {
user.AuthService = service
}
found := true
count := 0
for found {
if found = a.ch.srv.userService.IsUsernameTaken(user.Username); found {
user.Username = user.Username + strconv.Itoa(count)
count++
}
}
userByAuth, _ := a.ch.srv.userService.GetUserByAuth(user.AuthData, service)
if userByAuth != nil {
return userByAuth, nil
}
userByEmail, _ := a.ch.srv.userService.GetUserByEmail(user.Email)
if userByEmail != nil {
if userByEmail.AuthService == "" {
return nil, model.NewAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error", map[string]any{"Service": service, "Auth": model.UserAuthServiceEmail}, "email="+user.Email, http.StatusBadRequest)
}
if provider.IsSameUser(userByEmail, user) {
if _, err := a.Srv().Store().User().UpdateAuthData(userByEmail.Id, user.AuthService, user.AuthData, "", false); err != nil {
// if the user is not updated, write a warning to the log, but don't prevent user login
c.Logger().Warn("Error attempting to update user AuthData", mlog.Err(err))
}
return userByEmail, nil
}
return nil, model.NewAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error", map[string]any{"Service": service, "Auth": userByEmail.AuthService}, "email="+user.Email+" authData="+*user.AuthData, http.StatusBadRequest)
}
user.EmailVerified = true
ruser, err := a.CreateUser(c, user)
if err != nil {
return nil, err
}
if teamID != "" {
err = a.AddUserToTeamByTeamId(c, teamID, user)
if err != nil {
return nil, err
}
err = a.AddDirectChannels(c, teamID, user)
if err != nil {
c.Logger().Warn("Failed to add direct channels", mlog.Err(err))
}
}
return ruser, nil
}
func (a *App) GetUser(userID string) (*model.User, *model.AppError) {
user, err := a.ch.srv.userService.GetUser(userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetUser", MissingAccountError, nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetUser", "app.user.get_by_username.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return user, nil
}
func (a *App) GetUsers(userIDs []string) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsers(userIDs)
if err != nil {
return nil, model.NewAppError("GetUsers", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUserByUsername(username string) (*model.User, *model.AppError) {
result, err := a.ch.srv.userService.GetUserByUsername(username)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetUserByUsername", "app.user.get_by_username.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetUserByUsername", "app.user.get_by_username.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return result, nil
}
func (a *App) GetUserByEmail(email string) (*model.User, *model.AppError) {
user, err := a.ch.srv.userService.GetUserByEmail(email)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetUserByEmail", MissingAccountError, nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetUserByEmail", MissingAccountError, nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return user, nil
}
func (a *App) GetUserByAuth(authData *string, authService string) (*model.User, *model.AppError) {
user, err := a.ch.srv.userService.GetUserByAuth(authData, authService)
if err != nil {
var invErr *store.ErrInvalidInput
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("GetUserByAuth", MissingAuthAccountError, nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetUserByAuth", MissingAuthAccountError, nil, "", http.StatusInternalServerError).Wrap(err)
default:
return nil, model.NewAppError("GetUserByAuth", "app.user.get_by_auth.other.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return user, nil
}
func (a *App) GetUsersFromProfiles(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsersFromProfiles(options)
if err != nil {
return nil, model.NewAppError("GetUsers", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUsersPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsersPage(options, asAdmin)
if err != nil {
return nil, model.NewAppError("GetUsersPage", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUsersEtag(restrictionsHash string) string {
return a.ch.srv.userService.GetUsersEtag(restrictionsHash)
}
func (a *App) GetUsersInTeam(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsersInTeam(options)
if err != nil {
return nil, model.NewAppError("GetUsersInTeam", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUsersNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsersNotInTeam(teamID, groupConstrained, offset, limit, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetUsersNotInTeam", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUsersInTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsersInTeamPage(options, asAdmin)
if err != nil {
return nil, model.NewAppError("GetUsersInTeamPage", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersNotInTeamPage(teamID string, groupConstrained bool, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsersNotInTeamPage(teamID, groupConstrained, page*perPage, perPage, asAdmin, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetUsersNotInTeamPage", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersInTeamEtag(teamID string, restrictionsHash string) string {
return a.ch.srv.userService.GetUsersInTeamEtag(teamID, restrictionsHash)
}
func (a *App) GetUsersNotInTeamEtag(teamID string, restrictionsHash string) string {
return a.ch.srv.userService.GetUsersNotInTeamEtag(teamID, restrictionsHash)
}
func (a *App) GetUsersInChannel(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store().User().GetProfilesInChannel(options)
if err != nil {
return nil, model.NewAppError("GetUsersInChannel", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUsersInChannelByStatus(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store().User().GetProfilesInChannelByStatus(options)
if err != nil {
return nil, model.NewAppError("GetUsersInChannelByStatus", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUsersInChannelByAdmin(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store().User().GetProfilesInChannelByAdmin(options)
if err != nil {
return nil, model.NewAppError("GetUsersInChannelByAdmin", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUsersInChannelMap(options *model.UserGetOptions, asAdmin bool) (map[string]*model.User, *model.AppError) {
users, err := a.GetUsersInChannel(options)
if err != nil {
return nil, err
}
userMap := make(map[string]*model.User, len(users))
for _, user := range users {
a.SanitizeProfile(user, asAdmin)
userMap[user.Id] = user
}
return userMap, nil
}
func (a *App) GetUsersInChannelPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
users, err := a.GetUsersInChannel(options)
if err != nil {
return nil, err
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersInChannelPageByStatus(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
users, err := a.GetUsersInChannelByStatus(options)
if err != nil {
return nil, err
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersInChannelPageByAdmin(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
users, err := a.GetUsersInChannelByAdmin(options)
if err != nil {
return nil, err
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersNotInChannel(teamID string, channelID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store().User().GetProfilesNotInChannel(teamID, channelID, groupConstrained, offset, limit, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetUsersNotInChannel", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUsersNotInChannelMap(teamID string, channelID string, groupConstrained bool, offset int, limit int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) (map[string]*model.User, *model.AppError) {
users, err := a.GetUsersNotInChannel(teamID, channelID, groupConstrained, offset, limit, viewRestrictions)
if err != nil {
return nil, err
}
userMap := make(map[string]*model.User, len(users))
for _, user := range users {
a.SanitizeProfile(user, asAdmin)
userMap[user.Id] = user
}
return userMap, nil
}
func (a *App) GetUsersNotInChannelPage(teamID string, channelID string, groupConstrained bool, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
users, err := a.GetUsersNotInChannel(teamID, channelID, groupConstrained, page*perPage, perPage, viewRestrictions)
if err != nil {
return nil, err
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersWithoutTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsersWithoutTeamPage(options, asAdmin)
if err != nil {
return nil, model.NewAppError("GetUsersWithoutTeamPage", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) GetUsersWithoutTeam(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsersWithoutTeam(options)
if err != nil {
return nil, model.NewAppError("GetUsersWithoutTeam", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
// GetTeamGroupUsers returns the users who are associated to the team via GroupTeams and GroupMembers.
func (a *App) GetTeamGroupUsers(teamID string) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store().User().GetTeamGroupUsers(teamID)
if err != nil {
return nil, model.NewAppError("GetTeamGroupUsers", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
// GetChannelGroupUsers returns the users who are associated to the channel via GroupChannels and GroupMembers.
func (a *App) GetChannelGroupUsers(channelID string) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store().User().GetChannelGroupUsers(channelID)
if err != nil {
return nil, model.NewAppError("GetChannelGroupUsers", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUsersByIds(userIDs []string, options *store.UserGetByIdsOpts) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsersByIds(userIDs, options)
if err != nil {
return nil, model.NewAppError("GetUsersByIds", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetUsersByGroupChannelIds(c *request.Context, channelIDs []string, asAdmin bool) (map[string][]*model.User, *model.AppError) {
usersByChannelId, err := a.Srv().Store().User().GetProfileByGroupChannelIdsForUser(c.Session().UserId, channelIDs)
if err != nil {
return nil, model.NewAppError("GetUsersByGroupChannelIds", "app.user.get_profile_by_group_channel_ids_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for channelID, userList := range usersByChannelId {
usersByChannelId[channelID] = a.sanitizeProfiles(userList, asAdmin)
}
return usersByChannelId, nil
}
func (a *App) GetUsersByUsernames(usernames []string, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
users, err := a.ch.srv.userService.GetUsersByUsernames(usernames, &model.UserGetOptions{ViewRestrictions: viewRestrictions})
if err != nil {
return nil, model.NewAppError("GetUsersByUsernames", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return a.sanitizeProfiles(users, asAdmin), nil
}
func (a *App) sanitizeProfiles(users []*model.User, asAdmin bool) []*model.User {
for _, u := range users {
a.SanitizeProfile(u, asAdmin)
}
return users
}
func (a *App) GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError) {
user, appErr := a.GetUser(userID)
if appErr != nil {
return nil, appErr
}
if !*a.Config().ServiceSettings.EnableMultifactorAuthentication {
return nil, model.NewAppError("GenerateMfaSecret", "mfa.mfa_disabled.app_error", nil, "", http.StatusNotImplemented)
}
mfaSecret, err := a.ch.srv.userService.GenerateMfaSecret(user)
if err != nil {
return nil, model.NewAppError("GenerateMfaSecret", "mfa.generate_qr_code.create_code.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return mfaSecret, nil
}
func (a *App) ActivateMfa(userID, token string) *model.AppError {
user, appErr := a.GetUser(userID)
if appErr != nil {
return appErr
}
if user.AuthService != "" && user.AuthService != model.UserAuthServiceLdap {
return model.NewAppError("ActivateMfa", "api.user.activate_mfa.email_and_ldap_only.app_error", nil, "", http.StatusBadRequest)
}
if !*a.Config().ServiceSettings.EnableMultifactorAuthentication {
return model.NewAppError("ActivateMfa", "mfa.mfa_disabled.app_error", nil, "", http.StatusNotImplemented)
}
if err := a.ch.srv.userService.ActivateMfa(user, token); err != nil {
switch {
case errors.Is(err, mfa.InvalidToken):
return model.NewAppError("ActivateMfa", "mfa.activate.bad_token.app_error", nil, "", http.StatusUnauthorized)
default:
return model.NewAppError("ActivateMfa", "mfa.activate.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// Make sure old MFA status is not cached locally or in cluster nodes.
a.InvalidateCacheForUser(userID)
return nil
}
func (a *App) DeactivateMfa(userID string) *model.AppError {
user, appErr := a.GetUser(userID)
if appErr != nil {
return appErr
}
if err := a.ch.srv.userService.DeactivateMfa(user); err != nil {
return model.NewAppError("DeactivateMfa", "mfa.deactivate.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Make sure old MFA status is not cached locally or in cluster nodes.
a.InvalidateCacheForUser(userID)
return nil
}
func (a *App) GetProfileImage(user *model.User) ([]byte, bool, *model.AppError) {
return a.ch.srv.GetProfileImage(user)
}
func (a *App) GetDefaultProfileImage(user *model.User) ([]byte, *model.AppError) {
return a.ch.srv.GetDefaultProfileImage(user)
}
func (a *App) SetDefaultProfileImage(c request.CTX, user *model.User) *model.AppError {
img, appErr := a.GetDefaultProfileImage(user)
if appErr != nil {
return appErr
}
path := getProfileImagePath(user.Id)
if _, err := a.WriteFile(bytes.NewReader(img), path); err != nil {
return err
}
if err := a.Srv().Store().User().ResetLastPictureUpdate(user.Id); err != nil {
c.Logger().Warn("Failed to reset last picture update", mlog.Err(err))
}
a.InvalidateCacheForUser(user.Id)
updatedUser, appErr := a.GetUser(user.Id)
if appErr != nil {
c.Logger().Warn("Error in getting users profile forcing logout", mlog.String("user_id", user.Id), mlog.Err(appErr))
return nil
}
options := a.Config().GetSanitizeOptions()
updatedUser.SanitizeProfile(options)
message := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", "", nil, "")
message.Add("user", updatedUser)
a.Publish(message)
return nil
}
func (a *App) SetProfileImage(c request.CTX, userID string, imageData *multipart.FileHeader) *model.AppError {
file, err := imageData.Open()
if err != nil {
return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.open.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
defer file.Close()
return a.SetProfileImageFromMultiPartFile(c, userID, file)
}
func (a *App) SetProfileImageFromMultiPartFile(c request.CTX, userID string, file multipart.File) *model.AppError {
if limitErr := checkImageLimits(file, *a.Config().FileSettings.MaxImageResolution); limitErr != nil {
return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.check_image_limits.app_error", nil, "", http.StatusBadRequest)
}
return a.SetProfileImageFromFile(c, userID, file)
}
func (a *App) AdjustImage(file io.Reader) (*bytes.Buffer, *model.AppError) {
// Decode image into Image object
img, _, err := a.ch.imgDecoder.Decode(file)
if err != nil {
return nil, model.NewAppError("SetProfileImage", "api.user.upload_profile_user.decode.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
orientation, _ := imaging.GetImageOrientation(file)
img = imaging.MakeImageUpright(img, orientation)
// Scale profile image
profileWidthAndHeight := 128
img = imaging.FillCenter(img, profileWidthAndHeight, profileWidthAndHeight)
buf := new(bytes.Buffer)
err = a.ch.imgEncoder.EncodePNG(buf, img)
if err != nil {
return nil, model.NewAppError("SetProfileImage", "api.user.upload_profile_user.encode.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return buf, nil
}
func (a *App) SetProfileImageFromFile(c request.CTX, userID string, file io.Reader) *model.AppError {
buf, err := a.AdjustImage(file)
if err != nil {
return err
}
path := getProfileImagePath(userID)
if storedData, err := a.ReadFile(path); err == nil && bytes.Equal(storedData, buf.Bytes()) {
return nil
}
if _, err := a.WriteFile(buf, path); err != nil {
return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.upload_profile.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().User().UpdateLastPictureUpdate(userID); err != nil {
c.Logger().Warn("Error with updating last picture update", mlog.Err(err))
}
a.invalidateUserCacheAndPublish(userID)
a.onUserProfileChange(userID)
return nil
}
func (a *App) UpdatePasswordAsUser(c request.CTX, userID, currentPassword, newPassword string) *model.AppError {
user, err := a.GetUser(userID)
if err != nil {
return err
}
if user == nil {
return model.NewAppError("updatePassword", "api.user.update_password.valid_account.app_error", nil, "", http.StatusBadRequest)
}
if user.AuthData != nil && *user.AuthData != "" {
return model.NewAppError("updatePassword", "api.user.update_password.oauth.app_error", nil, "auth_service="+user.AuthService, http.StatusBadRequest)
}
if err := a.DoubleCheckPassword(user, currentPassword); err != nil {
if err.Id == "api.user.check_user_password.invalid.app_error" {
err = model.NewAppError("updatePassword", "api.user.update_password.incorrect.app_error", nil, "", http.StatusBadRequest)
}
return err
}
T := i18n.GetUserTranslations(user.Locale)
return a.UpdatePasswordSendEmail(c, user, newPassword, T("api.user.update_password.menu"))
}
func (a *App) userDeactivated(c request.CTX, userID string) *model.AppError {
a.SetStatusOffline(userID, false)
user, err := a.GetUser(userID)
if err != nil {
return err
}
// when disable a user, userDeactivated is called for the user and the
// bots the user owns. Only notify once, when the user is the owner, not the
// owners bots
if !user.IsBot {
a.notifySysadminsBotOwnerDeactivated(c, userID)
}
if *a.Config().ServiceSettings.DisableBotsWhenOwnerIsDeactivated {
a.disableUserBots(c, userID)
}
return nil
}
func (a *App) invalidateUserChannelMembersCaches(c request.CTX, userID string) *model.AppError {
teamsForUser, err := a.GetTeamsForUser(userID)
if err != nil {
return err
}
for _, team := range teamsForUser {
channelsForUser, err := a.GetChannelsForTeamForUser(c, team.Id, userID, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
})
if err != nil {
return err
}
for _, channel := range channelsForUser {
a.invalidateCacheForChannelMembers(channel.Id)
}
}
return nil
}
func (a *App) UpdateActive(c request.CTX, user *model.User, active bool) (*model.User, *model.AppError) {
user.UpdateAt = model.GetMillis()
if active {
user.DeleteAt = 0
} else {
user.DeleteAt = user.UpdateAt
}
userUpdate, err := a.ch.srv.userService.UpdateUser(user, true)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("UpdateActive", "app.user.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpdateActive", "app.user.update.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
ruser := userUpdate.New
if !active {
if err := a.RevokeAllSessions(ruser.Id); err != nil {
return nil, err
}
if err := a.userDeactivated(c, ruser.Id); err != nil {
return nil, err
}
}
a.invalidateUserChannelMembersCaches(c, user.Id)
a.InvalidateCacheForUser(user.Id)
a.sendUpdatedUserEvent(*ruser)
return ruser, nil
}
func (a *App) DeactivateGuests(c *request.Context) *model.AppError {
userIDs, err := a.ch.srv.userService.DeactivateAllGuests()
if err != nil {
return model.NewAppError("DeactivateGuests", "app.user.update_active_for_multiple_users.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, userID := range userIDs {
if err := a.Srv().Platform().RevokeAllSessions(userID); err != nil {
return model.NewAppError("DeactivateGuests", "app.user.update_active_for_multiple_users.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
for _, userID := range userIDs {
if err := a.userDeactivated(c, userID); err != nil {
return err
}
}
a.Srv().Store().Channel().ClearCaches()
a.Srv().Store().User().ClearCaches()
message := model.NewWebSocketEvent(model.WebsocketEventGuestsDeactivated, "", "", "", nil, "")
a.Publish(message)
return nil
}
func (a *App) GetSanitizeOptions(asAdmin bool) map[string]bool {
return a.ch.srv.userService.GetSanitizeOptions(asAdmin)
}
func (a *App) SanitizeProfile(user *model.User, asAdmin bool) {
options := a.ch.srv.userService.GetSanitizeOptions(asAdmin)
user.SanitizeProfile(options)
}
func (a *App) UpdateUserAsUser(c request.CTX, user *model.User, asAdmin bool) (*model.User, *model.AppError) {
updatedUser, err := a.UpdateUser(c, user, true)
if err != nil {
return nil, err
}
return updatedUser, nil
}
// CheckProviderAttributes returns the empty string if the patch can be applied without
// overriding attributes set by the user's login provider; otherwise, the name of the offending
// field is returned.
func (a *App) CheckProviderAttributes(user *model.User, patch *model.UserPatch) string {
tryingToChange := func(userValue *string, patchValue *string) bool {
return patchValue != nil && *patchValue != *userValue
}
// If any login provider is used, then the username may not be changed
if user.AuthService != "" && tryingToChange(&user.Username, patch.Username) {
return "username"
}
LdapSettings := &a.Config().LdapSettings
SamlSettings := &a.Config().SamlSettings
conflictField := ""
if a.Ldap() != nil &&
(user.IsLDAPUser() || (user.IsSAMLUser() && *SamlSettings.EnableSyncWithLdap)) {
conflictField = a.Ldap().CheckProviderAttributes(LdapSettings, user, patch)
} else if a.Saml() != nil && user.IsSAMLUser() {
conflictField = a.Saml().CheckProviderAttributes(SamlSettings, user, patch)
} else if user.IsOAuthUser() {
if tryingToChange(&user.FirstName, patch.FirstName) || tryingToChange(&user.LastName, patch.LastName) {
conflictField = "full name"
}
}
return conflictField
}
func (a *App) PatchUser(c request.CTX, userID string, patch *model.UserPatch, asAdmin bool) (*model.User, *model.AppError) {
user, err := a.GetUser(userID)
if err != nil {
return nil, err
}
user.Patch(patch)
updatedUser, err := a.UpdateUser(c, user, true)
if err != nil {
return nil, err
}
return updatedUser, nil
}
func (a *App) UpdateUserAuth(userID string, userAuth *model.UserAuth) (*model.UserAuth, *model.AppError) {
userAuth.Password = ""
if _, err := a.Srv().Store().User().UpdateAuthData(userID, userAuth.AuthService, userAuth.AuthData, "", false); err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("UpdateUserAuth", "app.user.update_auth_data.email_exists.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpdateUserAuth", "app.user.update_auth_data.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return userAuth, nil
}
func (a *App) sendUpdatedUserEvent(user model.User) {
// exclude event creator user from admin, member user broadcast
omitUsers := make(map[string]bool, 1)
omitUsers[user.Id] = true
// declare admin and unsanitized copy of user
adminCopyOfUser := user.DeepCopy()
unsanitizedCopyOfUser := user.DeepCopy()
a.SanitizeProfile(adminCopyOfUser, true)
adminMessage := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", "", omitUsers, "")
adminMessage.Add("user", adminCopyOfUser)
adminMessage.GetBroadcast().ContainsSensitiveData = true
a.Publish(adminMessage)
a.SanitizeProfile(&user, false)
message := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", "", omitUsers, "")
message.Add("user", &user)
message.GetBroadcast().ContainsSanitizedData = true
a.Publish(message)
// send unsanitized user to event creator
sourceUserMessage := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", unsanitizedCopyOfUser.Id, nil, "")
sourceUserMessage.Add("user", unsanitizedCopyOfUser)
a.Publish(sourceUserMessage)
}
func (a *App) isUniqueToGroupNames(val string) *model.AppError {
if val == "" {
return nil
}
var notFoundErr *store.ErrNotFound
group, err := a.Srv().Store().Group().GetByName(val, model.GroupSearchOpts{})
if err != nil && !errors.As(err, ¬FoundErr) {
return model.NewAppError("isUniqueToGroupNames", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
if group != nil {
return model.NewAppError("isUniqueToGroupNames", model.NoTranslation, nil, fmt.Sprintf("group name %s exists", val), http.StatusBadRequest)
}
return nil
}
func (a *App) UpdateUser(c request.CTX, user *model.User, sendNotifications bool) (*model.User, *model.AppError) {
prev, err := a.ch.srv.userService.GetUser(user.Id)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("UpdateUser", MissingAccountError, nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("UpdateUser", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if prev.CreateAt != user.CreateAt {
user.CreateAt = prev.CreateAt
}
if user.Username != prev.Username {
if err := a.isUniqueToGroupNames(user.Username); err != nil {
err.Where = "UpdateUser"
return nil, err
}
}
var newEmail string
if user.Email != prev.Email {
if !users.CheckUserDomain(user, *a.Config().TeamSettings.RestrictCreationToDomains) {
if !prev.IsGuest() && !prev.IsLDAPUser() && !prev.IsSAMLUser() {
return nil, model.NewAppError("UpdateUser", "api.user.update_user.accepted_domain.app_error", nil, "", http.StatusBadRequest)
}
}
if !users.CheckUserDomain(user, *a.Config().GuestAccountsSettings.RestrictCreationToDomains) {
if prev.IsGuest() && !prev.IsLDAPUser() && !prev.IsSAMLUser() {
return nil, model.NewAppError("UpdateUser", "api.user.update_user.accepted_guest_domain.app_error", nil, "", http.StatusBadRequest)
}
}
if *a.Config().EmailSettings.RequireEmailVerification {
newEmail = user.Email
// Don't set new eMail on user account if email verification is required, this will be done as a post-verification action
// to avoid users being able to set non-controlled eMails as their account email
if _, appErr := a.GetUserByEmail(newEmail); appErr == nil {
return nil, model.NewAppError("UpdateUser", "app.user.save.email_exists.app_error", nil, "user_id="+user.Id, http.StatusBadRequest)
}
// When a bot is created, prev.Email will be an autogenerated faked email,
// which will not match a CLI email input during bot to user conversions.
// To update a bot users email, do not set the email to the faked email
// stored in prev.Email. Allow using the email defined in the CLI
if !user.IsBot {
user.Email = prev.Email
}
}
}
userUpdate, err := a.ch.srv.userService.UpdateUser(user, false)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
var conErr *store.ErrConflict
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("UpdateUser", "app.user.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &conErr):
if conErr.Resource == "Username" {
return nil, model.NewAppError("UpdateUser", "app.user.save.username_exists.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
return nil, model.NewAppError("UpdateUser", "app.user.save.email_exists.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpdateUser", "app.user.update.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if sendNotifications {
if userUpdate.New.Email != userUpdate.Old.Email || newEmail != "" {
if *a.Config().EmailSettings.RequireEmailVerification {
a.Srv().Go(func() {
if err := a.SendEmailVerification(userUpdate.New, newEmail, ""); err != nil {
c.Logger().Error("Failed to send email verification", mlog.Err(err))
}
})
} else {
a.Srv().Go(func() {
if err := a.Srv().EmailService.SendEmailChangeEmail(userUpdate.Old.Email, userUpdate.New.Email, userUpdate.New.Locale, a.GetSiteURL()); err != nil {
c.Logger().Error("Failed to send email change email", mlog.Err(err))
}
})
}
}
if userUpdate.New.Username != userUpdate.Old.Username {
a.Srv().Go(func() {
if err := a.Srv().EmailService.SendChangeUsernameEmail(userUpdate.New.Username, userUpdate.New.Email, userUpdate.New.Locale, a.GetSiteURL()); err != nil {
c.Logger().Error("Failed to send change username email", mlog.Err(err))
}
})
}
a.sendUpdatedUserEvent(*userUpdate.New)
}
a.InvalidateCacheForUser(user.Id)
a.onUserProfileChange(user.Id)
return userUpdate.New, nil
}
func (a *App) UpdateUserActive(c request.CTX, userID string, active bool) *model.AppError {
user, err := a.GetUser(userID)
if err != nil {
return err
}
if _, err = a.UpdateActive(c, user, active); err != nil {
return err
}
return nil
}
func (a *App) updateUserNotifyProps(userID string, props map[string]string) *model.AppError {
err := a.ch.srv.userService.UpdateUserNotifyProps(userID, props)
if err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return appErr
default:
return model.NewAppError("UpdateUser", "app.user.update.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
a.InvalidateCacheForUser(userID)
a.onUserProfileChange(userID)
return nil
}
func (a *App) UpdateMfa(c request.CTX, activate bool, userID, token string) *model.AppError {
if activate {
if err := a.ActivateMfa(userID, token); err != nil {
return err
}
} else {
if err := a.DeactivateMfa(userID); err != nil {
return err
}
}
a.Srv().Go(func() {
user, err := a.GetUser(userID)
if err != nil {
c.Logger().Error("Failed to get user", mlog.Err(err))
return
}
if err := a.Srv().EmailService.SendMfaChangeEmail(user.Email, activate, user.Locale, a.GetSiteURL()); err != nil {
c.Logger().Error("Failed to send mfa change email", mlog.Err(err))
}
})
return nil
}
func (a *App) UpdatePasswordByUserIdSendEmail(c request.CTX, userID, newPassword, method string) *model.AppError {
user, err := a.GetUser(userID)
if err != nil {
return err
}
return a.UpdatePasswordSendEmail(c, user, newPassword, method)
}
func (a *App) UpdatePassword(user *model.User, newPassword string) *model.AppError {
if err := a.IsPasswordValid(newPassword); err != nil {
return err
}
hashedPassword := model.HashPassword(newPassword)
if err := a.Srv().Store().User().UpdatePassword(user.Id, hashedPassword); err != nil {
return model.NewAppError("UpdatePassword", "api.user.update_password.failed.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.InvalidateCacheForUser(user.Id)
return nil
}
func (a *App) UpdatePasswordSendEmail(c request.CTX, user *model.User, newPassword, method string) *model.AppError {
if err := a.UpdatePassword(user, newPassword); err != nil {
return err
}
a.Srv().Go(func() {
if err := a.Srv().EmailService.SendPasswordChangeEmail(user.Email, method, user.Locale, a.GetSiteURL()); err != nil {
c.Logger().Error("Failed to send password change email", mlog.Err(err))
}
})
return nil
}
func (a *App) UpdateHashedPasswordByUserId(userID, newHashedPassword string) *model.AppError {
user, err := a.GetUser(userID)
if err != nil {
return err
}
return a.UpdateHashedPassword(user, newHashedPassword)
}
func (a *App) UpdateHashedPassword(user *model.User, newHashedPassword string) *model.AppError {
if err := a.Srv().Store().User().UpdatePassword(user.Id, newHashedPassword); err != nil {
return model.NewAppError("UpdatePassword", "api.user.update_password.failed.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.InvalidateCacheForUser(user.Id)
return nil
}
func (a *App) ResetPasswordFromToken(c request.CTX, userSuppliedTokenString, newPassword string) *model.AppError {
return a.resetPasswordFromToken(c, userSuppliedTokenString, newPassword, model.GetMillis())
}
func (a *App) resetPasswordFromToken(c request.CTX, userSuppliedTokenString, newPassword string, nowMilli int64) *model.AppError {
token, err := a.GetPasswordRecoveryToken(userSuppliedTokenString)
if err != nil {
return err
}
if nowMilli-token.CreateAt >= PasswordRecoverExpiryTime {
return model.NewAppError("resetPassword", "api.user.reset_password.link_expired.app_error", nil, "", http.StatusBadRequest)
}
tokenData := struct {
UserId string
Email string
}{}
err2 := json.Unmarshal([]byte(token.Extra), &tokenData)
if err2 != nil {
return model.NewAppError("resetPassword", "api.user.reset_password.token_parse.error", nil, "", http.StatusInternalServerError)
}
user, err := a.GetUser(tokenData.UserId)
if err != nil {
return err
}
if user.Email != tokenData.Email {
return model.NewAppError("resetPassword", "api.user.reset_password.link_expired.app_error", nil, "", http.StatusBadRequest)
}
if user.IsSSOUser() {
return model.NewAppError("ResetPasswordFromCode", "api.user.reset_password.sso.app_error", nil, "userId="+user.Id, http.StatusBadRequest)
}
T := i18n.GetUserTranslations(user.Locale)
if err := a.UpdatePasswordSendEmail(c, user, newPassword, T("api.user.reset_password.method")); err != nil {
return err
}
if err := a.DeleteToken(token); err != nil {
c.Logger().Warn("Failed to delete token", mlog.Err(err))
}
return nil
}
func (a *App) SendPasswordReset(email string, siteURL string) (bool, *model.AppError) {
user, err := a.GetUserByEmail(email)
if err != nil {
return false, nil
}
if user.AuthData != nil && *user.AuthData != "" {
return false, model.NewAppError("SendPasswordReset", "api.user.send_password_reset.sso.app_error", nil, "userId="+user.Id, http.StatusBadRequest)
}
token, err := a.CreatePasswordRecoveryToken(user.Id, user.Email)
if err != nil {
return false, err
}
result, eErr := a.Srv().EmailService.SendPasswordResetEmail(user.Email, token, user.Locale, siteURL)
if eErr != nil {
return result, model.NewAppError("SendPasswordReset", "api.user.send_password_reset.send.app_error", nil, "err="+eErr.Error(), http.StatusInternalServerError)
}
return result, nil
}
func (a *App) CreatePasswordRecoveryToken(userID, email string) (*model.Token, *model.AppError) {
tokenExtra := struct {
UserId string
Email string
}{
userID,
email,
}
jsonData, err := json.Marshal(tokenExtra)
if err != nil {
return nil, model.NewAppError("CreatePasswordRecoveryToken", "api.user.create_password_token.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
token := model.NewToken(TokenTypePasswordRecovery, string(jsonData))
if err := a.Srv().Store().Token().Save(token); err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CreatePasswordRecoveryToken", "app.recover.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return token, nil
}
func (a *App) GetPasswordRecoveryToken(token string) (*model.Token, *model.AppError) {
rtoken, err := a.Srv().Store().Token().GetByToken(token)
if err != nil {
return nil, model.NewAppError("GetPasswordRecoveryToken", "api.user.reset_password.invalid_link.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if rtoken.Type != TokenTypePasswordRecovery {
return nil, model.NewAppError("GetPasswordRecoveryToken", "api.user.reset_password.broken_token.app_error", nil, "", http.StatusBadRequest)
}
return rtoken, nil
}
func (a *App) GetTokenById(token string) (*model.Token, *model.AppError) {
rtoken, err := a.Srv().Store().Token().GetByToken(token)
if err != nil {
var status int
switch err.(type) {
case *store.ErrNotFound:
status = http.StatusNotFound
default:
status = http.StatusInternalServerError
}
return nil, model.NewAppError("GetTokenById", "api.user.create_user.signup_link_invalid.app_error", nil, "", status).Wrap(err)
}
return rtoken, nil
}
func (a *App) DeleteToken(token *model.Token) *model.AppError {
err := a.Srv().Store().Token().Delete(token.Token)
if err != nil {
return model.NewAppError("DeleteToken", "app.recover.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) UpdateUserRoles(c request.CTX, userID string, newRoles string, sendWebSocketEvent bool) (*model.User, *model.AppError) {
user, err := a.GetUser(userID)
if err != nil {
err.StatusCode = http.StatusBadRequest
return nil, err
}
return a.UpdateUserRolesWithUser(c, user, newRoles, sendWebSocketEvent)
}
func (a *App) UpdateUserRolesWithUser(c request.CTX, user *model.User, newRoles string, sendWebSocketEvent bool) (*model.User, *model.AppError) {
if err := a.CheckRolesExist(strings.Fields(newRoles)); err != nil {
return nil, err
}
user.Roles = newRoles
uchan := make(chan store.StoreResult, 1)
go func() {
userUpdate, err := a.Srv().Store().User().Update(user, true)
uchan <- store.StoreResult{Data: userUpdate, NErr: err}
close(uchan)
}()
schan := make(chan store.StoreResult, 1)
go func() {
id, err := a.Srv().Store().Session().UpdateRoles(user.Id, newRoles)
schan <- store.StoreResult{Data: id, NErr: err}
close(schan)
}()
result := <-uchan
if result.NErr != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(result.NErr, &appErr):
return nil, appErr
case errors.As(result.NErr, &invErr):
return nil, model.NewAppError("UpdateUserRoles", "app.user.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(result.NErr)
default:
return nil, model.NewAppError("UpdateUserRoles", "app.user.update.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
}
}
ruser := result.Data.(*model.UserUpdate).New
if result := <-schan; result.NErr != nil {
// soft error since the user roles were still updated
c.Logger().Warn("Failed during updating user roles", mlog.Err(result.NErr))
}
a.InvalidateCacheForUser(user.Id)
a.ClearSessionCacheForUser(user.Id)
if sendWebSocketEvent {
message := model.NewWebSocketEvent(model.WebsocketEventUserRoleUpdated, "", "", user.Id, nil, "")
message.Add("user_id", user.Id)
message.Add("roles", newRoles)
a.Publish(message)
}
return ruser, nil
}
func (a *App) PermanentDeleteUser(c *request.Context, user *model.User) *model.AppError {
c.Logger().Warn("Attempting to permanently delete account", mlog.String("user_id", user.Id), mlog.String("user_email", user.Email))
if user.IsInRole(model.SystemAdminRoleId) {
c.Logger().Warn("You are deleting a user that is a system administrator. You may need to set another account as the system administrator using the command line tools.", mlog.String("user_email", user.Email))
}
if _, err := a.UpdateActive(c, user, false); err != nil {
return err
}
if err := a.Srv().Store().Session().PermanentDeleteSessionsByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.session.permanent_delete_sessions_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().UserAccessToken().DeleteAllForUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.user_access_token.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().OAuth().PermanentDeleteAuthDataByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.oauth.permanent_delete_auth_data_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Webhook().PermanentDeleteIncomingByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.webhooks.permanent_delete_incoming_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Webhook().PermanentDeleteOutgoingByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.webhooks.permanent_delete_outgoing_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Command().PermanentDeleteByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.user.permanentdeleteuser.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Preference().PermanentDeleteByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.preference.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Channel().PermanentDeleteMembersByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.channel.permanent_delete_members_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Group().PermanentDeleteMembersByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.group.permanent_delete_members_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Post().PermanentDeleteByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.post.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Bot().PermanentDelete(user.Id); err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
return model.NewAppError("PermanentDeleteUser", "app.bot.permenent_delete.bad_id", map[string]any{"user_id": invErr.Value}, "", http.StatusBadRequest).Wrap(err)
default: // last fallback in case it doesn't map to an existing app error.
return model.NewAppError("PermanentDeleteUser", "app.bot.permanent_delete.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
infos, err := a.Srv().Store().FileInfo().GetForUser(user.Id)
if err != nil {
c.Logger().Warn("Error getting file list for user from FileInfoStore", mlog.Err(err))
}
for _, info := range infos {
res, err := a.FileExists(info.Path)
if err != nil {
c.Logger().Warn(
"Error checking existence of file",
mlog.String("path", info.Path),
mlog.Err(err),
)
continue
}
if !res {
c.Logger().Warn("File not found", mlog.String("path", info.Path))
continue
}
err = a.RemoveFile(info.Path)
if err != nil {
c.Logger().Warn(
"Unable to remove file",
mlog.String("path", info.Path),
mlog.Err(err),
)
}
}
// delete directory containing user's profile image
profileImageDirectory := getProfileImageDirectory(user.Id)
profileImagePath := getProfileImagePath(user.Id)
resProfileImageExists, errProfileImageExists := a.FileExists(profileImagePath)
fileHandlingErrorsFound := false
if errProfileImageExists != nil {
fileHandlingErrorsFound = true
mlog.Warn(
"Error checking existence of profile image.",
mlog.String("path", profileImagePath),
mlog.Err(errProfileImageExists),
)
}
if resProfileImageExists {
errRemoveDirectory := a.RemoveDirectory(profileImageDirectory)
if errRemoveDirectory != nil {
fileHandlingErrorsFound = true
mlog.Warn(
"Unable to remove profile image directory",
mlog.String("path", profileImageDirectory),
mlog.Err(errRemoveDirectory),
)
}
}
if _, err := a.Srv().Store().FileInfo().PermanentDeleteByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.file_info.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().User().PermanentDelete(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.user.permanent_delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Audit().PermanentDeleteByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.audit.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Team().RemoveAllMembersByUser(user.Id); err != nil {
return model.NewAppError("PermanentDeleteUser", "app.team.remove_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.InvalidateCacheForUser(user.Id)
if fileHandlingErrorsFound {
return model.NewAppError("PermanentDeleteUser", "app.file_info.permanent_delete_by_user.app_error", nil, "Couldn't delete profile image of the user.", http.StatusAccepted)
}
c.Logger().Warn("Permanently deleted account", mlog.String("user_email", user.Email), mlog.String("user_id", user.Id))
return nil
}
func (a *App) PermanentDeleteAllUsers(c *request.Context) *model.AppError {
users, err := a.Srv().Store().User().GetAll()
if err != nil {
return model.NewAppError("PermanentDeleteAllUsers", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range users {
a.PermanentDeleteUser(c, user)
}
return nil
}
func (a *App) SendEmailVerification(user *model.User, newEmail, redirect string) *model.AppError {
token, err := a.Srv().EmailService.CreateVerifyEmailToken(user.Id, newEmail)
if err != nil {
switch {
case errors.Is(err, email.CreateEmailTokenError):
return model.NewAppError("CreateVerifyEmailToken", "api.user.create_email_token.error", nil, "", http.StatusInternalServerError)
default:
return model.NewAppError("CreateVerifyEmailToken", "app.recover.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if _, err := a.GetStatus(user.Id); err != nil {
if err.StatusCode != http.StatusNotFound {
return err
}
eErr := a.Srv().EmailService.SendVerifyEmail(newEmail, user.Locale, a.GetSiteURL(), token.Token, redirect)
if eErr != nil {
return model.NewAppError("SendVerifyEmail", "api.user.send_verify_email_and_forget.failed.error", nil, "", http.StatusInternalServerError).Wrap(eErr)
}
return nil
}
if err := a.Srv().EmailService.SendEmailChangeVerifyEmail(newEmail, user.Locale, a.GetSiteURL(), token.Token); err != nil {
return model.NewAppError("sendEmailChangeVerifyEmail", "api.user.send_email_change_verify_email_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) VerifyEmailFromToken(c request.CTX, userSuppliedTokenString string) *model.AppError {
token, err := a.GetVerifyEmailToken(userSuppliedTokenString)
if err != nil {
return err
}
if model.GetMillis()-token.CreateAt >= PasswordRecoverExpiryTime {
return model.NewAppError("VerifyEmailFromToken", "api.user.verify_email.link_expired.app_error", nil, "", http.StatusBadRequest)
}
tokenData := struct {
UserId string
Email string
}{}
err2 := json.Unmarshal([]byte(token.Extra), &tokenData)
if err2 != nil {
return model.NewAppError("VerifyEmailFromToken", "api.user.verify_email.token_parse.error", nil, "", http.StatusInternalServerError)
}
user, err := a.GetUser(tokenData.UserId)
if err != nil {
return err
}
tokenData.Email = strings.ToLower(tokenData.Email)
if err := a.VerifyUserEmail(tokenData.UserId, tokenData.Email); err != nil {
return err
}
if user.Email != tokenData.Email {
a.Srv().Go(func() {
if err := a.Srv().EmailService.SendEmailChangeEmail(user.Email, tokenData.Email, user.Locale, a.GetSiteURL()); err != nil {
mlog.Error("Failed to send email change email", mlog.Err(err))
}
})
}
if err := a.DeleteToken(token); err != nil {
c.Logger().Warn("Failed to delete token", mlog.Err(err))
}
return nil
}
func (a *App) GetVerifyEmailToken(token string) (*model.Token, *model.AppError) {
rtoken, err := a.Srv().Store().Token().GetByToken(token)
if err != nil {
return nil, model.NewAppError("GetVerifyEmailToken", "api.user.verify_email.bad_link.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
if rtoken.Type != TokenTypeVerifyEmail {
return nil, model.NewAppError("GetVerifyEmailToken", "api.user.verify_email.broken_token.app_error", nil, "", http.StatusBadRequest)
}
return rtoken, nil
}
// GetTotalUsersStats is used for the DM list total
func (a *App) GetTotalUsersStats(viewRestrictions *model.ViewUsersRestrictions) (*model.UsersStats, *model.AppError) {
count, err := a.Srv().Store().User().Count(model.UserCountOptions{
IncludeBotAccounts: true,
ViewRestrictions: viewRestrictions,
})
if err != nil {
return nil, model.NewAppError("GetTotalUsersStats", "app.user.get_total_users_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
stats := &model.UsersStats{
TotalUsersCount: count,
}
return stats, nil
}
// GetFilteredUsersStats is used to get a count of users based on the set of filters supported by UserCountOptions.
func (a *App) GetFilteredUsersStats(options *model.UserCountOptions) (*model.UsersStats, *model.AppError) {
count, err := a.Srv().Store().User().Count(*options)
if err != nil {
return nil, model.NewAppError("GetFilteredUsersStats", "app.user.get_total_users_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
stats := &model.UsersStats{
TotalUsersCount: count,
}
return stats, nil
}
func (a *App) VerifyUserEmail(userID, email string) *model.AppError {
if _, err := a.Srv().Store().User().VerifyEmail(userID, email); err != nil {
return model.NewAppError("VerifyUserEmail", "app.user.verify_email.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.InvalidateCacheForUser(userID)
user, err := a.GetUser(userID)
if err != nil {
return err
}
a.sendUpdatedUserEvent(*user)
return nil
}
func (a *App) SearchUsers(props *model.UserSearch, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
if props.WithoutTeam {
return a.SearchUsersWithoutTeam(props.Term, options)
}
if props.InChannelId != "" {
return a.SearchUsersInChannel(props.InChannelId, props.Term, options)
}
if props.NotInChannelId != "" {
return a.SearchUsersNotInChannel(props.TeamId, props.NotInChannelId, props.Term, options)
}
if props.NotInTeamId != "" {
return a.SearchUsersNotInTeam(props.NotInTeamId, props.Term, options)
}
if props.InGroupId != "" {
return a.SearchUsersInGroup(props.InGroupId, props.Term, options)
}
if props.NotInGroupId != "" {
return a.SearchUsersNotInGroup(props.NotInGroupId, props.Term, options)
}
return a.SearchUsersInTeam(props.TeamId, props.Term, options)
}
func (a *App) SearchUsersInChannel(channelID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store().User().SearchInChannel(channelID, term, options)
if err != nil {
return nil, model.NewAppError("SearchUsersInChannel", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
return users, nil
}
func (a *App) SearchUsersNotInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store().User().SearchNotInChannel(teamID, channelID, term, options)
if err != nil {
return nil, model.NewAppError("SearchUsersNotInChannel", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
return users, nil
}
func (a *App) SearchUsersInTeam(teamID, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store().User().Search(teamID, term, options)
if err != nil {
return nil, model.NewAppError("SearchUsersInTeam", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
return users, nil
}
func (a *App) SearchUsersNotInTeam(notInTeamId string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store().User().SearchNotInTeam(notInTeamId, term, options)
if err != nil {
return nil, model.NewAppError("SearchUsersNotInTeam", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
return users, nil
}
func (a *App) SearchUsersWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store().User().SearchWithoutTeam(term, options)
if err != nil {
return nil, model.NewAppError("SearchUsersWithoutTeam", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
return users, nil
}
func (a *App) SearchUsersInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store().User().SearchInGroup(groupID, term, options)
if err != nil {
return nil, model.NewAppError("SearchUsersInGroup", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
return users, nil
}
func (a *App) SearchUsersNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store().User().SearchNotInGroup(groupID, term, options)
if err != nil {
return nil, model.NewAppError("SearchUsersNotInGroup", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
return users, nil
}
func (a *App) AutocompleteUsersInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, *model.AppError) {
term = strings.TrimSpace(term)
autocomplete, err := a.Srv().Store().User().AutocompleteUsersInChannel(teamID, channelID, term, options)
if err != nil {
return nil, model.NewAppError("AutocompleteUsersInChannel", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range autocomplete.InChannel {
a.SanitizeProfile(user, options.IsAdmin)
}
for _, user := range autocomplete.OutOfChannel {
a.SanitizeProfile(user, options.IsAdmin)
}
return autocomplete, nil
}
func (a *App) AutocompleteUsersInTeam(teamID string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInTeam, *model.AppError) {
term = strings.TrimSpace(term)
users, err := a.Srv().Store().User().Search(teamID, term, options)
if err != nil {
return nil, model.NewAppError("AutocompleteUsersInTeam", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range users {
a.SanitizeProfile(user, options.IsAdmin)
}
autocomplete := &model.UserAutocompleteInTeam{}
autocomplete.InTeam = users
return autocomplete, nil
}
func (a *App) UpdateOAuthUserAttrs(userData io.Reader, user *model.User, provider einterfaces.OAuthProvider, service string, tokenUser *model.User) *model.AppError {
oauthUser, err1 := provider.GetUserFromJSON(userData, tokenUser)
if err1 != nil {
return model.NewAppError("UpdateOAuthUserAttrs", "api.user.update_oauth_user_attrs.get_user.app_error", map[string]any{"Service": service}, "", http.StatusBadRequest).Wrap(err1)
}
userAttrsChanged := false
if oauthUser.Username != user.Username {
if existingUser, _ := a.GetUserByUsername(oauthUser.Username); existingUser == nil {
user.Username = oauthUser.Username
userAttrsChanged = true
}
}
if oauthUser.GetFullName() != user.GetFullName() {
user.FirstName = oauthUser.FirstName
user.LastName = oauthUser.LastName
userAttrsChanged = true
}
if oauthUser.Email != user.Email {
if existingUser, _ := a.GetUserByEmail(oauthUser.Email); existingUser == nil {
user.Email = oauthUser.Email
userAttrsChanged = true
}
}
if user.DeleteAt > 0 {
// Make sure they are not disabled
user.DeleteAt = 0
userAttrsChanged = true
}
if userAttrsChanged {
users, err := a.Srv().Store().User().Update(user, true)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return appErr
case errors.As(err, &invErr):
return model.NewAppError("UpdateOAuthUserAttrs", "app.user.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return model.NewAppError("UpdateOAuthUserAttrs", "app.user.update.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
user = users.New
a.InvalidateCacheForUser(user.Id)
}
return nil
}
func (a *App) RestrictUsersGetByPermissions(userID string, options *model.UserGetOptions) (*model.UserGetOptions, *model.AppError) {
restrictions, err := a.GetViewUsersRestrictions(userID)
if err != nil {
return nil, err
}
options.ViewRestrictions = restrictions
return options, nil
}
// FilterNonGroupTeamMembers returns the subset of the given user IDs of the users who are not members of groups
// associated to the team excluding bots.
func (a *App) FilterNonGroupTeamMembers(userIDs []string, team *model.Team) ([]string, error) {
teamGroupUsers, err := a.GetTeamGroupUsers(team.Id)
if err != nil {
return nil, err
}
return a.filterNonGroupUsers(userIDs, teamGroupUsers)
}
// FilterNonGroupChannelMembers returns the subset of the given user IDs of the users who are not members of groups
// associated to the channel excluding bots
func (a *App) FilterNonGroupChannelMembers(userIDs []string, channel *model.Channel) ([]string, error) {
channelGroupUsers, err := a.GetChannelGroupUsers(channel.Id)
if err != nil {
return nil, err
}
return a.filterNonGroupUsers(userIDs, channelGroupUsers)
}
// filterNonGroupUsers is a helper function that takes a list of user ids and a list of users
// and returns the list of normal users present in userIDs but not in groupUsers.
func (a *App) filterNonGroupUsers(userIDs []string, groupUsers []*model.User) ([]string, error) {
nonMemberIds := []string{}
users, err := a.Srv().Store().User().GetProfileByIds(context.Background(), userIDs, nil, false)
if err != nil {
return nil, err
}
for _, user := range users {
userIsMember := user.IsBot
for _, pu := range groupUsers {
if pu.Id == user.Id {
userIsMember = true
break
}
}
if !userIsMember {
nonMemberIds = append(nonMemberIds, user.Id)
}
}
return nonMemberIds, nil
}
func (a *App) RestrictUsersSearchByPermissions(userID string, options *model.UserSearchOptions) (*model.UserSearchOptions, *model.AppError) {
restrictions, err := a.GetViewUsersRestrictions(userID)
if err != nil {
return nil, err
}
options.ViewRestrictions = restrictions
return options, nil
}
func (a *App) UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError) {
if userID == otherUserId {
return true, nil
}
restrictions, err := a.GetViewUsersRestrictions(userID)
if err != nil {
return false, err
}
if restrictions == nil {
return true, nil
}
if len(restrictions.Teams) > 0 {
result, err := a.Srv().Store().Team().UserBelongsToTeams(otherUserId, restrictions.Teams)
if err != nil {
return false, model.NewAppError("UserCanSeeOtherUser", "app.team.user_belongs_to_teams.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if result {
return true, nil
}
}
if len(restrictions.Channels) > 0 {
result, err := a.userBelongsToChannels(otherUserId, restrictions.Channels)
if err != nil {
return false, err
}
if result {
return true, nil
}
}
return false, nil
}
func (a *App) userBelongsToChannels(userID string, channelIDs []string) (bool, *model.AppError) {
belongs, err := a.Srv().Store().Channel().UserBelongsToChannels(userID, channelIDs)
if err != nil {
return false, model.NewAppError("userBelongsToChannels", "app.channel.user_belongs_to_channels.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return belongs, nil
}
func (a *App) GetViewUsersRestrictions(userID string) (*model.ViewUsersRestrictions, *model.AppError) {
if a.HasPermissionTo(userID, model.PermissionViewMembers) {
return nil, nil
}
teamIDs, nErr := a.Srv().Store().Team().GetUserTeamIds(userID, true)
if nErr != nil {
return nil, model.NewAppError("GetViewUsersRestrictions", "app.team.get_user_team_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
teamIDsWithPermission := []string{}
for _, teamID := range teamIDs {
if a.HasPermissionToTeam(userID, teamID, model.PermissionViewMembers) {
teamIDsWithPermission = append(teamIDsWithPermission, teamID)
}
}
userChannelMembers, err := a.Srv().Store().Channel().GetAllChannelMembersForUser(userID, true, true)
if err != nil {
return nil, model.NewAppError("GetViewUsersRestrictions", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
channelIDs := []string{}
for channelID := range userChannelMembers {
channelIDs = append(channelIDs, channelID)
}
return &model.ViewUsersRestrictions{Teams: teamIDsWithPermission, Channels: channelIDs}, nil
}
// PromoteGuestToUser Convert user's roles and all his membership's roles from
// guest roles to regular user roles.
func (a *App) PromoteGuestToUser(c *request.Context, user *model.User, requestorId string) *model.AppError {
nErr := a.ch.srv.userService.PromoteGuestToUser(user)
a.InvalidateCacheForUser(user.Id)
if nErr != nil {
return model.NewAppError("PromoteGuestToUser", "app.user.promote_guest.user_update.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
userTeams, nErr := a.Srv().Store().Team().GetTeamsByUserId(user.Id)
if nErr != nil {
return model.NewAppError("PromoteGuestToUser", "app.team.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
for _, team := range userTeams {
// Soft error if there is an issue joining the default channels
if err := a.JoinDefaultChannels(c, team.Id, user, false, requestorId); err != nil {
c.Logger().Warn("Failed to join default channels", mlog.String("user_id", user.Id), mlog.String("team_id", team.Id), mlog.String("requestor_id", requestorId), mlog.Err(err))
}
}
promotedUser, err := a.GetUser(user.Id)
if err != nil {
c.Logger().Warn("Failed to get user on promote guest to user", mlog.Err(err))
} else {
a.sendUpdatedUserEvent(*promotedUser)
if uErr := a.ch.srv.platform.UpdateSessionsIsGuest(promotedUser.Id, promotedUser.IsGuest()); uErr != nil {
c.Logger().Warn("Unable to update user sessions", mlog.String("user_id", promotedUser.Id), mlog.Err(uErr))
}
}
teamMembers, err := a.GetTeamMembersForUser(user.Id, "", true)
if err != nil {
c.Logger().Warn("Failed to get team members for user on promote guest to user", mlog.Err(err))
}
for _, member := range teamMembers {
a.sendUpdatedMemberRoleEvent(user.Id, member)
channelMembers, appErr := a.GetChannelMembersForUser(c, member.TeamId, user.Id)
if appErr != nil {
c.Logger().Warn("Failed to get channel members for user on promote guest to user", mlog.Err(appErr))
}
for _, member := range channelMembers {
a.invalidateCacheForChannelMembers(member.ChannelId)
evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", user.Id, nil, "")
memberJSON, jsonErr := json.Marshal(member)
if jsonErr != nil {
return model.NewAppError("PromoteGuestToUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
evt.Add("channelMember", string(memberJSON))
a.Publish(evt)
}
}
a.ClearSessionCacheForUser(user.Id)
return nil
}
// DemoteUserToGuest Convert user's roles and all his membership's roles from
// regular user roles to guest roles.
func (a *App) DemoteUserToGuest(c request.CTX, user *model.User) *model.AppError {
demotedUser, nErr := a.ch.srv.userService.DemoteUserToGuest(user)
a.InvalidateCacheForUser(user.Id)
if nErr != nil {
return model.NewAppError("DemoteUserToGuest", "app.user.demote_user_to_guest.user_update.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
a.sendUpdatedUserEvent(*demotedUser)
if uErr := a.ch.srv.platform.UpdateSessionsIsGuest(demotedUser.Id, demotedUser.IsGuest()); uErr != nil {
c.Logger().Warn("Unable to update user sessions", mlog.String("user_id", demotedUser.Id), mlog.Err(uErr))
}
teamMembers, err := a.GetTeamMembersForUser(user.Id, "", true)
if err != nil {
c.Logger().Warn("Failed to get team members for users on demote user to guest", mlog.Err(err))
}
for _, member := range teamMembers {
a.sendUpdatedMemberRoleEvent(user.Id, member)
channelMembers, appErr := a.GetChannelMembersForUser(c, member.TeamId, user.Id)
if appErr != nil {
c.Logger().Warn("Failed to get channel members for users on demote user to guest", mlog.Err(appErr))
continue
}
for _, member := range channelMembers {
a.invalidateCacheForChannelMembers(member.ChannelId)
evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", user.Id, nil, "")
memberJSON, jsonErr := json.Marshal(member)
if jsonErr != nil {
return model.NewAppError("DemoteUserToGuest", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
evt.Add("channelMember", string(memberJSON))
a.Publish(evt)
}
}
a.ClearSessionCacheForUser(user.Id)
return nil
}
func (a *App) PublishUserTyping(userID, channelID, parentId string) *model.AppError {
omitUsers := make(map[string]bool, 1)
omitUsers[userID] = true
event := model.NewWebSocketEvent(model.WebsocketEventTyping, "", channelID, "", omitUsers, "")
event.Add("parent_id", parentId)
event.Add("user_id", userID)
a.Publish(event)
return nil
}
// invalidateUserCacheAndPublish Invalidates cache for a user and publishes user updated event
func (a *App) invalidateUserCacheAndPublish(userID string) {
a.InvalidateCacheForUser(userID)
user, userErr := a.GetUser(userID)
if userErr != nil {
mlog.Error("Error in getting users profile", mlog.String("user_id", userID), mlog.Err(userErr))
return
}
options := a.Config().GetSanitizeOptions()
user.SanitizeProfile(options)
message := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", "", nil, "")
message.Add("user", user)
a.Publish(message)
}
// GetKnownUsers returns the list of user ids of users with any direct
// relationship with a user. That means any user sharing any channel, including
// direct and group channels.
func (a *App) GetKnownUsers(userID string) ([]string, *model.AppError) {
users, err := a.Srv().Store().User().GetKnownUsers(userID)
if err != nil {
return nil, model.NewAppError("GetKnownUsers", "app.user.get_known_users.get_users.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
// ConvertBotToUser converts a bot to user.
func (a *App) ConvertBotToUser(c request.CTX, bot *model.Bot, userPatch *model.UserPatch, sysadmin bool) (*model.User, *model.AppError) {
user, nErr := a.Srv().Store().User().Get(c.Context(), bot.UserId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("ConvertBotToUser", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("ConvertBotToUser", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if sysadmin && !user.IsInRole(model.SystemAdminRoleId) {
_, appErr := a.UpdateUserRoles(c,
user.Id,
fmt.Sprintf("%s %s", user.Roles, model.SystemAdminRoleId),
false)
if appErr != nil {
return nil, appErr
}
}
user.Patch(userPatch)
user, err := a.UpdateUser(c, user, false)
if err != nil {
return nil, err
}
err = a.UpdatePassword(user, *userPatch.Password)
if err != nil {
return nil, err
}
appErr := a.Srv().Store().Bot().PermanentDelete(bot.UserId)
if appErr != nil {
return nil, model.NewAppError("ConvertBotToUser", "app.user.convert_bot_to_user.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
}
return user, nil
}
func (a *App) GetThreadsForUser(userID, teamID string, options model.GetUserThreadsOpts) (*model.Threads, *model.AppError) {
var result model.Threads
var eg errgroup.Group
postPriorityIsEnabled := a.isPostPriorityEnabled()
if postPriorityIsEnabled {
options.IncludeIsUrgent = true
}
if !options.ThreadsOnly {
eg.Go(func() error {
totalUnreadThreads, err := a.Srv().Store().Thread().GetTotalUnreadThreads(userID, teamID, options)
if err != nil {
return errors.Wrapf(err, "failed to count unread threads for user id=%s", userID)
}
result.TotalUnreadThreads = totalUnreadThreads
return nil
})
// Unread is a legacy flag that caused GetTotalThreads to compute the same value as
// GetTotalUnreadThreads. If unspecified, do this work normally; otherwise, skip,
// and send back duplicate values down below.
if !options.Unread {
eg.Go(func() error {
totalCount, err := a.Srv().Store().Thread().GetTotalThreads(userID, teamID, options)
if err != nil {
return errors.Wrapf(err, "failed to count threads for user id=%s", userID)
}
result.Total = totalCount
return nil
})
}
eg.Go(func() error {
totalUnreadMentions, err := a.Srv().Store().Thread().GetTotalUnreadMentions(userID, teamID, options)
if err != nil {
return errors.Wrapf(err, "failed to count threads for user id=%s", userID)
}
result.TotalUnreadMentions = totalUnreadMentions
return nil
})
if postPriorityIsEnabled {
eg.Go(func() error {
totalUnreadUrgentMentions, err := a.Srv().Store().Thread().GetTotalUnreadUrgentMentions(userID, teamID, options)
if err != nil {
return errors.Wrapf(err, "failed to count urgent mentioned threads for user id=%s", userID)
}
result.TotalUnreadUrgentMentions = totalUnreadUrgentMentions
return nil
})
}
}
if !options.TotalsOnly {
eg.Go(func() error {
threads, err := a.Srv().Store().Thread().GetThreadsForUser(userID, teamID, options)
if err != nil {
return errors.Wrapf(err, "failed to get threads for user id=%s", userID)
}
result.Threads = threads
return nil
})
}
if err := eg.Wait(); err != nil {
return nil, model.NewAppError("GetThreadsForUser", "app.user.get_threads_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if options.Unread {
result.Total = result.TotalUnreadThreads
}
for _, thread := range result.Threads {
a.sanitizeProfiles(thread.Participants, false)
thread.Post.SanitizeProps()
}
return &result, nil
}
func (a *App) GetThreadMembershipForUser(userId, threadId string) (*model.ThreadMembership, *model.AppError) {
threadMembership, nErr := a.Srv().Store().Thread().GetMembershipForUser(userId, threadId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("GetThreadMembershipForUser", "app.user.get_thread_membership_for_user.not_found", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("GetThreadMembershipForUser", "app.user.get_thread_membership_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return threadMembership, nil
}
func (a *App) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool) (*model.ThreadResponse, *model.AppError) {
thread, nErr := a.Srv().Store().Thread().GetThreadForUser(threadMembership, extended, a.isPostPriorityEnabled())
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("GetThreadForUser", "app.user.get_threads_for_user.not_found", nil, "thread not found/followed", http.StatusNotFound)
default:
return nil, model.NewAppError("GetThreadForUser", "app.user.get_threads_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
a.sanitizeProfiles(thread.Participants, false)
thread.Post.SanitizeProps()
return thread, nil
}
func (a *App) UpdateThreadsReadForUser(userID, teamID string) *model.AppError {
nErr := a.Srv().Store().Thread().MarkAllAsReadByTeam(userID, teamID)
if nErr != nil {
return model.NewAppError("UpdateThreadsReadForUser", "app.user.update_threads_read_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
message := model.NewWebSocketEvent(model.WebsocketEventThreadReadChanged, teamID, "", userID, nil, "")
a.Publish(message)
return nil
}
func (a *App) UpdateThreadFollowForUser(userID, teamID, threadID string, state bool) *model.AppError {
opts := store.ThreadMembershipOpts{
Following: state,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: state,
UpdateParticipants: false,
}
_, err := a.Srv().Store().Thread().MaintainMembership(userID, threadID, opts)
if err != nil {
return model.NewAppError("UpdateThreadFollowForUser", "app.user.update_thread_follow_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
thread, err := a.Srv().Store().Thread().Get(threadID)
if err != nil {
return model.NewAppError("UpdateThreadFollowForUser", "app.user.update_thread_follow_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
replyCount := int64(0)
if thread != nil {
replyCount = thread.ReplyCount
}
message := model.NewWebSocketEvent(model.WebsocketEventThreadFollowChanged, teamID, "", userID, nil, "")
message.Add("thread_id", threadID)
message.Add("state", state)
message.Add("reply_count", replyCount)
a.Publish(message)
return nil
}
func (a *App) UpdateThreadFollowForUserFromChannelAdd(c request.CTX, userID, teamID, threadID string) *model.AppError {
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
tm, err := a.Srv().Store().Thread().MaintainMembership(userID, threadID, opts)
if err != nil {
return model.NewAppError("UpdateThreadFollowForUserFromChannelAdd", "app.user.update_thread_follow_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
post, appErr := a.GetSinglePost(threadID, false)
if appErr != nil {
return appErr
}
user, appErr := a.GetUser(userID)
if appErr != nil {
return appErr
}
tm.UnreadMentions, appErr = a.countThreadMentions(c, user, post, teamID, post.CreateAt-1)
if appErr != nil {
return appErr
}
tm.LastViewed = post.CreateAt - 1
_, err = a.Srv().Store().Thread().UpdateMembership(tm)
if err != nil {
return model.NewAppError("UpdateThreadFollowForUserFromChannelAdd", "app.user.update_thread_follow_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
message := model.NewWebSocketEvent(model.WebsocketEventThreadUpdated, teamID, "", userID, nil, "")
userThread, err := a.Srv().Store().Thread().GetThreadForUser(tm, true, a.isPostPriorityEnabled())
if err != nil {
var errNotFound *store.ErrNotFound
if errors.As(err, &errNotFound) {
return nil
}
return model.NewAppError("UpdateThreadFollowForUserFromChannelAdd", "app.user.update_thread_follow_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.sanitizeProfiles(userThread.Participants, false)
userThread.Post.SanitizeProps()
sanitizedPost, appErr := a.SanitizePostMetadataForUser(c, userThread.Post, userID)
if appErr != nil {
return appErr
}
userThread.Post = sanitizedPost
payload, jsonErr := json.Marshal(userThread)
if jsonErr != nil {
return model.NewAppError("UpdateThreadFollowForUserFromChannelAdd", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
message.Add("thread", string(payload))
message.Add("previous_unread_replies", int64(0))
message.Add("previous_unread_mentions", int64(0))
a.Publish(message)
return nil
}
func (a *App) UpdateThreadReadForUserByPost(c request.CTX, currentSessionId, userID, teamID, threadID, postID string) (*model.ThreadResponse, *model.AppError) {
post, err := a.GetSinglePost(postID, false)
if err != nil {
return nil, err
}
if post.RootId != threadID && postID != threadID {
return nil, model.NewAppError("UpdateThreadReadForUser", "app.user.update_thread_read_for_user_by_post.app_error", nil, "", http.StatusBadRequest)
}
return a.UpdateThreadReadForUser(c, currentSessionId, userID, teamID, threadID, post.CreateAt-1)
}
func (a *App) UpdateThreadReadForUser(c request.CTX, currentSessionId, userID, teamID, threadID string, timestamp int64) (*model.ThreadResponse, *model.AppError) {
user, err := a.GetUser(userID)
if err != nil {
return nil, err
}
opts := store.ThreadMembershipOpts{
Following: true,
UpdateFollowing: true,
}
membership, storeErr := a.Srv().Store().Thread().MaintainMembership(userID, threadID, opts)
if storeErr != nil {
return nil, model.NewAppError("UpdateThreadReadForUser", "app.user.update_thread_read_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(storeErr)
}
previousUnreadMentions := membership.UnreadMentions
previousUnreadReplies, nErr := a.Srv().Store().Thread().GetThreadUnreadReplyCount(membership)
if nErr != nil {
return nil, model.NewAppError("UpdateThreadReadForUser", "app.user.update_thread_read_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
post, err := a.GetSinglePost(threadID, false)
if err != nil {
return nil, err
}
membership.UnreadMentions, err = a.countThreadMentions(c, user, post, teamID, timestamp)
if err != nil {
return nil, err
}
_, nErr = a.Srv().Store().Thread().UpdateMembership(membership)
if nErr != nil {
return nil, model.NewAppError("UpdateThreadReadForUser", "app.user.update_thread_read_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
membership.LastViewed = timestamp
nErr = a.Srv().Store().Thread().MarkAsRead(userID, threadID, timestamp)
if nErr != nil {
return nil, model.NewAppError("UpdateThreadReadForUser", "app.user.update_thread_read_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
thread, err := a.GetThreadForUser(membership, false)
if err != nil {
return nil, err
}
// Clear if user has read the messages
if thread.UnreadReplies == 0 && a.IsCRTEnabledForUser(c, userID) {
a.clearPushNotification(currentSessionId, userID, post.ChannelId, threadID)
}
message := model.NewWebSocketEvent(model.WebsocketEventThreadReadChanged, teamID, "", userID, nil, "")
message.Add("thread_id", threadID)
message.Add("timestamp", timestamp)
message.Add("unread_mentions", membership.UnreadMentions)
message.Add("unread_replies", thread.UnreadReplies)
message.Add("previous_unread_mentions", previousUnreadMentions)
message.Add("previous_unread_replies", previousUnreadReplies)
message.Add("channel_id", post.ChannelId)
a.Publish(message)
return thread, nil
}
func (a *App) GetUsersWithInvalidEmails(page int, perPage int) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store().User().GetUsersWithInvalidEmails(page, perPage, *a.Config().TeamSettings.RestrictCreationToDomains)
if err != nil {
return nil, model.NewAppError("GetUsersPage", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func getProfileImagePath(userID string) string {
return filepath.Join("users", userID, "profile.png")
}
func getProfileImageDirectory(userID string) string {
return filepath.Join("users", userID)
}
func (a *App) UserIsFirstAdmin(user *model.User) bool {
if !user.IsSystemAdmin() {
return false
}
systemAdminUsers, errServer := a.Srv().Store().User().GetSystemAdminProfiles()
if errServer != nil {
mlog.Warn("Failed to get system admins to check for first admin from Mattermost.")
return false
}
for _, systemAdminUser := range systemAdminUsers {
systemAdminUser := systemAdminUser
if systemAdminUser.CreateAt < user.CreateAt {
return false
}
}
return true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"fmt"
"strings"
"github.com/avct/uasurfer"
)
var platformNames = map[uasurfer.Platform]string{
uasurfer.PlatformUnknown: "Windows",
uasurfer.PlatformWindows: "Windows",
uasurfer.PlatformMac: "Macintosh",
uasurfer.PlatformLinux: "Linux",
uasurfer.PlatformiPad: "iPad",
uasurfer.PlatformiPhone: "iPhone",
uasurfer.PlatformiPod: "iPod",
uasurfer.PlatformBlackberry: "BlackBerry",
uasurfer.PlatformWindowsPhone: "Windows Phone",
}
func getPlatformName(ua *uasurfer.UserAgent) string {
platform := ua.OS.Platform
name, ok := platformNames[platform]
if !ok {
return platformNames[uasurfer.PlatformUnknown]
}
return name
}
var osNames = map[uasurfer.OSName]string{
uasurfer.OSUnknown: "",
uasurfer.OSWindowsPhone: "Windows Phone",
uasurfer.OSWindows: "Windows",
uasurfer.OSMacOSX: "Mac OS",
uasurfer.OSiOS: "iOS",
uasurfer.OSAndroid: "Android",
uasurfer.OSBlackberry: "BlackBerry",
uasurfer.OSChromeOS: "Chrome OS",
uasurfer.OSKindle: "Kindle",
uasurfer.OSWebOS: "webOS",
uasurfer.OSLinux: "Linux",
}
func getOSName(ua *uasurfer.UserAgent) string {
os := ua.OS
if os.Name == uasurfer.OSWindows {
major := os.Version.Major
minor := os.Version.Minor
switch {
case major == 5 && minor == 0:
return "Windows 2000"
case major == 5 && minor == 1:
return "Windows XP"
case major == 5 && minor == 2:
return "Windows XP x64 Edition"
case major == 6 && minor == 0:
return "Windows Vista"
case major == 6 && minor == 1:
return "Windows 7"
case major == 6 && minor == 2:
return "Windows 8"
case major == 6 && minor == 3:
return "Windows 8.1"
case major == 10:
return "Windows 10"
default:
return "Windows"
}
}
name, ok := osNames[os.Name]
if ok {
return name
}
return osNames[uasurfer.OSUnknown]
}
func getBrowserVersion(ua *uasurfer.UserAgent, userAgentString string) string {
if index := strings.Index(userAgentString, "Mattermost/"); index != -1 {
afterVersion := userAgentString[index+len("Mattermost/"):]
return strings.Fields(afterVersion)[0]
}
if index := strings.Index(userAgentString, "mmctl/"); index != -1 {
afterVersion := userAgentString[index+len("mmctl/"):]
return strings.Fields(afterVersion)[0]
}
if index := strings.Index(userAgentString, "Franz/"); index != -1 {
afterVersion := userAgentString[index+len("Franz/"):]
return strings.Fields(afterVersion)[0]
}
return getUAVersion(ua.Browser.Version)
}
func getUAVersion(version uasurfer.Version) string {
if version.Patch == 0 {
return fmt.Sprintf("%v.%v", version.Major, version.Minor)
}
return fmt.Sprintf("%v.%v.%v", version.Major, version.Minor, version.Patch)
}
var browserNames = map[uasurfer.BrowserName]string{
uasurfer.BrowserUnknown: "Unknown",
uasurfer.BrowserChrome: "Chrome",
uasurfer.BrowserIE: "Internet Explorer",
uasurfer.BrowserSafari: "Safari",
uasurfer.BrowserFirefox: "Firefox",
uasurfer.BrowserAndroid: "Android",
uasurfer.BrowserOpera: "Opera",
uasurfer.BrowserBlackberry: "BlackBerry",
}
func getBrowserName(ua *uasurfer.UserAgent, userAgentString string) string {
browser := ua.Browser.Name
if strings.Contains(userAgentString, "Mattermost") {
return "Desktop App"
}
if strings.Contains(userAgentString, "mmctl") {
return "mmctl"
}
if browser == uasurfer.BrowserIE && ua.Browser.Version.Major > 11 {
return "Edge"
}
if name, ok := browserNames[browser]; ok {
return name
}
return browserNames[uasurfer.BrowserUnknown]
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func (a *App) GetUserTermsOfService(userID string) (*model.UserTermsOfService, *model.AppError) {
u, err := a.Srv().Store().UserTermsOfService().GetByUser(userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetUserTermsOfService", "app.user_terms_of_service.get_by_user.no_rows.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetUserTermsOfService", "app.user_terms_of_service.get_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return u, nil
}
func (a *App) SaveUserTermsOfService(userID, termsOfServiceId string, accepted bool) *model.AppError {
if accepted {
userTermsOfService := &model.UserTermsOfService{
UserId: userID,
TermsOfServiceId: termsOfServiceId,
}
if _, err := a.Srv().Store().UserTermsOfService().Save(userTermsOfService); err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return appErr
default:
return model.NewAppError("SaveUserTermsOfService", "app.user_terms_of_service.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
} else {
if err := a.Srv().Store().UserTermsOfService().Delete(userID, termsOfServiceId); err != nil {
return model.NewAppError("SaveUserTermsOfService", "app.user_terms_of_service.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package users
import "errors"
var (
AcceptedDomainError = errors.New("the email provided does not belong to an accepted domain")
VerifyUserError = errors.New("could not update verify email field")
UserCountError = errors.New("could not get the total number of the users.")
UserCreationDisabledError = errors.New("user creation is not allowed")
UserStoreIsEmptyError = errors.New("could not check if the user store is empty")
DeleteAllAccessDataError = errors.New("could not delete all access data")
DefaultFontError = errors.New("could not get default font")
UserInitialsError = errors.New("could not get user initials")
ImageEncodingError = errors.New("could not encode image")
)
// ErrInvalidPassword indicates an error against the password settings
type ErrInvalidPassword struct {
id string
}
func NewErrInvalidPassword(id string) *ErrInvalidPassword {
return &ErrInvalidPassword{
id: id,
}
}
func (e *ErrInvalidPassword) Error() string {
return "invalid password"
}
func (e *ErrInvalidPassword) Id() string {
return e.id
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package users
import (
"errors"
"strings"
"golang.org/x/crypto/bcrypt"
"github.com/mattermost/mattermost-server/v6/model"
)
func CheckUserPassword(user *model.User, password string) error {
if err := ComparePassword(user.Password, password); err != nil {
return NewErrInvalidPassword("")
}
return nil
}
// HashPassword generates a hash using the bcrypt.GenerateFromPassword
func HashPassword(password string) string {
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
if err != nil {
panic(err)
}
return string(hash)
}
func ComparePassword(hash string, password string) error {
if password == "" || hash == "" {
return errors.New("empty password or hash")
}
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
}
func (us *UserService) isPasswordValid(password string) error {
return IsPasswordValidWithSettings(password, &us.config().PasswordSettings)
}
// IsPasswordValidWithSettings is a utility functions that checks if the given password
// conforms to the password settings. It returns the error id as error value.
func IsPasswordValidWithSettings(password string, settings *model.PasswordSettings) error {
id := "model.user.is_valid.pwd"
isError := false
if len(password) < *settings.MinimumLength || len(password) > model.PasswordMaximumLength {
isError = true
}
if *settings.Lowercase {
if !strings.ContainsAny(password, model.LowercaseLetters) {
isError = true
}
id = id + "_lowercase"
}
if *settings.Uppercase {
if !strings.ContainsAny(password, model.UppercaseLetters) {
isError = true
}
id = id + "_uppercase"
}
if *settings.Number {
if !strings.ContainsAny(password, model.NUMBERS) {
isError = true
}
id = id + "_number"
}
if *settings.Symbol {
if !strings.ContainsAny(password, model.SYMBOLS) {
isError = true
}
id = id + "_symbol"
}
if isError {
return NewErrInvalidPassword(id + ".app_error")
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package users
import (
"bytes"
"hash/fnv"
"image"
"image/color"
"image/draw"
"image/png"
"io"
"os"
"path"
"path/filepath"
"strings"
"github.com/golang/freetype"
"github.com/golang/freetype/truetype"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
)
const (
imageProfilePixelDimension = 128
)
func (us *UserService) GetProfileImage(user *model.User) ([]byte, bool, error) {
if *us.config().FileSettings.DriverName == "" {
img, err := us.GetDefaultProfileImage(user)
if err != nil {
return nil, false, err
}
return img, false, nil
}
path := path.Join("users", user.Id, "profile.png")
data, err := us.ReadFile(path)
if err != nil {
img, appErr := us.GetDefaultProfileImage(user)
if appErr != nil {
return nil, false, appErr
}
if user.LastPictureUpdate == 0 {
if _, err := us.writeFile(bytes.NewReader(img), path); err != nil {
return nil, false, err
}
}
return img, true, nil
}
return data, false, nil
}
func (us *UserService) FileBackend() (filestore.FileBackend, error) {
license := us.license()
insecure := us.config().ServiceSettings.EnableInsecureOutgoingConnections
backend, err := filestore.NewFileBackend(us.config().FileSettings.ToFileBackendSettings(license != nil && *license.Features.Compliance, insecure != nil && *insecure))
if err != nil {
return nil, err
}
return backend, nil
}
func (us *UserService) ReadFile(path string) ([]byte, error) {
backend, err := us.FileBackend()
if err != nil {
return nil, err
}
result, nErr := backend.ReadFile(path)
if nErr != nil {
return nil, nErr
}
return result, nil
}
func (us *UserService) writeFile(fr io.Reader, path string) (int64, error) {
backend, err := us.FileBackend()
if err != nil {
return 0, err
}
result, nErr := backend.WriteFile(fr, path)
if nErr != nil {
return result, nErr
}
return result, nil
}
func (us *UserService) GetDefaultProfileImage(user *model.User) ([]byte, error) {
if user.IsBot {
return botDefaultImage, nil
}
return createProfileImage(user.Username, user.Id, *us.config().FileSettings.InitialFont)
}
func createProfileImage(username string, userID string, initialFont string) ([]byte, error) {
colors := []color.NRGBA{
{197, 8, 126, 255},
{227, 207, 18, 255},
{28, 181, 105, 255},
{35, 188, 224, 255},
{116, 49, 196, 255},
{197, 8, 126, 255},
{197, 19, 19, 255},
{250, 134, 6, 255},
{227, 207, 18, 255},
{123, 201, 71, 255},
{28, 181, 105, 255},
{35, 188, 224, 255},
{116, 49, 196, 255},
{197, 8, 126, 255},
{197, 19, 19, 255},
{250, 134, 6, 255},
{227, 207, 18, 255},
{123, 201, 71, 255},
{28, 181, 105, 255},
{35, 188, 224, 255},
{116, 49, 196, 255},
{197, 8, 126, 255},
{197, 19, 19, 255},
{250, 134, 6, 255},
{227, 207, 18, 255},
{123, 201, 71, 255},
}
h := fnv.New32a()
h.Write([]byte(userID))
seed := h.Sum32()
initial := string(strings.ToUpper(username)[0])
font, err := getFont(initialFont)
if err != nil {
return nil, DefaultFontError
}
color := colors[int64(seed)%int64(len(colors))]
dstImg := image.NewRGBA(image.Rect(0, 0, imageProfilePixelDimension, imageProfilePixelDimension))
srcImg := image.White
draw.Draw(dstImg, dstImg.Bounds(), &image.Uniform{color}, image.Point{}, draw.Src)
size := float64(imageProfilePixelDimension / 2)
c := freetype.NewContext()
c.SetFont(font)
c.SetFontSize(size)
c.SetClip(dstImg.Bounds())
c.SetDst(dstImg)
c.SetSrc(srcImg)
pt := freetype.Pt(imageProfilePixelDimension/5, imageProfilePixelDimension*2/3)
_, err = c.DrawString(initial, pt)
if err != nil {
return nil, UserInitialsError
}
buf := new(bytes.Buffer)
enc := png.Encoder{
CompressionLevel: png.BestCompression,
}
if imgErr := enc.Encode(buf, dstImg); imgErr != nil {
return nil, ImageEncodingError
}
return buf.Bytes(), nil
}
func getFont(initialFont string) (*truetype.Font, error) {
// Some people have the old default font still set, so just treat that as if they're using the new default
if initialFont == "luximbi.ttf" {
initialFont = "nunito-bold.ttf"
}
fontDir, _ := fileutils.FindDir("fonts")
fontBytes, err := os.ReadFile(filepath.Join(fontDir, initialFont))
if err != nil {
return nil, err
}
return freetype.ParseFont(fontBytes)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package users
import (
"errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type UserService struct {
store store.UserStore
sessionStore store.SessionStore
oAuthStore store.OAuthStore
metrics einterfaces.MetricsInterface
cluster einterfaces.ClusterInterface
config func() *model.Config
license func() *model.License
}
// ServiceConfig is used to initialize the UserService.
type ServiceConfig struct {
// Mandatory fields
UserStore store.UserStore
SessionStore store.SessionStore
OAuthStore store.OAuthStore
ConfigFn func() *model.Config
LicenseFn func() *model.License
// Optional fields
Metrics einterfaces.MetricsInterface
Cluster einterfaces.ClusterInterface
}
func New(c ServiceConfig) (*UserService, error) {
if err := c.validate(); err != nil {
return nil, err
}
return &UserService{
store: c.UserStore,
sessionStore: c.SessionStore,
oAuthStore: c.OAuthStore,
config: c.ConfigFn,
license: c.LicenseFn,
metrics: c.Metrics,
cluster: c.Cluster,
}, nil
}
func (c *ServiceConfig) validate() error {
if c.ConfigFn == nil || c.UserStore == nil || c.SessionStore == nil || c.OAuthStore == nil || c.LicenseFn == nil {
return errors.New("required parameters are not provided")
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package users
import (
"context"
"encoding/base64"
"fmt"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mfa"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/pkg/errors"
)
type UserCreateOptions struct {
Guest bool
FromImport bool
}
// CreateUser creates a user
func (us *UserService) CreateUser(user *model.User, opts UserCreateOptions) (*model.User, error) {
if opts.FromImport {
return us.createUser(user)
}
user.Roles = model.SystemUserRoleId
if opts.Guest {
user.Roles = model.SystemGuestRoleId
}
if !user.IsLDAPUser() && !user.IsSAMLUser() && !user.IsGuest() && !CheckUserDomain(user, *us.config().TeamSettings.RestrictCreationToDomains) {
return nil, AcceptedDomainError
}
if !user.IsLDAPUser() && !user.IsSAMLUser() && user.IsGuest() && !CheckUserDomain(user, *us.config().GuestAccountsSettings.RestrictCreationToDomains) {
return nil, AcceptedDomainError
}
// Below is a special case where the first user in the entire
// system is granted the system_admin role
if ok, err := us.store.IsEmpty(true); err != nil {
return nil, errors.Wrap(UserStoreIsEmptyError, err.Error())
} else if ok {
user.Roles = model.SystemAdminRoleId + " " + model.SystemUserRoleId
}
if _, ok := i18n.GetSupportedLocales()[user.Locale]; !ok {
user.Locale = *us.config().LocalizationSettings.DefaultClientLocale
}
return us.createUser(user)
}
func (us *UserService) createUser(user *model.User) (*model.User, error) {
user.MakeNonNil()
if err := us.isPasswordValid(user.Password); user.AuthService == "" && err != nil {
return nil, err
}
ruser, err := us.store.Save(user)
if err != nil {
return nil, err
}
if user.EmailVerified {
if err := us.verifyUserEmail(ruser.Id, user.Email); err != nil {
mlog.Warn("Failed to set email verified", mlog.Err(err))
}
}
// Determine whether to send the created user a welcome email
ruser.DisableWelcomeEmail = user.DisableWelcomeEmail
ruser.Sanitize(map[string]bool{})
return ruser, nil
}
func (us *UserService) verifyUserEmail(userID, email string) error {
if _, err := us.store.VerifyEmail(userID, email); err != nil {
return VerifyUserError
}
return nil
}
func (us *UserService) GetUser(userID string) (*model.User, error) {
return us.store.Get(context.Background(), userID)
}
func (us *UserService) GetUsers(userIDs []string) ([]*model.User, error) {
return us.store.GetMany(context.Background(), userIDs)
}
func (us *UserService) GetUserByUsername(username string) (*model.User, error) {
return us.store.GetByUsername(username)
}
func (us *UserService) GetUserByEmail(email string) (*model.User, error) {
return us.store.GetByEmail(email)
}
func (us *UserService) GetUserByAuth(authData *string, authService string) (*model.User, error) {
return us.store.GetByAuth(authData, authService)
}
func (us *UserService) GetUsersFromProfiles(options *model.UserGetOptions) ([]*model.User, error) {
return us.store.GetAllProfiles(options)
}
func (us *UserService) GetUsersByUsernames(usernames []string, options *model.UserGetOptions) ([]*model.User, error) {
return us.store.GetProfilesByUsernames(usernames, options.ViewRestrictions)
}
func (us *UserService) GetUsersPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, error) {
users, err := us.GetUsersFromProfiles(options)
if err != nil {
return nil, err
}
return us.sanitizeProfiles(users, asAdmin), nil
}
func (us *UserService) GetUsersEtag(restrictionsHash string) string {
return fmt.Sprintf("%v.%v.%v.%v", us.store.GetEtagForAllProfiles(), us.config().PrivacySettings.ShowFullName, us.config().PrivacySettings.ShowEmailAddress, restrictionsHash)
}
func (us *UserService) GetUsersByIds(userIDs []string, options *store.UserGetByIdsOpts) ([]*model.User, error) {
allowFromCache := options.ViewRestrictions == nil
users, err := us.store.GetProfileByIds(context.Background(), userIDs, options, allowFromCache)
if err != nil {
return nil, err
}
return us.sanitizeProfiles(users, options.IsAdmin), nil
}
func (us *UserService) GetUsersInTeam(options *model.UserGetOptions) ([]*model.User, error) {
return us.store.GetProfiles(options)
}
func (us *UserService) GetUsersNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
return us.store.GetProfilesNotInTeam(teamID, groupConstrained, offset, limit, viewRestrictions)
}
func (us *UserService) GetUsersInTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, error) {
users, err := us.GetUsersInTeam(options)
if err != nil {
return nil, err
}
return us.sanitizeProfiles(users, asAdmin), nil
}
func (us *UserService) GetUsersNotInTeamPage(teamID string, groupConstrained bool, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
users, err := us.GetUsersNotInTeam(teamID, groupConstrained, page*perPage, perPage, viewRestrictions)
if err != nil {
return nil, err
}
return us.sanitizeProfiles(users, asAdmin), nil
}
func (us *UserService) GetUsersInTeamEtag(teamID string, restrictionsHash string) string {
return fmt.Sprintf("%v.%v.%v.%v", us.store.GetEtagForProfiles(teamID), us.config().PrivacySettings.ShowFullName, us.config().PrivacySettings.ShowEmailAddress, restrictionsHash)
}
func (us *UserService) GetUsersNotInTeamEtag(teamID string, restrictionsHash string) string {
return fmt.Sprintf("%v.%v.%v.%v", us.store.GetEtagForProfilesNotInTeam(teamID), us.config().PrivacySettings.ShowFullName, us.config().PrivacySettings.ShowEmailAddress, restrictionsHash)
}
func (us *UserService) GetUsersWithoutTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, error) {
users, err := us.GetUsersWithoutTeam(options)
if err != nil {
return nil, err
}
return us.sanitizeProfiles(users, asAdmin), nil
}
func (us *UserService) GetUsersWithoutTeam(options *model.UserGetOptions) ([]*model.User, error) {
users, err := us.store.GetProfilesWithoutTeam(options)
if err != nil {
return nil, err
}
return users, nil
}
func (us *UserService) UpdateUser(user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error) {
return us.store.Update(user, allowRoleUpdate)
}
func (us *UserService) UpdateUserNotifyProps(userID string, props map[string]string) error {
return us.store.UpdateNotifyProps(userID, props)
}
func (us *UserService) DeactivateAllGuests() ([]string, error) {
users, err := us.store.DeactivateGuests()
if err != nil {
return nil, err
}
return users, nil
}
func (us *UserService) InvalidateCacheForUser(userID string) {
us.store.InvalidateProfilesInChannelCacheByUser(userID)
us.store.InvalidateProfileCacheForUser(userID)
if us.cluster != nil {
msg := &model.ClusterMessage{
Event: model.ClusterEventInvalidateCacheForUser,
SendType: model.ClusterSendBestEffort,
Data: []byte(userID),
}
us.cluster.SendClusterMessage(msg)
}
}
func (us *UserService) GenerateMfaSecret(user *model.User) (*model.MfaSecret, error) {
secret, img, err := mfa.New(us.store).GenerateSecret(*us.config().ServiceSettings.SiteURL, user.Email, user.Id)
if err != nil {
return nil, err
}
// Make sure the old secret is not cached on any cluster nodes.
us.InvalidateCacheForUser(user.Id)
mfaSecret := &model.MfaSecret{Secret: secret, QRCode: base64.StdEncoding.EncodeToString(img)}
return mfaSecret, nil
}
func (us *UserService) ActivateMfa(user *model.User, token string) error {
return mfa.New(us.store).Activate(user.MfaSecret, user.Id, token)
}
func (us *UserService) DeactivateMfa(user *model.User) error {
return mfa.New(us.store).Deactivate(user.Id)
}
func (us *UserService) PromoteGuestToUser(user *model.User) error {
return us.store.PromoteGuestToUser(user.Id)
}
func (us *UserService) DemoteUserToGuest(user *model.User) (*model.User, error) {
return us.store.DemoteUserToGuest(user.Id)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package users
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
)
// CheckUserDomain checks that a user's email domain matches a list of space-delimited domains as a string.
func CheckUserDomain(user *model.User, domains string) bool {
return CheckEmailDomain(user.Email, domains)
}
// CheckEmailDomain checks that an email domain matches a list of space-delimited domains as a string.
func CheckEmailDomain(email string, domains string) bool {
if domains == "" {
return true
}
domainArray := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(domains, "@", " ", -1), ",", " ", -1))))
for _, d := range domainArray {
if strings.HasSuffix(strings.ToLower(email), "@"+d) {
return true
}
}
return false
}
func (us *UserService) sanitizeProfiles(users []*model.User, asAdmin bool) []*model.User {
for _, u := range users {
us.SanitizeProfile(u, asAdmin)
}
return users
}
func (us *UserService) SanitizeProfile(user *model.User, asAdmin bool) {
options := us.GetSanitizeOptions(asAdmin)
user.SanitizeProfile(options)
}
func (us *UserService) GetSanitizeOptions(asAdmin bool) map[string]bool {
options := us.config().GetSanitizeOptions()
if asAdmin {
options["email"] = true
options["fullname"] = true
options["authservice"] = true
}
return options
}
// IsUsernameTaken checks if the username is already used by another user. Return false if the username is invalid.
func (us *UserService) IsUsernameTaken(name string) bool {
if !model.IsValidUsername(name) {
return false
}
if _, err := us.store.GetByUsername(name); err != nil {
return false
}
return true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
)
// PopulateWebConnConfig checks if the connection id already exists in the hub,
// and if so, accordingly populates the other fields of the webconn.
func (a *App) PopulateWebConnConfig(s *model.Session, cfg *platform.WebConnConfig, seqVal string) (*platform.WebConnConfig, error) {
return a.Srv().Platform().PopulateWebConnConfig(s, cfg, seqVal)
}
// NewWebConn returns a new WebConn instance.
func (a *App) NewWebConn(cfg *platform.WebConnConfig) *platform.WebConn {
return a.Srv().Platform().NewWebConn(cfg, a, a.ch)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
)
func (a *App) TotalWebsocketConnections() int {
return a.Srv().Platform().TotalWebsocketConnections()
}
func (a *App) GetHubForUserId(userID string) *platform.Hub {
return a.Srv().Platform().GetHubForUserId(userID)
}
// HubRegister registers a connection to a hub.
func (a *App) HubRegister(webConn *platform.WebConn) {
a.Srv().Platform().HubRegister(webConn)
}
// HubUnregister unregisters a connection from a hub.
func (a *App) HubUnregister(webConn *platform.WebConn) {
a.Srv().Platform().HubUnregister(webConn)
}
func (a *App) Publish(message *model.WebSocketEvent) {
a.Srv().Platform().Publish(message)
}
func (ch *Channels) Publish(message *model.WebSocketEvent) {
ch.srv.Platform().Publish(message)
}
func (a *App) invalidateCacheForChannelMembers(channelID string) {
a.Srv().Platform().InvalidateCacheForChannelMembers(channelID)
}
func (a *App) invalidateCacheForChannelMembersNotifyProps(channelID string) {
a.Srv().Platform().InvalidateCacheForChannelMembersNotifyProps(channelID)
}
func (a *App) invalidateCacheForChannelPosts(channelID string) {
a.Srv().Platform().InvalidateCacheForChannelPosts(channelID)
}
func (a *App) InvalidateCacheForUser(userID string) {
a.Srv().Platform().InvalidateCacheForUser(userID)
}
func (a *App) invalidateCacheForUserTeams(userID string) {
a.Srv().Platform().InvalidateCacheForUserTeams(userID)
}
// UpdateWebConnUserActivity sets the LastUserActivityAt of the hub for the given session.
func (a *App) UpdateWebConnUserActivity(session model.Session, activityAt int64) {
a.Srv().Platform().UpdateWebConnUserActivity(session, activityAt)
}
// SessionIsRegistered determines if a specific session has been registered
func (a *App) SessionIsRegistered(session model.Session) bool {
return a.Srv().Platform().SessionIsRegistered(session)
}
func (a *App) CheckWebConn(userID, connectionID string) *platform.CheckConnResult {
return a.Srv().Platform().CheckWebConn(userID, connectionID)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"regexp"
"strings"
"unicode/utf8"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
TriggerwordsExactMatch = 0
TriggerwordsStartsWith = 1
MaxIntegrationResponseSize = 1024 * 1024 // Posts can be <100KB at most, so this is likely more than enough
)
func (a *App) handleWebhookEvents(c request.CTX, post *model.Post, team *model.Team, channel *model.Channel, user *model.User) *model.AppError {
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
return nil
}
if channel.Type != model.ChannelTypeOpen {
return nil
}
hooks, err := a.Srv().Store().Webhook().GetOutgoingByTeam(team.Id, -1, -1)
if err != nil {
return model.NewAppError("handleWebhookEvents", "app.webhooks.get_outgoing_by_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(hooks) == 0 {
return nil
}
var firstWord, triggerWord string
splitWords := strings.Fields(post.Message)
if len(splitWords) > 0 {
firstWord = splitWords[0]
}
relevantHooks := []*model.OutgoingWebhook{}
for _, hook := range hooks {
if hook.ChannelId == post.ChannelId || hook.ChannelId == "" {
if hook.ChannelId == post.ChannelId && len(hook.TriggerWords) == 0 {
relevantHooks = append(relevantHooks, hook)
triggerWord = ""
} else if hook.TriggerWhen == TriggerwordsExactMatch && hook.TriggerWordExactMatch(firstWord) {
relevantHooks = append(relevantHooks, hook)
triggerWord = hook.GetTriggerWord(firstWord, true)
} else if hook.TriggerWhen == TriggerwordsStartsWith && hook.TriggerWordStartsWith(firstWord) {
relevantHooks = append(relevantHooks, hook)
triggerWord = hook.GetTriggerWord(firstWord, false)
}
}
}
for _, hook := range relevantHooks {
payload := &model.OutgoingWebhookPayload{
Token: hook.Token,
TeamId: hook.TeamId,
TeamDomain: team.Name,
ChannelId: post.ChannelId,
ChannelName: channel.Name,
Timestamp: post.CreateAt,
UserId: post.UserId,
UserName: user.Username,
PostId: post.Id,
Text: post.Message,
TriggerWord: triggerWord,
FileIds: strings.Join(post.FileIds, ","),
}
a.Srv().Go(func(hook *model.OutgoingWebhook) func() {
return func() {
a.TriggerWebhook(c, payload, hook, post, channel)
}
}(hook))
}
return nil
}
func (a *App) TriggerWebhook(c request.CTX, payload *model.OutgoingWebhookPayload, hook *model.OutgoingWebhook, post *model.Post, channel *model.Channel) {
var body io.Reader
var contentType string
if hook.ContentType == "application/json" {
js, err := json.Marshal(payload)
if err != nil {
c.Logger().Warn("Failed to encode to JSON", mlog.Err(err))
}
body = bytes.NewReader(js)
contentType = "application/json"
} else {
body = strings.NewReader(payload.ToFormValues())
contentType = "application/x-www-form-urlencoded"
}
for i := range hook.CallbackURLs {
// Get the callback URL by index to properly capture it for the go func
url := hook.CallbackURLs[i]
a.Srv().Go(func() {
webhookResp, err := a.doOutgoingWebhookRequest(url, body, contentType)
if err != nil {
c.Logger().Error("Event POST failed.", mlog.Err(err))
return
}
if webhookResp != nil && (webhookResp.Text != nil || len(webhookResp.Attachments) > 0) {
postRootId := ""
if webhookResp.ResponseType == model.OutgoingHookResponseTypeComment {
postRootId = post.Id
}
if len(webhookResp.Props) == 0 {
webhookResp.Props = make(model.StringInterface)
}
webhookResp.Props["webhook_display_name"] = hook.DisplayName
text := ""
if webhookResp.Text != nil {
text = a.ProcessSlackText(*webhookResp.Text)
}
webhookResp.Attachments = a.ProcessSlackAttachments(webhookResp.Attachments)
// attachments is in here for slack compatibility
if len(webhookResp.Attachments) > 0 {
webhookResp.Props["attachments"] = webhookResp.Attachments
}
if *a.Config().ServiceSettings.EnablePostUsernameOverride && hook.Username != "" && webhookResp.Username == "" {
webhookResp.Username = hook.Username
}
if *a.Config().ServiceSettings.EnablePostIconOverride && hook.IconURL != "" && webhookResp.IconURL == "" {
webhookResp.IconURL = hook.IconURL
}
if _, err := a.CreateWebhookPost(c, hook.CreatorId, channel, text, webhookResp.Username, webhookResp.IconURL, "", webhookResp.Props, webhookResp.Type, postRootId); err != nil {
c.Logger().Error("Failed to create response post.", mlog.Err(err))
}
}
})
}
}
func (a *App) doOutgoingWebhookRequest(url string, body io.Reader, contentType string) (*model.OutgoingWebhookResponse, error) {
req, err := http.NewRequest("POST", url, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", contentType)
req.Header.Set("Accept", "application/json")
resp, err := a.HTTPService().MakeClient(false).Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var hookResp model.OutgoingWebhookResponse
if jsonErr := json.NewDecoder(io.LimitReader(resp.Body, MaxIntegrationResponseSize)).Decode(&hookResp); jsonErr != nil {
if jsonErr == io.EOF {
return nil, nil
}
return nil, model.NewAppError("doOutgoingWebhookRequest", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
return &hookResp, nil
}
func SplitWebhookPost(post *model.Post, maxPostSize int) ([]*model.Post, *model.AppError) {
splits := make([]*model.Post, 0)
remainingText := post.Message
base := post.Clone()
base.Message = ""
base.SetProps(make(map[string]any))
for k, v := range post.GetProps() {
if k != "attachments" {
base.AddProp(k, v)
}
}
if utf8.RuneCountInString(model.StringInterfaceToJSON(base.GetProps())) > model.PostPropsMaxUserRunes {
return nil, model.NewAppError("SplitWebhookPost", "web.incoming_webhook.split_props_length.app_error", map[string]any{"Max": model.PostPropsMaxUserRunes}, "", http.StatusBadRequest)
}
for utf8.RuneCountInString(remainingText) > maxPostSize {
split := base.Clone()
x := 0
for index := range remainingText {
x++
if x > maxPostSize {
split.Message = remainingText[:index]
remainingText = remainingText[index:]
break
}
}
splits = append(splits, split)
}
split := base.Clone()
split.Message = remainingText
splits = append(splits, split)
attachments, _ := post.GetProp("attachments").([]*model.SlackAttachment)
for _, attachment := range attachments {
newAttachment := *attachment
for {
lastSplit := splits[len(splits)-1]
newProps := make(map[string]any)
for k, v := range lastSplit.GetProps() {
newProps[k] = v
}
origAttachments, _ := newProps["attachments"].([]*model.SlackAttachment)
newProps["attachments"] = append(origAttachments, &newAttachment)
newPropsString := model.StringInterfaceToJSON(newProps)
runeCount := utf8.RuneCountInString(newPropsString)
if runeCount <= model.PostPropsMaxUserRunes {
lastSplit.SetProps(newProps)
break
}
if len(origAttachments) > 0 {
newSplit := base.Clone()
splits = append(splits, newSplit)
continue
}
truncationNeeded := runeCount - model.PostPropsMaxUserRunes
textRuneCount := utf8.RuneCountInString(attachment.Text)
if textRuneCount < truncationNeeded {
return nil, model.NewAppError("SplitWebhookPost", "web.incoming_webhook.split_props_length.app_error", map[string]any{"Max": model.PostPropsMaxUserRunes}, "", http.StatusBadRequest)
}
x := 0
for index := range attachment.Text {
x++
if x > textRuneCount-truncationNeeded {
newAttachment.Text = newAttachment.Text[:index]
break
}
}
lastSplit.SetProps(newProps)
break
}
}
return splits, nil
}
func (a *App) CreateWebhookPost(c request.CTX, userID string, channel *model.Channel, text, overrideUsername, overrideIconURL, overrideIconEmoji string, props model.StringInterface, postType string, postRootId string) (*model.Post, *model.AppError) {
// parse links into Markdown format
linkWithTextRegex := regexp.MustCompile(`<([^\n<\|>]+)\|([^\n>]+)>`)
text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
post := &model.Post{UserId: userID, ChannelId: channel.Id, Message: text, Type: postType, RootId: postRootId}
post.AddProp("from_webhook", "true")
if strings.HasPrefix(post.Type, model.PostSystemMessagePrefix) {
err := model.NewAppError("CreateWebhookPost", "api.context.invalid_param.app_error", map[string]any{"Name": "post.type"}, "", http.StatusBadRequest)
return nil, err
}
if metrics := a.Metrics(); metrics != nil {
metrics.IncrementWebhookPost()
}
if *a.Config().ServiceSettings.EnablePostUsernameOverride {
if overrideUsername != "" {
post.AddProp("override_username", overrideUsername)
} else {
post.AddProp("override_username", model.DefaultWebhookUsername)
}
}
if *a.Config().ServiceSettings.EnablePostIconOverride {
if overrideIconURL != "" {
post.AddProp("override_icon_url", overrideIconURL)
}
if overrideIconEmoji != "" {
post.AddProp("override_icon_emoji", overrideIconEmoji)
}
}
if len(props) > 0 {
for key, val := range props {
if key == "attachments" {
if attachments, success := val.([]*model.SlackAttachment); success {
model.ParseSlackAttachment(post, attachments)
}
} else if key != "override_icon_url" && key != "override_username" && key != "from_webhook" {
post.AddProp(key, val)
}
}
}
splits, err := SplitWebhookPost(post, a.MaxPostSize())
if err != nil {
return nil, err
}
for _, split := range splits {
if _, err = a.CreatePostMissingChannel(c, split, false, false); err != nil {
return nil, model.NewAppError("CreateWebhookPost", "api.post.create_webhook_post.creating.app_error", nil, "err="+err.Message, http.StatusInternalServerError)
}
}
return splits[0], nil
}
func (a *App) CreateIncomingWebhookForChannel(creatorId string, channel *model.Channel, hook *model.IncomingWebhook) (*model.IncomingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
return nil, model.NewAppError("CreateIncomingWebhookForChannel", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
hook.UserId = creatorId
hook.TeamId = channel.TeamId
if !*a.Config().ServiceSettings.EnablePostUsernameOverride {
hook.Username = ""
}
if !*a.Config().ServiceSettings.EnablePostIconOverride {
hook.IconURL = ""
}
if hook.Username != "" && !model.IsValidUsername(hook.Username) {
return nil, model.NewAppError("CreateIncomingWebhookForChannel", "api.incoming_webhook.invalid_username.app_error", nil, "", http.StatusBadRequest)
}
webhook, err := a.Srv().Store().Webhook().SaveIncoming(hook)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateIncomingWebhookForChannel", "app.webhooks.save_incoming.existing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("CreateIncomingWebhookForChannel", "app.webhooks.save_incoming.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return webhook, nil
}
func (a *App) UpdateIncomingWebhook(oldHook, updatedHook *model.IncomingWebhook) (*model.IncomingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
return nil, model.NewAppError("UpdateIncomingWebhook", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
if !*a.Config().ServiceSettings.EnablePostUsernameOverride {
updatedHook.Username = oldHook.Username
}
if !*a.Config().ServiceSettings.EnablePostIconOverride {
updatedHook.IconURL = oldHook.IconURL
}
if updatedHook.Username != "" && !model.IsValidUsername(updatedHook.Username) {
return nil, model.NewAppError("UpdateIncomingWebhook", "api.incoming_webhook.invalid_username.app_error", nil, "", http.StatusBadRequest)
}
updatedHook.Id = oldHook.Id
updatedHook.UserId = oldHook.UserId
updatedHook.CreateAt = oldHook.CreateAt
updatedHook.UpdateAt = model.GetMillis()
updatedHook.TeamId = oldHook.TeamId
updatedHook.DeleteAt = oldHook.DeleteAt
newWebhook, err := a.Srv().Store().Webhook().UpdateIncoming(updatedHook)
if err != nil {
return nil, model.NewAppError("UpdateIncomingWebhook", "app.webhooks.update_incoming.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.Srv().Platform().InvalidateCacheForWebhook(oldHook.Id)
return newWebhook, nil
}
func (a *App) DeleteIncomingWebhook(hookID string) *model.AppError {
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
return model.NewAppError("DeleteIncomingWebhook", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
if err := a.Srv().Store().Webhook().DeleteIncoming(hookID, model.GetMillis()); err != nil {
return model.NewAppError("DeleteIncomingWebhook", "app.webhooks.delete_incoming.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.Srv().Platform().InvalidateCacheForWebhook(hookID)
return nil
}
func (a *App) GetIncomingWebhook(hookID string) (*model.IncomingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
return nil, model.NewAppError("GetIncomingWebhook", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
webhook, err := a.Srv().Store().Webhook().GetIncoming(hookID, true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetIncomingWebhook", "app.webhooks.get_incoming.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetIncomingWebhook", "app.webhooks.get_incoming.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return webhook, nil
}
func (a *App) GetIncomingWebhooksForTeamPage(teamID string, page, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
return a.GetIncomingWebhooksForTeamPageByUser(teamID, "", page, perPage)
}
func (a *App) GetIncomingWebhooksForTeamPageByUser(teamID string, userID string, page, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
return nil, model.NewAppError("GetIncomingWebhooksForTeamPage", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
webhooks, err := a.Srv().Store().Webhook().GetIncomingByTeamByUser(teamID, userID, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetIncomingWebhooksForTeamPage", "app.webhooks.get_incoming_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return webhooks, nil
}
func (a *App) GetIncomingWebhooksPageByUser(userID string, page, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
return nil, model.NewAppError("GetIncomingWebhooksPageByUser", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
webhooks, err := a.Srv().Store().Webhook().GetIncomingListByUser(userID, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetIncomingWebhooksPageByUser", "app.webhooks.get_incoming_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return webhooks, nil
}
func (a *App) GetIncomingWebhooksPage(page, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
return a.GetIncomingWebhooksPageByUser("", page, perPage)
}
func (a *App) CreateOutgoingWebhook(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
return nil, model.NewAppError("CreateOutgoingWebhook", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
if hook.ChannelId != "" {
channel, errCh := a.Srv().Store().Channel().Get(hook.ChannelId, true)
if errCh != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(errCh, &nfErr):
return nil, model.NewAppError("CreateOutgoingWebhook", "app.channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(errCh)
default:
return nil, model.NewAppError("CreateOutgoingWebhook", "app.channel.get.find.app_error", nil, "", http.StatusInternalServerError).Wrap(errCh)
}
}
if channel.Type != model.ChannelTypeOpen {
return nil, model.NewAppError("CreateOutgoingWebhook", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusForbidden)
}
if channel.Type != model.ChannelTypeOpen || channel.TeamId != hook.TeamId {
return nil, model.NewAppError("CreateOutgoingWebhook", "api.webhook.create_outgoing.permissions.app_error", nil, "", http.StatusForbidden)
}
} else if len(hook.TriggerWords) == 0 {
return nil, model.NewAppError("CreateOutgoingWebhook", "api.webhook.create_outgoing.triggers.app_error", nil, "", http.StatusBadRequest)
}
allHooks, err := a.Srv().Store().Webhook().GetOutgoingByTeam(hook.TeamId, -1, -1)
if err != nil {
return nil, model.NewAppError("CreateOutgoingWebhook", "app.webhooks.get_outgoing_by_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, existingOutHook := range allHooks {
urlIntersect := utils.StringArrayIntersection(existingOutHook.CallbackURLs, hook.CallbackURLs)
triggerIntersect := utils.StringArrayIntersection(existingOutHook.TriggerWords, hook.TriggerWords)
if existingOutHook.ChannelId == hook.ChannelId && len(urlIntersect) != 0 && len(triggerIntersect) != 0 {
return nil, model.NewAppError("CreateOutgoingWebhook", "api.webhook.create_outgoing.intersect.app_error", nil, "", http.StatusInternalServerError)
}
}
webhook, err := a.Srv().Store().Webhook().SaveOutgoing(hook)
if err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateOutgoingWebhook", "app.webhooks.save_outgoing.override.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("CreateOutgoingWebhook", "app.webhooks.save_outgoing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return webhook, nil
}
func (a *App) UpdateOutgoingWebhook(c request.CTX, oldHook, updatedHook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
return nil, model.NewAppError("UpdateOutgoingWebhook", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
if updatedHook.ChannelId != "" {
channel, err := a.GetChannel(c, updatedHook.ChannelId)
if err != nil {
return nil, err
}
if channel.Type != model.ChannelTypeOpen {
return nil, model.NewAppError("UpdateOutgoingWebhook", "api.webhook.create_outgoing.not_open.app_error", nil, "", http.StatusForbidden)
}
if channel.TeamId != oldHook.TeamId {
return nil, model.NewAppError("UpdateOutgoingWebhook", "api.webhook.create_outgoing.permissions.app_error", nil, "", http.StatusForbidden)
}
} else if len(updatedHook.TriggerWords) == 0 {
return nil, model.NewAppError("UpdateOutgoingWebhook", "api.webhook.create_outgoing.triggers.app_error", nil, "", http.StatusInternalServerError)
}
allHooks, err := a.Srv().Store().Webhook().GetOutgoingByTeam(oldHook.TeamId, -1, -1)
if err != nil {
return nil, model.NewAppError("UpdateOutgoingWebhook", "app.webhooks.get_outgoing_by_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, existingOutHook := range allHooks {
urlIntersect := utils.StringArrayIntersection(existingOutHook.CallbackURLs, updatedHook.CallbackURLs)
triggerIntersect := utils.StringArrayIntersection(existingOutHook.TriggerWords, updatedHook.TriggerWords)
if existingOutHook.ChannelId == updatedHook.ChannelId && len(urlIntersect) != 0 && len(triggerIntersect) != 0 && existingOutHook.Id != updatedHook.Id {
return nil, model.NewAppError("UpdateOutgoingWebhook", "api.webhook.update_outgoing.intersect.app_error", nil, "", http.StatusBadRequest)
}
}
updatedHook.CreatorId = oldHook.CreatorId
updatedHook.CreateAt = oldHook.CreateAt
updatedHook.DeleteAt = oldHook.DeleteAt
updatedHook.TeamId = oldHook.TeamId
updatedHook.UpdateAt = model.GetMillis()
webhook, err := a.Srv().Store().Webhook().UpdateOutgoing(updatedHook)
if err != nil {
return nil, model.NewAppError("UpdateOutgoingWebhook", "app.webhooks.update_outgoing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return webhook, nil
}
func (a *App) GetOutgoingWebhook(hookID string) (*model.OutgoingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
return nil, model.NewAppError("GetOutgoingWebhook", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
webhook, err := a.Srv().Store().Webhook().GetOutgoing(hookID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetOutgoingWebhook", "app.webhooks.get_outgoing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetOutgoingWebhook", "app.webhooks.get_outgoing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return webhook, nil
}
func (a *App) GetOutgoingWebhooksPage(page, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
return a.GetOutgoingWebhooksPageByUser("", page, perPage)
}
func (a *App) GetOutgoingWebhooksPageByUser(userID string, page, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
return nil, model.NewAppError("GetOutgoingWebhooksPageByUser", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
webhooks, err := a.Srv().Store().Webhook().GetOutgoingListByUser(userID, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetOutgoingWebhooksPageByUser", "app.webhooks.get_outgoing_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return webhooks, nil
}
func (a *App) GetOutgoingWebhooksForChannelPageByUser(channelID string, userID string, page, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
return nil, model.NewAppError("GetOutgoingWebhooksForChannelPage", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
webhooks, err := a.Srv().Store().Webhook().GetOutgoingByChannelByUser(channelID, userID, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetOutgoingWebhooksForChannelPage", "app.webhooks.get_outgoing_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return webhooks, nil
}
func (a *App) GetOutgoingWebhooksForTeamPage(teamID string, page, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
return a.GetOutgoingWebhooksForTeamPageByUser(teamID, "", page, perPage)
}
func (a *App) GetOutgoingWebhooksForTeamPageByUser(teamID string, userID string, page, perPage int) ([]*model.OutgoingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
return nil, model.NewAppError("GetOutgoingWebhooksForTeamPageByUser", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
webhooks, err := a.Srv().Store().Webhook().GetOutgoingByTeamByUser(teamID, userID, page*perPage, perPage)
if err != nil {
return nil, model.NewAppError("GetOutgoingWebhooksForTeamPageByUser", "app.webhooks.get_outgoing_by_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return webhooks, nil
}
func (a *App) DeleteOutgoingWebhook(hookID string) *model.AppError {
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
return model.NewAppError("DeleteOutgoingWebhook", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
if err := a.Srv().Store().Webhook().DeleteOutgoing(hookID, model.GetMillis()); err != nil {
return model.NewAppError("DeleteOutgoingWebhook", "app.webhooks.delete_outgoing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) RegenOutgoingWebhookToken(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
return nil, model.NewAppError("RegenOutgoingWebhookToken", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
hook.Token = model.NewId()
webhook, err := a.Srv().Store().Webhook().UpdateOutgoing(hook)
if err != nil {
return nil, model.NewAppError("RegenOutgoingWebhookToken", "app.webhooks.update_outgoing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return webhook, nil
}
func (a *App) HandleIncomingWebhook(c *request.Context, hookID string, req *model.IncomingWebhookRequest) *model.AppError {
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
}
hchan := make(chan store.StoreResult, 1)
go func() {
webhook, err := a.Srv().Store().Webhook().GetIncoming(hookID, true)
hchan <- store.StoreResult{Data: webhook, NErr: err}
close(hchan)
}()
if req == nil {
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.parse.app_error", nil, "", http.StatusBadRequest)
}
text := req.Text
if text == "" && req.Attachments == nil {
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.text.app_error", nil, "", http.StatusBadRequest)
}
channelName := req.ChannelName
webhookType := req.Type
var hook *model.IncomingWebhook
result := <-hchan
if result.NErr != nil {
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.invalid.app_error", nil, "", http.StatusBadRequest).Wrap(result.NErr)
}
hook = result.Data.(*model.IncomingWebhook)
uchan := make(chan store.StoreResult, 1)
go func() {
user, err := a.Srv().Store().User().Get(context.Background(), hook.UserId)
uchan <- store.StoreResult{Data: user, NErr: err}
close(uchan)
}()
if len(req.Props) == 0 {
req.Props = make(model.StringInterface)
}
req.Props["webhook_display_name"] = hook.DisplayName
text = a.ProcessSlackText(text)
req.Attachments = a.ProcessSlackAttachments(req.Attachments)
// attachments is in here for slack compatibility
if len(req.Attachments) > 0 {
req.Props["attachments"] = req.Attachments
webhookType = model.PostTypeSlackAttachment
}
var channel *model.Channel
var cchan chan store.StoreResult
if channelName != "" {
if channelName[0] == '@' {
result, nErr := a.Srv().Store().User().GetByUsername(channelName[1:])
if nErr != nil {
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.user.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
}
ch, err := a.GetOrCreateDirectChannel(c, hook.UserId, result.Id)
if err != nil {
return err
}
channel = ch
} else if channelName[0] == '#' {
cchan = make(chan store.StoreResult, 1)
go func() {
chnn, chnnErr := a.Srv().Store().Channel().GetByName(hook.TeamId, channelName[1:], true)
cchan <- store.StoreResult{Data: chnn, NErr: chnnErr}
close(cchan)
}()
} else {
cchan = make(chan store.StoreResult, 1)
go func() {
chnn, chnnErr := a.Srv().Store().Channel().GetByName(hook.TeamId, channelName, true)
cchan <- store.StoreResult{Data: chnn, NErr: chnnErr}
close(cchan)
}()
}
} else {
var err error
channel, err = a.Srv().Store().Channel().Get(hook.ChannelId, true)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return model.NewAppError("HandleIncomingWebhook", "app.channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return model.NewAppError("HandleIncomingWebhook", "app.channel.get.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
if channel == nil {
result2 := <-cchan
if result2.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(result2.NErr, &nfErr):
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.channel.app_error", nil, "", http.StatusNotFound).Wrap(result2.NErr)
default:
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.channel.app_error", nil, "", http.StatusInternalServerError).Wrap(result2.NErr)
}
}
channel = result2.Data.(*model.Channel)
}
if hook.ChannelLocked && hook.ChannelId != channel.Id {
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.channel_locked.app_error", nil, "", http.StatusForbidden)
}
result = <-uchan
if result.NErr != nil {
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.user.app_error", nil, "", http.StatusForbidden).Wrap(result.NErr)
}
if channel.Type != model.ChannelTypeOpen && !a.HasPermissionToChannel(c, hook.UserId, channel.Id, model.PermissionReadChannel) {
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.permissions.app_error", nil, "", http.StatusForbidden)
}
overrideUsername := hook.Username
if req.Username != "" {
overrideUsername = req.Username
}
overrideIconURL := hook.IconURL
if req.IconURL != "" {
overrideIconURL = req.IconURL
}
_, err := a.CreateWebhookPost(c, hook.UserId, channel, text, overrideUsername, overrideIconURL, req.IconEmoji, req.Props, webhookType, "")
return err
}
func (a *App) CreateCommandWebhook(commandID string, args *model.CommandArgs) (*model.CommandWebhook, *model.AppError) {
hook := &model.CommandWebhook{
CommandId: commandID,
UserId: args.UserId,
ChannelId: args.ChannelId,
RootId: args.RootId,
}
savedHook, err := a.Srv().Store().CommandWebhook().Save(hook)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateCommandWebhook", "app.command_webhook.create_command_webhook.existing", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("CreateCommandWebhook", "app.command_webhook.create_command_webhook.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return savedHook, nil
}
func (a *App) HandleCommandWebhook(c *request.Context, hookID string, response *model.CommandResponse) *model.AppError {
if response == nil {
return model.NewAppError("HandleCommandWebhook", "app.command_webhook.handle_command_webhook.parse", nil, "", http.StatusBadRequest)
}
hook, nErr := a.Srv().Store().CommandWebhook().Get(hookID)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return model.NewAppError("HandleCommandWebhook", "app.command_webhook.get.missing", map[string]any{"hook_id": hookID}, "", http.StatusNotFound).Wrap(nErr)
default:
return model.NewAppError("HandleCommandWebhook", "app.command_webhook.get.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
cmd, cmdErr := a.Srv().Store().Command().Get(hook.CommandId)
if cmdErr != nil {
var appErr *model.AppError
switch {
case errors.As(cmdErr, &appErr):
return appErr
default:
return model.NewAppError("HandleCommandWebhook", "web.command_webhook.command.app_error", nil, "", http.StatusBadRequest).Wrap(cmdErr)
}
}
args := &model.CommandArgs{
UserId: hook.UserId,
ChannelId: hook.ChannelId,
TeamId: cmd.TeamId,
RootId: hook.RootId,
}
if nErr := a.Srv().Store().CommandWebhook().TryUse(hook.Id, 5); nErr != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(nErr, &invErr):
return model.NewAppError("HandleCommandWebhook", "app.command_webhook.try_use.invalid", nil, "", http.StatusBadRequest).Wrap(nErr)
default:
return model.NewAppError("HandleCommandWebhook", "app.command_webhook.try_use.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
_, err := a.HandleCommandResponse(c, cmd, args, response, false)
return err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"
pbclient "github.com/mattermost/mattermost-plugin-playbooks/client"
fb_model "github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/app/worktemplates"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type WorkTemplateExecutor interface {
CreatePlaybook(c *request.Context, wtcr *worktemplates.ExecutionRequest, playbook *model.WorkTemplatePlaybook, channel model.WorkTemplateChannel) (string, error)
CreateChannel(c *request.Context, wtcr *worktemplates.ExecutionRequest, cChannel *model.WorkTemplateChannel) (string, error)
CreateBoard(c *request.Context, wtcr *worktemplates.ExecutionRequest, cBoard *model.WorkTemplateBoard, linkToChannelID string) (string, error)
InstallPlugin(c *request.Context, wtcr *worktemplates.ExecutionRequest, cIntegration *model.WorkTemplateIntegration, sendToChannelID string) error
}
type appWorkTemplateExecutor struct {
app *App
}
func (e *appWorkTemplateExecutor) CreatePlaybook(
c *request.Context,
wtcr *worktemplates.ExecutionRequest,
playbook *model.WorkTemplatePlaybook,
channel model.WorkTemplateChannel) (string, error) {
// determine playbook name
name := playbook.Name
if wtcr.Name != "" {
name += " " + wtcr.Name
}
// get the correct playbook pbTemplate
pbTemplate, err := wtcr.FindPlaybookTemplate(playbook.Template)
if err != nil {
return "", fmt.Errorf("unable to find playbook template: %w", err)
}
pbTemplate.TeamID = wtcr.TeamID
pbTemplate.Title = name
pbTemplate.Public = wtcr.Visibility == model.WorkTemplateVisibilityPublic
pbTemplate.CreatePublicPlaybookRun = wtcr.Visibility == model.WorkTemplateVisibilityPublic
data, err := json.Marshal(pbTemplate)
if err != nil {
return "", fmt.Errorf("unable to marshal playbook template: %w", err)
}
resp, appErr := e.app.doPluginRequest(c, http.MethodPost, "/plugins/playbooks/api/v0/playbooks", nil, data)
if appErr != nil {
return "", fmt.Errorf("unable to create playbook: %w", appErr)
}
defer resp.Body.Close()
pbcResp := playbookCreateResponse{}
err = json.NewDecoder(resp.Body).Decode(&pbcResp)
if err != nil {
return "", fmt.Errorf("unable to decode playbook create response: %w", err)
}
runName := channel.Name
if wtcr.Name != "" {
runName = wtcr.Name
}
data, err = json.Marshal(pbclient.PlaybookRunCreateOptions{
Name: runName,
OwnerUserID: c.Session().UserId,
TeamID: wtcr.TeamID,
PlaybookID: pbcResp.ID,
})
if err != nil {
return "", fmt.Errorf("unable to marshal playbook run create request: %w", err)
}
resp, appErr = e.app.doPluginRequest(c, http.MethodPost, "/plugins/playbooks/api/v0/runs", nil, data)
if appErr != nil {
return "", fmt.Errorf("unable to create playbook run: %w", appErr)
}
defer resp.Body.Close()
pbrResp := playbookRunCreateResponse{}
err = json.NewDecoder(resp.Body).Decode(&pbrResp)
if err != nil {
return "", fmt.Errorf("unable to decode playbook run create response: %w", err)
}
// using pbrResp.ChannelID, update the channel to add metadata
dbChannel, err := e.app.Srv().Store().Channel().Get(pbrResp.ChannelID, false)
if err != nil {
return "", fmt.Errorf("unable to find channel: %w", err)
}
if dbChannel == nil {
return "", fmt.Errorf("channel not found")
}
dbChannel.AddProp(model.WorkTemplateIDChannelProp, wtcr.WorkTemplate.ID)
_, err = e.app.Srv().Store().Channel().Update(dbChannel)
if err != nil {
e.app.Srv().Log().Error("Failed to update playbook channel metadata", mlog.Err(err))
}
return pbrResp.ChannelID, nil
}
func (e *appWorkTemplateExecutor) CreateChannel(
c *request.Context,
wtcr *worktemplates.ExecutionRequest,
cChannel *model.WorkTemplateChannel,
) (string, error) {
channelID := ""
channelDisplayName := cChannel.Name
if wtcr.Name != "" {
channelDisplayName = wtcr.Name
}
var channelCreationAppErr *model.AppError = &model.AppError{}
cleanChannelName := cleanChannelName(channelDisplayName)
channelName := cleanChannelName
if len(channelName) > model.ChannelNameMaxLength {
channelName = channelName[:model.ChannelNameMaxLength]
}
// Mostly because of the "quick use" feature, we might try to create channel that have the exact same "Name"
// This loop ensures that if the original name is taken, we try again by adding a suffix to the Name
for channelCreationAppErr != nil {
// create channel
var newChan *model.Channel
newChan, channelCreationAppErr = e.app.CreateChannelWithUser(c, &model.Channel{
TeamId: wtcr.TeamID,
Name: channelName,
DisplayName: channelDisplayName,
Type: model.ChannelTypeOpen,
Purpose: cChannel.Purpose,
Props: map[string]any{
model.WorkTemplateIDChannelProp: wtcr.WorkTemplate.ID,
},
}, c.Session().UserId)
if channelCreationAppErr != nil {
if channelCreationAppErr.Id == store.ChannelExistsError {
// compute a new unique name
suffix := fmt.Sprintf("-%s", model.NewId()[0:4])
channelName = cleanChannelName
if len(cleanChannelName)+len(suffix) > model.ChannelNameMaxLength {
channelName = cleanChannelName[:model.ChannelNameMaxLength-len(suffix)]
}
channelName = channelName + suffix
continue
}
return "", fmt.Errorf("error while creating channel: %w", channelCreationAppErr)
}
channelID = newChan.Id
}
return channelID, nil
}
func (e *appWorkTemplateExecutor) CreateBoard(
c *request.Context,
wtcr *worktemplates.ExecutionRequest,
cBoard *model.WorkTemplateBoard,
linkToChannelID string,
) (string, error) {
boardService := e.app.Srv().services[product.BoardsKey].(product.BoardsService)
templates, err := boardService.GetTemplates("0", c.Session().UserId)
if err != nil {
return "", fmt.Errorf("error while getting templates: %w", err)
}
var template *fb_model.Board = nil
for _, t := range templates {
v, ok := t.Properties["trackingTemplateId"]
if ok && v == cBoard.Template {
template = t
break
}
}
if template == nil {
return "", fmt.Errorf("template not found")
}
title := cBoard.Name
if wtcr.Name != "" {
title += " " + wtcr.Name
}
// Duplicate board From template
boardsAndBlocks, _, err := boardService.DuplicateBoard(template.ID, c.Session().UserId, wtcr.TeamID, false)
if err != nil {
return "", fmt.Errorf("failed to create new board from template: %w", err)
}
if len(boardsAndBlocks.Boards) != 1 {
return "", fmt.Errorf("only one board was expected, found %d", len(boardsAndBlocks.Boards))
}
// Apply patch for the title and linked channel
patchedBoard, err := boardService.PatchBoard(&fb_model.BoardPatch{
Title: &title,
ChannelID: &linkToChannelID,
}, boardsAndBlocks.Boards[0].ID, c.Session().UserId)
if err != nil {
return "", fmt.Errorf("failed to patch board: %w", err)
}
return patchedBoard.ID, nil
}
func (e *appWorkTemplateExecutor) InstallPlugin(
c *request.Context,
wtcr *worktemplates.ExecutionRequest,
cIntegration *model.WorkTemplateIntegration,
sendToChannelID string,
) error {
// check if this plugin is already installed
pluginID := cIntegration.ID
_, appErr := e.app.GetPluginStatus(pluginID)
if appErr != nil {
if appErr.Id == "app.plugin.not_installed.app_error" {
// we install them in the background as we don't want user to wait for this
manifest, installAppErr := e.app.Channels().InstallMarketplacePlugin(&model.InstallMarketplacePluginRequest{
Id: pluginID,
Version: "",
})
if installAppErr != nil {
return fmt.Errorf("unable to install plugin: %w", installAppErr)
}
if sendToChannelID != "" {
e.app.SendEphemeralPost(c, c.Session().UserId, &model.Post{
ChannelId: sendToChannelID,
Message: fmt.Sprintf("plugin %s has been installed", manifest.Name),
CreateAt: model.GetMillis(),
})
}
} else {
return fmt.Errorf("unable to get plugin status: %w", appErr)
}
}
// get plugin state
if err := e.app.EnablePlugin(pluginID); err != nil {
return fmt.Errorf("unable to enable plugin: %w", err)
}
return nil
}
type playbookCreateResponse struct {
ID string `json:"id"`
}
type playbookRunCreateResponse struct {
ID string `json:"id"`
ChannelID string `json:"channel_id"`
}
// cleaning channel name code bellow comes from the playbook repository.
var allNonSpaceNonWordRegex = regexp.MustCompile(`[^\w\s]`)
func cleanChannelName(channelName string) string {
// Lower case only
channelName = strings.ToLower(channelName)
// Trim spaces
channelName = strings.TrimSpace(channelName)
// Change all dashes to whitespace, remove everything that's not a word or whitespace, all space becomes dashes
channelName = strings.ReplaceAll(channelName, "-", " ")
channelName = allNonSpaceNonWordRegex.ReplaceAllString(channelName, "")
channelName = strings.ReplaceAll(channelName, " ", "-")
// Remove all leading and trailing dashes
channelName = strings.Trim(channelName, "-")
return channelName
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/app/worktemplates"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
func (a *App) GetWorkTemplateCategories(t i18n.TranslateFunc) ([]*model.WorkTemplateCategory, *model.AppError) {
categories, err := worktemplates.ListCategories()
if err != nil {
return nil, model.NewAppError("GetWorkTemplateCategories", "app.worktemplates.get_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
modelCategories := make([]*model.WorkTemplateCategory, len(categories))
for i := range categories {
modelCategories[i] = &model.WorkTemplateCategory{
ID: categories[i].ID,
Name: t(categories[i].Name),
}
}
return modelCategories, nil
}
func (a *App) GetWorkTemplates(category string, featureFlags map[string]string, t i18n.TranslateFunc) ([]*model.WorkTemplate, *model.AppError) {
templates, err := worktemplates.ListByCategory(category)
if err != nil {
return nil, model.NewAppError("GetWorkTemplates", "app.worktemplates.get_templates.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// filter out templates that are not enabled by feature Flag
enabledTemplates := []*model.WorkTemplate{}
for _, template := range templates {
mTemplate := template.ToModelWorkTemplate(t)
if template.FeatureFlag == nil {
enabledTemplates = append(enabledTemplates, mTemplate)
continue
}
if featureFlags[template.FeatureFlag.Name] == template.FeatureFlag.Value {
enabledTemplates = append(enabledTemplates, mTemplate)
}
}
return enabledTemplates, nil
}
func (a *App) ExecuteWorkTemplate(c *request.Context, wtcr *worktemplates.ExecutionRequest, installPlugins bool) (*WorkTemplateExecutionResult, *model.AppError) {
e := &appWorkTemplateExecutor{app: a}
return a.executeWorkTemplate(c, wtcr, e, installPlugins)
}
func (a *App) executeWorkTemplate(
c *request.Context,
wtcr *worktemplates.ExecutionRequest,
e WorkTemplateExecutor,
installPlugins bool,
) (*WorkTemplateExecutionResult, *model.AppError) {
res := &WorkTemplateExecutionResult{
ChannelWithPlaybookIDs: []string{},
ChannelIDs: []string{},
}
if wtcr.Name != "" {
if len(wtcr.Name) > model.ChannelNameMaxLength {
return res, model.NewAppError("ExecuteWorkTemplate", "app.worktemplates.execute_work_template.name_too_long", nil, "", http.StatusBadRequest)
}
}
contentByType := map[string][]model.WorkTemplateContent{
"channel": {},
"board": {},
"playbook": {},
"integration": {},
}
for _, content := range wtcr.WorkTemplate.Content {
if content.Channel != nil {
contentByType["channel"] = append(contentByType["channel"], content)
}
if content.Board != nil {
contentByType["board"] = append(contentByType["board"], content)
}
if content.Playbook != nil {
contentByType["playbook"] = append(contentByType["playbook"], content)
}
if content.Integration != nil {
contentByType["integration"] = append(contentByType["integration"], content)
}
}
firstChannelId := ""
channelIDByWorkTemplateID := map[string]string{}
for _, pbContent := range contentByType["playbook"] {
cPlaybook := pbContent.Playbook
// find associated channel
var associatedChannel *model.WorkTemplateChannel
for _, channelContent := range contentByType["channel"] {
if channelContent.Channel.Playbook == cPlaybook.ID {
associatedChannel = channelContent.Channel
break
}
}
if associatedChannel == nil {
return res, model.NewAppError("ExecuteWorkTemplate", "app.worktemplates.execute_work_template.playbooks.find_channel_error", nil, "no associated channel found for playbook", http.StatusInternalServerError)
}
channelID, err := e.CreatePlaybook(c, wtcr, cPlaybook, *associatedChannel)
if err != nil {
return res, model.NewAppError("ExecuteWorkTemplate", "app.worktemplates.execute_work_template.playbooks.create_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if firstChannelId == "" {
firstChannelId = channelID
}
res.ChannelWithPlaybookIDs = append(res.ChannelWithPlaybookIDs, channelID)
channelIDByWorkTemplateID[associatedChannel.ID] = channelID
}
// loop through all channels
for _, channelContent := range contentByType["channel"] {
cChannel := channelContent.Channel
// we only need to create a channel if there's no playbook
if cChannel.Playbook == "" {
chanID, err := e.CreateChannel(c, wtcr, cChannel)
if err != nil {
return res, model.NewAppError("ExecuteWorkTemplate", "app.worktemplates.execute_work_template.channels.create_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if firstChannelId == "" {
firstChannelId = chanID
}
res.ChannelIDs = append(res.ChannelIDs, chanID)
channelIDByWorkTemplateID[cChannel.ID] = chanID
}
}
for _, boardContent := range contentByType["board"] {
cBoard := boardContent.Board
channelID := ""
if cBoard.Channel != "" {
channel, ok := channelIDByWorkTemplateID[cBoard.Channel]
if !ok {
return res, model.NewAppError("ExecuteWorkTemplate", "app.worktemplates.execute_work_template.app_error", nil, "no associated channel found for board", http.StatusInternalServerError)
}
channelID = channel
}
_, err := e.CreateBoard(c, wtcr, cBoard, channelID)
if err != nil {
return res, model.NewAppError("ExecuteWorkTemplate", "app.worktemplates.execute_work_template.boards.create_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if installPlugins {
for _, integrationContent := range contentByType["integration"] {
cIntegration := integrationContent.Integration
// this can take a long time so we just start those as background tasks
go e.InstallPlugin(c, wtcr, cIntegration, firstChannelId)
}
}
for _, ch := range res.ChannelWithPlaybookIDs {
message := model.NewWebSocketEvent(model.WebsocketEventChannelCreated, "", "", c.Session().UserId, nil, "")
message.Add("channel_id", ch)
message.Add("team_id", wtcr.TeamID)
a.Publish(message)
}
for _, ch := range res.ChannelIDs {
message := model.NewWebSocketEvent(model.WebsocketEventChannelCreated, "", "", c.Session().UserId, nil, "")
message.Add("channel_id", ch)
message.Add("team_id", wtcr.TeamID)
a.Publish(message)
}
return res, nil
}
type WorkTemplateExecutionResult struct {
ChannelWithPlaybookIDs []string `json:"channel_with_playbook_ids"`
ChannelIDs []string `json:"channel_ids"`
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package worktemplates
import (
"errors"
"net/http"
pbclient "github.com/mattermost/mattermost-plugin-playbooks/client"
"github.com/mattermost/mattermost-server/v6/model"
)
type ExecutionRequest struct {
TeamID string `json:"team_id"`
Name string `json:"name"`
Visibility string `json:"visibility"`
WorkTemplate model.WorkTemplate `json:"work_template"`
PlaybookTemplates []*PlaybookTemplate `json:"playbook_templates"`
foundPlaybookTemplates map[string]*pbclient.PlaybookCreateOptions
}
type PermissionSet struct {
License *model.License
// channels
CanCreatePublicChannel bool
CanCreatePrivateChannel bool
// playbooks
CanCreatePublicPlaybook bool
CanCreatePrivatePlaybook bool
// boards
CanCreatePublicBoard bool
CanCreatePrivateBoard bool
}
func (r *ExecutionRequest) CanBeExecuted(p PermissionSet) *model.AppError {
public := r.Visibility == model.WorkTemplateVisibilityPublic
for _, c := range r.WorkTemplate.Content {
if c.Channel != nil {
if public && !p.CanCreatePublicChannel {
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.cannot_create_public_channel", nil, "", http.StatusForbidden)
}
if !public && !p.CanCreatePrivateChannel {
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.cannot_create_private_channel", nil, "", http.StatusForbidden)
}
continue
}
if c.Board != nil {
if public && !p.CanCreatePublicBoard {
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.cannot_create_public_board", nil, "", http.StatusForbidden)
}
if !public && !p.CanCreatePrivateBoard {
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.cannot_create_private_board", nil, "", http.StatusForbidden)
}
continue
}
if c.Playbook != nil {
if public && !p.CanCreatePublicPlaybook {
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.cannot_create_public_playbook", nil, "", http.StatusForbidden)
}
if !public && !p.CanCreatePrivatePlaybook {
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.cannot_create_private_playbook", nil, "", http.StatusForbidden)
}
// private playbook is an E20/Enterprise feature
if !public && (p.License == nil || (p.License.SkuShortName != model.LicenseShortSkuE20 && p.License.SkuShortName != model.LicenseShortSkuEnterprise)) {
return model.NewAppError("WorkTemplateExecutionRequest.CanBeExecuted", "app.worktemplate.execution_request.license_cannot_create_private_playbook", nil, "", http.StatusForbidden)
}
continue
}
}
return nil
}
// FindPlaybookTemplate returns the playbook template with the given title.
// it also feed a cache to avoid looking for the same template twice.
func (r *ExecutionRequest) FindPlaybookTemplate(templateTitle string) (*pbclient.PlaybookCreateOptions, error) {
if r.foundPlaybookTemplates == nil {
r.foundPlaybookTemplates = make(map[string]*pbclient.PlaybookCreateOptions)
}
if pt, ok := r.foundPlaybookTemplates[templateTitle]; ok {
if pt == nil {
return nil, errors.New("playbook template not found")
}
return pt, nil
}
for _, pt := range r.PlaybookTemplates {
if pt.Title == templateTitle {
r.foundPlaybookTemplates[templateTitle] = &pt.Template
return &pt.Template, nil
}
}
r.foundPlaybookTemplates[templateTitle] = nil
return nil, errors.New("playbook template not found")
}
type PlaybookTemplate struct {
Title string `json:"title"`
Template pbclient.PlaybookCreateOptions `json:"template"`
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package worktemplates
import (
"fmt"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type WorkTemplateCategory struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
}
type WorkTemplate struct {
ID string `yaml:"id"`
Category string `yaml:"category"`
UseCase string `yaml:"useCase"`
Illustration string `yaml:"illustration"`
Visibility string `yaml:"visibility"`
FeatureFlag *FeatureFlag `yaml:"featureFlag,omitempty"`
Description Description `yaml:"description"`
Content []Content `yaml:"content"`
}
func (wt WorkTemplate) ToModelWorkTemplate(t i18n.TranslateFunc) *model.WorkTemplate {
mwt := &model.WorkTemplate{
ID: wt.ID,
Category: wt.Category,
UseCase: wt.UseCase,
Illustration: wt.Illustration,
Visibility: wt.Visibility,
}
if wt.FeatureFlag != nil {
mwt.FeatureFlag = &model.WorkTemplateFeatureFlag{
Name: wt.FeatureFlag.Name,
Value: wt.FeatureFlag.Value,
}
}
if wt.Description.Channel != nil {
mwt.Description.Channel = &model.DescriptionContent{
Message: wt.Description.Channel.Translate(t),
Illustration: wt.Description.Channel.Illustration,
}
}
if wt.Description.Board != nil {
mwt.Description.Board = &model.DescriptionContent{
Message: wt.Description.Board.Translate(t),
Illustration: wt.Description.Board.Illustration,
}
}
if wt.Description.Playbook != nil {
mwt.Description.Playbook = &model.DescriptionContent{
Message: wt.Description.Playbook.Translate(t),
Illustration: wt.Description.Playbook.Illustration,
}
}
if wt.Description.Integration != nil {
mwt.Description.Integration = &model.DescriptionContent{
Message: wt.Description.Integration.Translate(t),
Illustration: wt.Description.Integration.Illustration,
}
}
for _, content := range wt.Content {
if content.Channel != nil {
mwt.Content = append(mwt.Content, model.WorkTemplateContent{
Channel: &model.WorkTemplateChannel{
ID: content.Channel.ID,
Name: content.Channel.Name,
Purpose: content.Channel.Purpose,
Playbook: content.Channel.Playbook,
Illustration: content.Channel.Illustration,
},
})
}
if content.Board != nil {
mwt.Content = append(mwt.Content, model.WorkTemplateContent{
Board: &model.WorkTemplateBoard{
ID: content.Board.ID,
Name: content.Board.Name,
Template: content.Board.Template,
Channel: content.Board.Channel,
Illustration: content.Board.Illustration,
},
})
}
if content.Playbook != nil {
mwt.Content = append(mwt.Content, model.WorkTemplateContent{
Playbook: &model.WorkTemplatePlaybook{
ID: content.Playbook.ID,
Name: content.Playbook.Name,
Template: content.Playbook.Template,
Illustration: content.Playbook.Illustration,
},
})
}
if content.Integration != nil {
mwt.Content = append(mwt.Content, model.WorkTemplateContent{
Integration: &model.WorkTemplateIntegration{
ID: content.Integration.ID,
},
})
}
}
return mwt
}
func (wt WorkTemplate) Validate(categoryIds map[string]struct{}) error {
if wt.ID == "" {
return errors.New("id is required")
}
if wt.Category == "" {
return errors.New("category is required")
}
if _, ok := categoryIds[wt.Category]; !ok {
return fmt.Errorf("category %s does not exist", wt.Category)
}
if wt.UseCase == "" {
return errors.New("useCase is required")
}
if wt.Illustration == "" {
return errors.New("illustration is required")
}
if wt.Visibility == "" {
return errors.New("visibility is required")
}
hasChannel := false
hasBoard := false
hasPlaybook := false
hasIntegration := false
foundChannels := map[string]struct{}{}
foundPlaybooks := map[string]struct{}{}
foundBoards := map[string]struct{}{}
foundIntegrations := map[string]struct{}{}
mustHaveChannels := []string{}
mustHavePlaybooks := []string{}
currentIdx := 0
for _, content := range wt.Content {
if content.Channel != nil {
hasChannel = true
if cErr := content.Channel.Validate(); cErr != nil {
return wrapContentError(cErr, currentIdx)
}
if _, ok := foundChannels[content.Channel.ID]; ok {
return wrapContentError(fmt.Errorf("duplicate channel %s found", content.Channel.ID), currentIdx)
}
foundChannels[content.Channel.ID] = struct{}{}
if content.Channel.Playbook != "" {
mustHavePlaybooks = append(mustHavePlaybooks, content.Channel.Playbook)
}
}
if content.Board != nil {
hasBoard = true
if cErr := content.Board.Validate(); cErr != nil {
return wrapContentError(cErr, currentIdx)
}
if _, ok := foundBoards[content.Board.ID]; ok {
return wrapContentError(fmt.Errorf("duplicate board %s found", content.Board.ID), currentIdx)
}
foundBoards[content.Board.ID] = struct{}{}
if content.Board.Channel != "" {
mustHaveChannels = append(mustHaveChannels, content.Board.Channel)
}
}
if content.Playbook != nil {
hasPlaybook = true
if cErr := content.Playbook.Validate(); cErr != nil {
return wrapContentError(cErr, currentIdx)
}
if _, ok := foundPlaybooks[content.Playbook.ID]; ok {
return wrapContentError(fmt.Errorf("duplicate playbook %s found", content.Playbook.ID), currentIdx)
}
foundPlaybooks[content.Playbook.ID] = struct{}{}
}
if content.Integration != nil {
hasIntegration = true
if cErr := content.Integration.Validate(); cErr != nil {
return wrapContentError(cErr, currentIdx)
}
if _, ok := foundIntegrations[content.Integration.ID]; ok {
return wrapContentError(fmt.Errorf("duplicate integration %s found", content.Integration.ID), currentIdx)
}
foundIntegrations[content.Integration.ID] = struct{}{}
}
}
if hasChannel && wt.Description.Channel == nil {
return errors.New("description.channel is required")
}
if hasBoard && wt.Description.Board == nil {
return errors.New("description.board is required")
}
if hasPlaybook && wt.Description.Playbook == nil {
return errors.New("description.playbook is required")
}
if hasIntegration && wt.Description.Integration == nil {
return errors.New("description.integration is required")
}
for _, channel := range mustHaveChannels {
if _, ok := foundChannels[channel]; !ok {
return fmt.Errorf("channel %s is required", channel)
}
}
for _, playbook := range mustHavePlaybooks {
if _, ok := foundPlaybooks[playbook]; !ok {
return fmt.Errorf("playbook %s is required", playbook)
}
}
return nil
}
type FeatureFlag struct {
Name string `yaml:"name"`
Value string `yaml:"value"`
}
type TranslatableString struct {
ID string `yaml:"id"`
DefaultMessage string `yaml:"defaultMessage"`
Illustration string `yaml:"illustration"`
}
func (ts TranslatableString) Translate(t i18n.TranslateFunc) string {
if ts.ID != "" {
msg := t(ts.ID)
if msg != ts.ID && msg != "" {
return msg
}
}
return ts.DefaultMessage
}
type Description struct {
Channel *TranslatableString `yaml:"channel"`
Board *TranslatableString `yaml:"board"`
Playbook *TranslatableString `yaml:"playbook"`
Integration *TranslatableString `yaml:"integration"`
}
type Channel struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
Purpose string `yaml:"purpose"`
Playbook string `yaml:"playbook"`
Illustration string `yaml:"illustration"`
}
func (c *Channel) Validate() error {
if c.ID == "" {
return errors.New("id is required")
}
if c.Name == "" {
return errors.New("name is required")
}
return nil
}
type Board struct {
ID string `yaml:"id"`
Template string `yaml:"template"`
Name string `yaml:"name"`
Channel string `yaml:"channel"`
Illustration string `yaml:"illustration"`
}
func (b Board) Validate() error {
if b.ID == "" {
return errors.New("id is required")
}
if b.Template == "" {
return errors.New("template is required")
}
if b.Name == "" {
return errors.New("name is required")
}
return nil
}
type Playbook struct {
Template string `yaml:"template"`
Name string `yaml:"name"`
ID string `yaml:"id"`
Illustration string `yaml:"illustration"`
}
func (p *Playbook) Validate() error {
if p.ID == "" {
return errors.New("id is required")
}
if p.Template == "" {
return errors.New("template is required")
}
if p.Name == "" {
return errors.New("name is required")
}
return nil
}
type Integration struct {
ID string `yaml:"id"`
}
func (i *Integration) Validate() error {
if i.ID == "" {
return errors.New("id is required")
}
return nil
}
type Content struct {
Channel *Channel `yaml:"channel,omitempty"`
Board *Board `yaml:"board,omitempty"`
Playbook *Playbook `yaml:"playbook,omitempty"`
Integration *Integration `yaml:"integration,omitempty"`
}
func wrapContentError(err error, index int) error {
return errors.Wrapf(err, "content #%d validation failed", index)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Code generated by "make generate-worktemplates"
// DO NOT EDIT
package worktemplates
func init() {
registerWorkTemplateCategory("product_teams", wtc846b565cd80043537945134a54812e07)
registerWorkTemplateCategory("devops", wtca21c218df41f6d7fd032535fe20394e2)
registerWorkTemplateCategory("companywide", wtca6def90c2edac0c33650ac8ebee1e094)
registerWorkTemplateCategory("leadership", wtce9b74766edff1096ba7c67999ca259b6)
registerWorkTemplate("product_teams/feature_release:v1", wt00a1b44a5831c0a3acb14787b3fdd352)
registerWorkTemplate("product_teams/goals_and_okrs:v1", wt5baa68055bf9ea423273662e01ccc575)
registerWorkTemplate("product_teams/bug_bash:v1", wtfeb56bc6a8f277c47b503bd1c92d830e)
registerWorkTemplate("product_teams/sprint_planning:v1", wt8d2ef53deac5517eb349dc5de6150196)
registerWorkTemplate("product_teams/product_roadmap:v1", wt00ab91a945627f4a624957dd80490bb2)
registerWorkTemplate("devops/incident_resolution:v1", wtce19b9352a59d6a5d26f292d83e84377)
registerWorkTemplate("devops/product_release:v1", wt37406285a41c18bcdeb881189f7acde0)
registerWorkTemplate("companywide/goals_and_okrs:v1", wtf7b846d35810f8272eeb9a1a562025b5)
registerWorkTemplate("companywide/create_project:v1", wtb9ab412890c2410c7b49eec8f12e7edc)
registerWorkTemplate("leadership/goals_and_okrs:v1", wt32ab773bfe021e3d4913931041552559)
// Register categories strings
_ = T("worktemplate.category.product_teams")
_ = T("worktemplate.category.devops")
_ = T("worktemplate.category.companywide")
_ = T("worktemplate.category.leadership")
// Register translation strings
_ = T("worktemplate.product_teams.feature_release.description.channel")
_ = T("worktemplate.product_teams.feature_release.description.board")
_ = T("worktemplate.product_teams.feature_release.description.playbook")
_ = T("worktemplate.product_teams.feature_release.description.integration")
_ = T("worktemplate.product_teams.goals_and_okrs.channel")
_ = T("worktemplate.product_teams.goals_and_okrs.board")
_ = T("worktemplate.product_teams.goals_and_okrs.integration")
_ = T("worktemplate.product_teams.bug_bash.channel")
_ = T("worktemplate.product_teams.bug_bash.board")
_ = T("worktemplate.product_teams.bug_bash.playbook")
_ = T("worktemplate.product_teams.bug_bash.integration")
_ = T("worktemplate.product_teams.sprint_planning.channel")
_ = T("worktemplate.product_teams.sprint_planning.board")
_ = T("worktemplate.product_teams.sprint_planning.integration")
_ = T("worktemplate.product_teams.product_roadmap.channel")
_ = T("worktemplate.product_teams.product_roadmap.board")
_ = T("worktemplate.devops.incident_resolution.description.channel")
_ = T("worktemplate.devops.incident_resolution.description.board")
_ = T("worktemplate.devops.incident_resolution.description.playbook")
_ = T("worktemplate.devops.product_release.channel")
_ = T("worktemplate.devops.product_release.board")
_ = T("worktemplate.devops.product_release.playbook")
_ = T("worktemplate.companywide.goals_and_okrs.channel")
_ = T("worktemplate.companywide.goals_and_okrs.board")
_ = T("worktemplate.companywide.goals_and_okrs.integration")
_ = T("worktemplate.companywide.create_project.channel")
_ = T("worktemplate.companywide.create_project.board")
_ = T("worktemplate.companywide.create_project.integration")
_ = T("worktemplate.leadership.goals_and_okrs.channel")
_ = T("worktemplate.leadership.goals_and_okrs.board")
_ = T("worktemplate.leadership.goals_and_okrs.integration")
}
var wtc846b565cd80043537945134a54812e07 = &WorkTemplateCategory{
ID: "product_teams",
Name: "worktemplate.category.product_teams",
}
var wtca21c218df41f6d7fd032535fe20394e2 = &WorkTemplateCategory{
ID: "devops",
Name: "worktemplate.category.devops",
}
var wtca6def90c2edac0c33650ac8ebee1e094 = &WorkTemplateCategory{
ID: "companywide",
Name: "worktemplate.category.companywide",
}
var wtce9b74766edff1096ba7c67999ca259b6 = &WorkTemplateCategory{
ID: "leadership",
Name: "worktemplate.category.leadership",
}
var wt00a1b44a5831c0a3acb14787b3fdd352 = &WorkTemplate{
ID: "product_teams/feature_release:v1",
Category: "product_teams",
UseCase: "Manage feature release",
Illustration: "/static/worktemplates/product_teams/feature_release/feature_release.png",
Visibility: "public",
Description: Description{
Channel: &TranslatableString{
ID: "worktemplate.product_teams.feature_release.description.channel",
DefaultMessage: "Chat with your team in a Feature Release channel that connects easily with your boards, playbooks and app bots.",
Illustration: "",
},
Board: &TranslatableString{
ID: "worktemplate.product_teams.feature_release.description.board",
DefaultMessage: "Use our Meeting Agenda board template for recurring meetings like standup and our Project Tasks board to manage the progress of tasks along the way.",
Illustration: "",
},
Playbook: &TranslatableString{
ID: "worktemplate.product_teams.feature_release.description.playbook",
DefaultMessage: "Create transparent workflows across development teams to ensure your feature development process is seamless.",
Illustration: "",
},
Integration: &TranslatableString{
ID: "worktemplate.product_teams.feature_release.description.integration",
DefaultMessage: "Increase productivity in your channel by integrating a Jira bot and Github bot. These will be downloaded for you.",
Illustration: "/static/worktemplates/integrations.png",
},
},
Content: []Content{
{
Channel: &Channel{
ID: "feature-release",
Name: "Feature Release",
Purpose: "",
Playbook: "product-release-playbook",
Illustration: "/static/worktemplates/product_teams/feature_release/channel.png",
},
},
{
Board: &Board{
ID: "board-meeting-agenda",
Template: "54fcf9c610f0ac5e4c522c0657c90602",
Name: "Meeting Agenda",
Channel: "feature-release",
Illustration: "/static/worktemplates/boards/meeting_agenda.png",
},
},
{
Board: &Board{
ID: "board-project-task",
Template: "a4ec399ab4f2088b1051c3cdf1dde4c3",
Name: "Project Task",
Channel: "feature-release",
Illustration: "/static/worktemplates/boards/project_tasks.png",
},
},
{
Playbook: &Playbook{
Template: "Product Release",
Name: "Feature release",
ID: "product-release-playbook",
Illustration: "/static/worktemplates/playbooks/product_release.png",
},
},
{
Integration: &Integration{
ID: "jira",
},
},
{
Integration: &Integration{
ID: "github",
},
},
},
}
var wt5baa68055bf9ea423273662e01ccc575 = &WorkTemplate{
ID: "product_teams/goals_and_okrs:v1",
Category: "product_teams",
UseCase: "Set goals and OKR's",
Illustration: "/static/worktemplates/product_teams/goals_and_okrs/goals_and_okrs.png",
Visibility: "public",
Description: Description{
Channel: &TranslatableString{
ID: "worktemplate.product_teams.goals_and_okrs.channel",
DefaultMessage: "Clear focus is essential to team success and with this Project you can document the team’s goals and OKR’s as well as post updates in the dedicated channel.",
Illustration: "",
},
Board: &TranslatableString{
ID: "worktemplate.product_teams.goals_and_okrs.board",
DefaultMessage: "Clear focus is essential to team success and with this Project you can document the team’s goals and OKR’s as well as post updates in the dedicated channel.",
Illustration: "",
},
Integration: &TranslatableString{
ID: "worktemplate.product_teams.goals_and_okrs.integration",
DefaultMessage: "Clear focus is essential to team success and with this Project you can document the team’s goals and OKR’s as well as post updates in the dedicated channel.",
Illustration: "/static/worktemplates/integrations.png",
},
},
Content: []Content{
{
Channel: &Channel{
ID: "channel-1674845108569",
Name: "Goals and OKR",
Purpose: "",
Playbook: "",
Illustration: "/static/worktemplates/product_teams/goals_and_okrs/channel.png",
},
},
{
Board: &Board{
ID: "board-1674845139258",
Template: "7ba22ccfdfac391d63dea5c4b8cde0de",
Name: "Goals and OKR",
Channel: "channel-1674845108569",
Illustration: "/static/worktemplates/boards/company_goal_and_okrs.png",
},
},
{
Board: &Board{
ID: "board-1674845175528",
Template: "54fcf9c610f0ac5e4c522c0657c90602",
Name: "Meeting Agenda",
Channel: "channel-1674845108569",
Illustration: "/static/worktemplates/boards/meeting_agenda.png",
},
},
{
Integration: &Integration{
ID: "zoom",
},
},
},
}
var wtfeb56bc6a8f277c47b503bd1c92d830e = &WorkTemplate{
ID: "product_teams/bug_bash:v1",
Category: "product_teams",
UseCase: "Run a bug bash",
Illustration: "/static/worktemplates/product_teams/bug_bash/bug_bash.png",
Visibility: "public",
Description: Description{
Channel: &TranslatableString{
ID: "worktemplate.product_teams.bug_bash.channel",
DefaultMessage: "Get organized and bash all the bugs with this project! Build momentum and measure progress using included Playbook, Board, and Channel.",
Illustration: "",
},
Board: &TranslatableString{
ID: "worktemplate.product_teams.bug_bash.board",
DefaultMessage: "Get organized and bash all the bugs with this project! Build momentum and measure progress using included Playbook, Board, and Channel.",
Illustration: "",
},
Playbook: &TranslatableString{
ID: "worktemplate.product_teams.bug_bash.playbook",
DefaultMessage: "Get organized and bash all the bugs with this project! Build momentum and measure progress using included Playbook, Board, and Channel.",
Illustration: "",
},
Integration: &TranslatableString{
ID: "worktemplate.product_teams.bug_bash.integration",
DefaultMessage: "Get organized and bash all the bugs with this project! Build momentum and measure progress using included Playbook, Board, and Channel.",
Illustration: "/static/worktemplates/integrations.png",
},
},
Content: []Content{
{
Playbook: &Playbook{
Template: "Bug Bash",
Name: "Bug Bash",
ID: "playbook-1674844017943",
Illustration: "/static/worktemplates/playbooks/bug_bash.png",
},
},
{
Channel: &Channel{
ID: "channel-1674844017943",
Name: "Bug Bash",
Purpose: "",
Playbook: "playbook-1674844017943",
Illustration: "/static/worktemplates/product_teams/bug_bash/channel.png",
},
},
{
Integration: &Integration{
ID: "jira",
},
},
},
}
var wt8d2ef53deac5517eb349dc5de6150196 = &WorkTemplate{
ID: "product_teams/sprint_planning:v1",
Category: "product_teams",
UseCase: "Plan sprints",
Illustration: "/static/worktemplates/product_teams/sprint_planning/sprint_planning.png",
Visibility: "public",
Description: Description{
Channel: &TranslatableString{
ID: "worktemplate.product_teams.sprint_planning.channel",
DefaultMessage: "Use a Project to make sprint planning a breeze. The channel keeps the conversation and questions focused. The sprint plan keeps everyone on task for the week and the Retrospective board brings the team together to continuously improve.",
Illustration: "",
},
Board: &TranslatableString{
ID: "worktemplate.product_teams.sprint_planning.board",
DefaultMessage: "Use a Project to make sprint planning a breeze. The channel keeps the conversation and questions focused. The sprint plan keeps everyone on task for the week and the Retrospective board brings the team together to continuously improve.",
Illustration: "",
},
Integration: &TranslatableString{
ID: "worktemplate.product_teams.sprint_planning.integration",
DefaultMessage: "Use a Project to make sprint planning a breeze. The channel keeps the conversation and questions focused. The sprint plan keeps everyone on task for the week and the Retrospective board brings the team together to continuously improve.",
Illustration: "/static/worktemplates/integrations.png",
},
},
Content: []Content{
{
Channel: &Channel{
ID: "channel-1674850783500",
Name: "Sprint planning",
Purpose: "",
Playbook: "",
Illustration: "/static/worktemplates/product_teams/sprint_planning/channel.png",
},
},
{
Board: &Board{
ID: "board-1674850783973",
Template: "99b74e26d2f5d0a9b346d43c0a7bfb09",
Name: "Sprint planning",
Channel: "channel-1674850783500",
Illustration: "/static/worktemplates/boards/sprint_planner.png",
},
},
{
Integration: &Integration{
ID: "zoom",
},
},
},
}
var wt00ab91a945627f4a624957dd80490bb2 = &WorkTemplate{
ID: "product_teams/product_roadmap:v1",
Category: "product_teams",
UseCase: "Create a product roadmap",
Illustration: "/static/worktemplates/product_teams/product_roadmap/product_roadmap.png",
Visibility: "public",
Description: Description{
Channel: &TranslatableString{
ID: "worktemplate.product_teams.product_roadmap.channel",
DefaultMessage: "Description of why the channel(s) are needed",
Illustration: "",
},
Board: &TranslatableString{
ID: "worktemplate.product_teams.product_roadmap.board",
DefaultMessage: "Description of why the board(s) are needed",
Illustration: "",
},
},
Content: []Content{
{
Channel: &Channel{
ID: "channel-1674851139450",
Name: "Product Roadmap",
Purpose: "",
Playbook: "",
Illustration: "/static/worktemplates/product_teams/product_roadmap/channel.png",
},
},
{
Board: &Board{
ID: "board-1674851139759",
Template: "b728c6ca730e2cfc229741c5a4712b65",
Name: "Product Roadmap",
Channel: "channel-1674851139450",
Illustration: "/static/worktemplates/boards/roadmap.png",
},
},
},
}
var wtce19b9352a59d6a5d26f292d83e84377 = &WorkTemplate{
ID: "devops/incident_resolution:v1",
Category: "devops",
UseCase: "Resolve incidents",
Illustration: "/static/worktemplates/devops/incident_resolution/incident_resolution.png",
Visibility: "public",
Description: Description{
Channel: &TranslatableString{
ID: "worktemplate.devops.incident_resolution.description.channel",
DefaultMessage: "When everything is going wrong, having a repeatable process is the key to making sure everything is made right as quickly as possible. This Project combines everything Mattermost offers to ensure the fires are put out and stakeholders informed along the way.",
Illustration: "",
},
Board: &TranslatableString{
ID: "worktemplate.devops.incident_resolution.description.board",
DefaultMessage: "When everything is going wrong, having a repeatable process is the key to making sure everything is made right as quickly as possible. This Project combines everything Mattermost offers to ensure the fires are put out and stakeholders informed along the way.",
Illustration: "",
},
Playbook: &TranslatableString{
ID: "worktemplate.devops.incident_resolution.description.playbook",
DefaultMessage: "When everything is going wrong, having a repeatable process is the key to making sure everything is made right as quickly as possible. This Project combines everything Mattermost offers to ensure the fires are put out and stakeholders informed along the way.",
Illustration: "",
},
},
Content: []Content{
{
Playbook: &Playbook{
Template: "Incident Resolution",
Name: "Incident Resolution",
ID: "irpb",
Illustration: "/static/worktemplates/playbooks/incident_resolution.png",
},
},
{
Channel: &Channel{
ID: "irc",
Name: "Incident Resolution",
Purpose: "",
Playbook: "irpb",
Illustration: "/static/worktemplates/devops/incident_resolution/channel.png",
},
},
{
Board: &Board{
ID: "irb",
Template: "a4ec399ab4f2088b1051c3cdf1dde4c3",
Name: "Incident Resolution",
Channel: "irc",
Illustration: "/static/worktemplates/boards/project_tasks.png",
},
},
},
}
var wt37406285a41c18bcdeb881189f7acde0 = &WorkTemplate{
ID: "devops/product_release:v1",
Category: "devops",
UseCase: "Prepare a product release",
Illustration: "/static/worktemplates/devops/product_release/product_release.png",
Visibility: "public",
Description: Description{
Channel: &TranslatableString{
ID: "worktemplate.devops.product_release.channel",
DefaultMessage: "Don’t miss a step during a product release with this Project. Assign tasks from the Playbook checklist and hit milestones with the Board. Use Channels to keep everyone on the same page.",
Illustration: "",
},
Board: &TranslatableString{
ID: "worktemplate.devops.product_release.board",
DefaultMessage: "Don’t miss a step during a product release with this Project. Assign tasks from the Playbook checklist and hit milestones with the Board. Use Channels to keep everyone on the same page.",
Illustration: "",
},
Playbook: &TranslatableString{
ID: "worktemplate.devops.product_release.playbook",
DefaultMessage: "Don’t miss a step during a product release with this Project. Assign tasks from the Playbook checklist and hit milestones with the Board. Use Channels to keep everyone on the same page.",
Illustration: "",
},
},
Content: []Content{
{
Playbook: &Playbook{
Template: "Product Release",
Name: "Product Release",
ID: "playbook-1674851385983",
Illustration: "/static/worktemplates/playbooks/product_release.png",
},
},
{
Channel: &Channel{
ID: "channel-1674851385983",
Name: "Product Release",
Purpose: "",
Playbook: "playbook-1674851385983",
Illustration: "/static/worktemplates/devops/product_release/channel.png",
},
},
{
Board: &Board{
ID: "board-1674851386432",
Template: "a4ec399ab4f2088b1051c3cdf1dde4c3",
Name: "Product Release",
Channel: "channel-1674851385983",
Illustration: "/static/worktemplates/boards/project_tasks.png",
},
},
},
}
var wtf7b846d35810f8272eeb9a1a562025b5 = &WorkTemplate{
ID: "companywide/goals_and_okrs:v1",
Category: "companywide",
UseCase: "Set goals and OKR's",
Illustration: "/static/worktemplates/companywide/goals_and_okrs/goals_and_okrs.png",
Visibility: "public",
Description: Description{
Channel: &TranslatableString{
ID: "worktemplate.companywide.goals_and_okrs.channel",
DefaultMessage: "Clear focus is essential to team success and with this Project you can document the team’s goals and OKR’s as well as post updates in the dedicated channel.",
Illustration: "",
},
Board: &TranslatableString{
ID: "worktemplate.companywide.goals_and_okrs.board",
DefaultMessage: "Clear focus is essential to team success and with this Project you can document the team’s goals and OKR’s as well as post updates in the dedicated channel.",
Illustration: "",
},
Integration: &TranslatableString{
ID: "worktemplate.companywide.goals_and_okrs.integration",
DefaultMessage: "Clear focus is essential to team success and with this Project you can document the team’s goals and OKR’s as well as post updates in the dedicated channel.",
Illustration: "/static/worktemplates/integrations.png",
},
},
Content: []Content{
{
Channel: &Channel{
ID: "channel-1674845108569",
Name: "Goals and OKR",
Purpose: "",
Playbook: "",
Illustration: "/static/worktemplates/companywide/goals_and_okrs/channel.png",
},
},
{
Board: &Board{
ID: "board-1674845139258",
Template: "7ba22ccfdfac391d63dea5c4b8cde0de",
Name: "Goals and OKR",
Channel: "channel-1674845108569",
Illustration: "/static/worktemplates/boards/company_goal_and_okrs.png",
},
},
{
Integration: &Integration{
ID: "zoom",
},
},
},
}
var wtb9ab412890c2410c7b49eec8f12e7edc = &WorkTemplate{
ID: "companywide/create_project:v1",
Category: "companywide",
UseCase: "Create a project",
Illustration: "/static/worktemplates/companywide/create_project/create_project.svg",
Visibility: "public",
Description: Description{
Channel: &TranslatableString{
ID: "worktemplate.companywide.create_project.channel",
DefaultMessage: "Plan a Roadmap using this Project Board and collaborate on topic in the channel created with this template.",
Illustration: "",
},
Board: &TranslatableString{
ID: "worktemplate.companywide.create_project.board",
DefaultMessage: "Plan a Roadmap using this Project Board and collaborate on topic in the channel created with this template.",
Illustration: "",
},
Integration: &TranslatableString{
ID: "worktemplate.companywide.create_project.integration",
DefaultMessage: "Plan a Roadmap using this Project Board and collaborate on topic in the channel created with this template.",
Illustration: "/static/worktemplates/integrations.png",
},
},
Content: []Content{
{
Channel: &Channel{
ID: "channel-1674851940114",
Name: "Create Project",
Purpose: "",
Playbook: "",
Illustration: "/static/worktemplates/companywide/create_project/channel.png",
},
},
{
Board: &Board{
ID: "board-1674851940548",
Template: "a4ec399ab4f2088b1051c3cdf1dde4c3",
Name: "Create Project",
Channel: "channel-1674851940114",
Illustration: "/static/worktemplates/boards/project_tasks.png",
},
},
{
Integration: &Integration{
ID: "jira",
},
},
{
Integration: &Integration{
ID: "github",
},
},
{
Integration: &Integration{
ID: "zoom",
},
},
},
}
var wt32ab773bfe021e3d4913931041552559 = &WorkTemplate{
ID: "leadership/goals_and_okrs:v1",
Category: "leadership",
UseCase: "Set goals and OKR's",
Illustration: "/static/worktemplates/leadership/goals_and_okrs/goals_and_okrs.png",
Visibility: "public",
Description: Description{
Channel: &TranslatableString{
ID: "worktemplate.leadership.goals_and_okrs.channel",
DefaultMessage: "Clear focus is essential to team success and with this Project you can document the team’s goals and OKR’s as well as post updates in the dedicated channel.",
Illustration: "",
},
Board: &TranslatableString{
ID: "worktemplate.leadership.goals_and_okrs.board",
DefaultMessage: "Clear focus is essential to team success and with this Project you can document the team’s goals and OKR’s as well as post updates in the dedicated channel.",
Illustration: "",
},
Integration: &TranslatableString{
ID: "worktemplate.leadership.goals_and_okrs.integration",
DefaultMessage: "Clear focus is essential to team success and with this Project you can document the team’s goals and OKR’s as well as post updates in the dedicated channel.",
Illustration: "/static/worktemplates/integrations.png",
},
},
Content: []Content{
{
Channel: &Channel{
ID: "channel-1674845108569",
Name: "Goals and OKR",
Purpose: "",
Playbook: "",
Illustration: "/static/worktemplates/leadership/goals_and_okrs/channel.png",
},
},
{
Board: &Board{
ID: "board-1674845139258",
Template: "7ba22ccfdfac391d63dea5c4b8cde0de",
Name: "Goals and OKR",
Channel: "channel-1674845108569",
Illustration: "/static/worktemplates/boards/company_goal_and_okrs.png",
},
},
{
Integration: &Integration{
ID: "zoom",
},
},
},
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:generate go run generator/main.go
package worktemplates
var OrderedWorkTemplates = []*WorkTemplate{}
var OrderedWorkTemplateCategories = []*WorkTemplateCategory{}
// T is a placeholder to allow the translation tool to register the strings
func T(id string) string {
return id
}
func registerWorkTemplate(id string, wt *WorkTemplate) {
OrderedWorkTemplates = append(OrderedWorkTemplates, wt)
}
func registerWorkTemplateCategory(id string, wtc *WorkTemplateCategory) {
OrderedWorkTemplateCategories = append(OrderedWorkTemplateCategories, wtc)
}
func ListCategories() ([]*WorkTemplateCategory, error) {
return OrderedWorkTemplateCategories, nil
}
func ListByCategory(category string) ([]*WorkTemplate, error) {
wts := []*WorkTemplate{}
for i := range OrderedWorkTemplates {
if OrderedWorkTemplates[i].Category == category {
wts = append(wts, OrderedWorkTemplates[i])
}
}
return wts, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package audit
import (
"fmt"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type Audit struct {
logger *mlog.Logger
// OnQueueFull is called on an attempt to add an audit record to a full queue.
// Return true to drop record, or false to block until there is room in queue.
OnQueueFull func(qname string, maxQueueSize int) bool
// OnError is called when an error occurs while writing an audit record.
OnError func(err error)
}
func (a *Audit) Init(maxQueueSize int) {
a.logger, _ = mlog.NewLogger(
mlog.MaxQueueSize(maxQueueSize),
mlog.OnLoggerError(a.onLoggerError),
mlog.OnQueueFull(a.onQueueFull),
mlog.OnTargetQueueFull(a.onTargetQueueFull),
)
}
// LogRecord emits an audit record with complete info.
func (a *Audit) LogRecord(level mlog.Level, rec Record) {
flds := []mlog.Field{
mlog.String(KeyEventName, rec.EventName),
mlog.String(KeyStatus, rec.Status),
mlog.Any(KeyActor, rec.Actor),
mlog.Any(KeyEvent, rec.EventData),
mlog.Any(KeyMeta, rec.Meta),
mlog.Any(KeyError, rec.Error),
}
a.logger.Log(level, "", flds...)
}
// Configure sets zero or more target to output audit logs to.
func (a *Audit) Configure(cfg mlog.LoggerConfiguration) error {
return a.logger.ConfigureTargets(cfg, nil)
}
// Flush attempts to write all queued audit records to all targets.
func (a *Audit) Flush() error {
err := a.logger.Flush()
if err != nil {
a.onLoggerError(err)
}
return err
}
// Shutdown cleanly stops the audit engine after making best efforts to flush all targets.
func (a *Audit) Shutdown() error {
err := a.logger.Shutdown()
if err != nil {
a.onLoggerError(err)
}
return err
}
func (a *Audit) onQueueFull(rec *mlog.LogRec, maxQueueSize int) bool {
if a.OnQueueFull != nil {
return a.OnQueueFull("main", maxQueueSize)
}
mlog.Error("Audit logging queue full, dropping record.", mlog.Int("queueSize", maxQueueSize))
return true
}
func (a *Audit) onTargetQueueFull(target mlog.Target, rec *mlog.LogRec, maxQueueSize int) bool {
if a.OnQueueFull != nil {
return a.OnQueueFull(fmt.Sprintf("%v", target), maxQueueSize)
}
mlog.Error("Audit logging queue full for target, dropping record.", mlog.Any("target", target), mlog.Int("queueSize", maxQueueSize))
return true
}
func (a *Audit) onLoggerError(err error) {
if a.OnError != nil {
a.OnError(err)
return
}
mlog.Error("Auditing error", mlog.Err(err))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package audit
// Record provides a consistent set of fields used for all audit logging.
type Record struct {
EventName string `json:"event_name"`
Status string `json:"status"`
EventData EventData `json:"event"`
Actor EventActor `json:"actor"`
Meta map[string]interface{} `json:"meta"`
Error EventError `json:"error,omitempty"`
}
// EventData contains all event specific data about the modified entity
type EventData struct {
Parameters map[string]interface{} `json:"parameters"` // Payload and parameters being processed as part of the request
PriorState map[string]interface{} `json:"prior_state"` // Prior state of the object being modified, nil if no prior state
ResultState map[string]interface{} `json:"resulting_state"` // Resulting object after creating or modifying it
ObjectType string `json:"object_type"` // String representation of the object type. eg. "post"
}
// EventActor is the subject triggering the event
type EventActor struct {
UserId string `json:"user_id"`
SessionId string `json:"session_id"`
Client string `json:"client"`
IpAddress string `json:"ip_address"`
}
// EventMeta is a key-value store to store related information to the event that is not directly related to the modified entity
type EventMeta struct {
ApiPath string `json:"api_path"`
ClusterId string `json:"cluster_id"`
}
// EventError contains error information in case of failure of the event
type EventError struct {
Description string `json:"description,omitempty"`
Code int `json:"status_code,omitempty"`
}
// Auditable for sensitive object classes, consider implementing Auditable and include whatever the
// AuditableObject returns. For example: it's likely OK to write a user object to the
// audit logs, but not the user password in cleartext or hashed form
type Auditable interface {
Auditable() map[string]interface{}
}
// Success marks the audit record status as successful.
func (rec *Record) Success() {
rec.Status = Success
}
// Fail marks the audit record status as failed.
func (rec *Record) Fail() {
rec.Status = Fail
}
// AddEventParameter adds a parameter, e.g. query or post body, to the event
func AddEventParameter[T string | bool | int | int64 | []string | map[string]string](rec *Record, key string, val T) {
if rec.EventData.Parameters == nil {
rec.EventData.Parameters = make(map[string]interface{})
}
rec.EventData.Parameters[key] = val
}
// AddEventParameterAuditable adds an object that is of type Auditable to the event
func AddEventParameterAuditable(rec *Record, key string, val Auditable) {
if rec.EventData.Parameters == nil {
rec.EventData.Parameters = make(map[string]interface{})
}
rec.EventData.Parameters[key] = val.Auditable()
}
// AddEventParameterAuditableArray adds an array of objects of type Auditable to the event
func AddEventParameterAuditableArray[T Auditable](rec *Record, key string, val []T) {
if rec.EventData.Parameters == nil {
rec.EventData.Parameters = make(map[string]interface{})
}
processedAuditables := make([]map[string]interface{}, 0, len(val))
for _, auditableVal := range val {
processedAuditables = append(processedAuditables, auditableVal.Auditable())
}
rec.EventData.Parameters[key] = processedAuditables
}
// AddEventPriorState adds the prior state of the modified object to the audit record
func (rec *Record) AddEventPriorState(object Auditable) {
rec.EventData.PriorState = object.Auditable()
}
// AddEventResultState adds the result state of the modified object to the audit record
func (rec *Record) AddEventResultState(object Auditable) {
rec.EventData.ResultState = object.Auditable()
}
// AddEventObjectType adds the object type of the modified object to the audit record
func (rec *Record) AddEventObjectType(objectType string) {
rec.EventData.ObjectType = objectType
}
// AddMeta adds a key/value entry to the audit record that can be used for related information not directly related to
// the modified object, e.g. authentication method
func (rec *Record) AddMeta(name string, val interface{}) {
rec.Meta[name] = val
}
// AddErrorCode adds the error code for a failed event to the audit record
func (rec *Record) AddErrorCode(code int) {
rec.Error.Code = code
}
// AddErrorDesc adds the error description for a failed event to the audit record
func (rec *Record) AddErrorDesc(description string) {
rec.Error.Description = description
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package db
import "embed"
//go:embed migrations
var assets embed.FS
func Assets() embed.FS {
return assets
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// AccountMigrationInterface is an autogenerated mock type for the AccountMigrationInterface type
type AccountMigrationInterface struct {
mock.Mock
}
// MigrateToLdap provides a mock function with given fields: fromAuthService, foreignUserFieldNameToMatch, force, dryRun
func (_m *AccountMigrationInterface) MigrateToLdap(fromAuthService string, foreignUserFieldNameToMatch string, force bool, dryRun bool) *model.AppError {
ret := _m.Called(fromAuthService, foreignUserFieldNameToMatch, force, dryRun)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(string, string, bool, bool) *model.AppError); ok {
r0 = rf(fromAuthService, foreignUserFieldNameToMatch, force, dryRun)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// MigrateToSaml provides a mock function with given fields: fromAuthService, usersMap, auto, dryRun
func (_m *AccountMigrationInterface) MigrateToSaml(fromAuthService string, usersMap map[string]string, auto bool, dryRun bool) *model.AppError {
ret := _m.Called(fromAuthService, usersMap, auto, dryRun)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(string, map[string]string, bool, bool) *model.AppError); ok {
r0 = rf(fromAuthService, usersMap, auto, dryRun)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// AppContextInterface is an autogenerated mock type for the AppContextInterface type
type AppContextInterface struct {
mock.Mock
}
// AcceptLanguage provides a mock function with given fields:
func (_m *AppContextInterface) AcceptLanguage() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// IPAddress provides a mock function with given fields:
func (_m *AppContextInterface) IPAddress() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// Path provides a mock function with given fields:
func (_m *AppContextInterface) Path() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// RequestId provides a mock function with given fields:
func (_m *AppContextInterface) RequestId() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// Session provides a mock function with given fields:
func (_m *AppContextInterface) Session() *model.Session {
ret := _m.Called()
var r0 *model.Session
if rf, ok := ret.Get(0).(func() *model.Session); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Session)
}
}
return r0
}
// T provides a mock function with given fields: translationID, args
func (_m *AppContextInterface) T(translationID string, args ...any) string {
var _ca []any
_ca = append(_ca, translationID)
_ca = append(_ca, args...)
ret := _m.Called(_ca...)
var r0 string
if rf, ok := ret.Get(0).(func(string, ...any) string); ok {
r0 = rf(translationID, args...)
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// UserAgent provides a mock function with given fields:
func (_m *AppContextInterface) UserAgent() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// CloudInterface is an autogenerated mock type for the CloudInterface type
type CloudInterface struct {
mock.Mock
}
// BootstrapSelfHostedSignup provides a mock function with given fields: req
func (_m *CloudInterface) BootstrapSelfHostedSignup(req model.BootstrapSelfHostedSignupRequest) (*model.BootstrapSelfHostedSignupResponse, error) {
ret := _m.Called(req)
var r0 *model.BootstrapSelfHostedSignupResponse
if rf, ok := ret.Get(0).(func(model.BootstrapSelfHostedSignupRequest) *model.BootstrapSelfHostedSignupResponse); ok {
r0 = rf(req)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.BootstrapSelfHostedSignupResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(model.BootstrapSelfHostedSignupRequest) error); ok {
r1 = rf(req)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ChangeSubscription provides a mock function with given fields: userID, subscriptionID, subscriptionChange
func (_m *CloudInterface) ChangeSubscription(userID string, subscriptionID string, subscriptionChange *model.SubscriptionChange) (*model.Subscription, error) {
ret := _m.Called(userID, subscriptionID, subscriptionChange)
var r0 *model.Subscription
if rf, ok := ret.Get(0).(func(string, string, *model.SubscriptionChange) *model.Subscription); ok {
r0 = rf(userID, subscriptionID, subscriptionChange)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Subscription)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *model.SubscriptionChange) error); ok {
r1 = rf(userID, subscriptionID, subscriptionChange)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CheckCWSConnection provides a mock function with given fields: userId
func (_m *CloudInterface) CheckCWSConnection(userId string) error {
ret := _m.Called(userId)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userId)
} else {
r0 = ret.Error(0)
}
return r0
}
// ConfirmCustomerPayment provides a mock function with given fields: userID, confirmRequest
func (_m *CloudInterface) ConfirmCustomerPayment(userID string, confirmRequest *model.ConfirmPaymentMethodRequest) error {
ret := _m.Called(userID, confirmRequest)
var r0 error
if rf, ok := ret.Get(0).(func(string, *model.ConfirmPaymentMethodRequest) error); ok {
r0 = rf(userID, confirmRequest)
} else {
r0 = ret.Error(0)
}
return r0
}
// ConfirmSelfHostedSignup provides a mock function with given fields: req, requesterEmail
func (_m *CloudInterface) ConfirmSelfHostedSignup(req model.SelfHostedConfirmPaymentMethodRequest, requesterEmail string) (*model.SelfHostedSignupConfirmResponse, error) {
ret := _m.Called(req, requesterEmail)
var r0 *model.SelfHostedSignupConfirmResponse
if rf, ok := ret.Get(0).(func(model.SelfHostedConfirmPaymentMethodRequest, string) *model.SelfHostedSignupConfirmResponse); ok {
r0 = rf(req, requesterEmail)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SelfHostedSignupConfirmResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(model.SelfHostedConfirmPaymentMethodRequest, string) error); ok {
r1 = rf(req, requesterEmail)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ConfirmSelfHostedSignupLicenseApplication provides a mock function with given fields:
func (_m *CloudInterface) ConfirmSelfHostedSignupLicenseApplication() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// CreateCustomerPayment provides a mock function with given fields: userID
func (_m *CloudInterface) CreateCustomerPayment(userID string) (*model.StripeSetupIntent, error) {
ret := _m.Called(userID)
var r0 *model.StripeSetupIntent
if rf, ok := ret.Get(0).(func(string) *model.StripeSetupIntent); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.StripeSetupIntent)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateCustomerSelfHostedSignup provides a mock function with given fields: req, requesterEmail
func (_m *CloudInterface) CreateCustomerSelfHostedSignup(req model.SelfHostedCustomerForm, requesterEmail string) (*model.SelfHostedSignupCustomerResponse, error) {
ret := _m.Called(req, requesterEmail)
var r0 *model.SelfHostedSignupCustomerResponse
if rf, ok := ret.Get(0).(func(model.SelfHostedCustomerForm, string) *model.SelfHostedSignupCustomerResponse); ok {
r0 = rf(req, requesterEmail)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SelfHostedSignupCustomerResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(model.SelfHostedCustomerForm, string) error); ok {
r1 = rf(req, requesterEmail)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateOrUpdateSubscriptionHistoryEvent provides a mock function with given fields: userID, userCount
func (_m *CloudInterface) CreateOrUpdateSubscriptionHistoryEvent(userID string, userCount int) (*model.SubscriptionHistory, error) {
ret := _m.Called(userID, userCount)
var r0 *model.SubscriptionHistory
if rf, ok := ret.Get(0).(func(string, int) *model.SubscriptionHistory); ok {
r0 = rf(userID, userCount)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SubscriptionHistory)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int) error); ok {
r1 = rf(userID, userCount)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetCloudCustomer provides a mock function with given fields: userID
func (_m *CloudInterface) GetCloudCustomer(userID string) (*model.CloudCustomer, error) {
ret := _m.Called(userID)
var r0 *model.CloudCustomer
if rf, ok := ret.Get(0).(func(string) *model.CloudCustomer); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.CloudCustomer)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetCloudLimits provides a mock function with given fields: userID
func (_m *CloudInterface) GetCloudLimits(userID string) (*model.ProductLimits, error) {
ret := _m.Called(userID)
var r0 *model.ProductLimits
if rf, ok := ret.Get(0).(func(string) *model.ProductLimits); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ProductLimits)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetCloudProduct provides a mock function with given fields: userID, productID
func (_m *CloudInterface) GetCloudProduct(userID string, productID string) (*model.Product, error) {
ret := _m.Called(userID, productID)
var r0 *model.Product
if rf, ok := ret.Get(0).(func(string, string) *model.Product); ok {
r0 = rf(userID, productID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Product)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, productID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetCloudProducts provides a mock function with given fields: userID, includeLegacyProducts
func (_m *CloudInterface) GetCloudProducts(userID string, includeLegacyProducts bool) ([]*model.Product, error) {
ret := _m.Called(userID, includeLegacyProducts)
var r0 []*model.Product
if rf, ok := ret.Get(0).(func(string, bool) []*model.Product); ok {
r0 = rf(userID, includeLegacyProducts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Product)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(userID, includeLegacyProducts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetInvoicePDF provides a mock function with given fields: userID, invoiceID
func (_m *CloudInterface) GetInvoicePDF(userID string, invoiceID string) ([]byte, string, error) {
ret := _m.Called(userID, invoiceID)
var r0 []byte
if rf, ok := ret.Get(0).(func(string, string) []byte); ok {
r0 = rf(userID, invoiceID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
var r1 string
if rf, ok := ret.Get(1).(func(string, string) string); ok {
r1 = rf(userID, invoiceID)
} else {
r1 = ret.Get(1).(string)
}
var r2 error
if rf, ok := ret.Get(2).(func(string, string) error); ok {
r2 = rf(userID, invoiceID)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// GetInvoicesForSubscription provides a mock function with given fields: userID
func (_m *CloudInterface) GetInvoicesForSubscription(userID string) ([]*model.Invoice, error) {
ret := _m.Called(userID)
var r0 []*model.Invoice
if rf, ok := ret.Get(0).(func(string) []*model.Invoice); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Invoice)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetLicenseSelfServeStatus provides a mock function with given fields: userID, token
func (_m *CloudInterface) GetLicenseSelfServeStatus(userID string, token string) (*model.SubscriptionLicenseSelfServeStatusResponse, error) {
ret := _m.Called(userID, token)
var r0 *model.SubscriptionLicenseSelfServeStatusResponse
if rf, ok := ret.Get(0).(func(string, string) *model.SubscriptionLicenseSelfServeStatusResponse); ok {
r0 = rf(userID, token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SubscriptionLicenseSelfServeStatusResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, token)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSelfHostedInvoicePDF provides a mock function with given fields: invoiceID
func (_m *CloudInterface) GetSelfHostedInvoicePDF(invoiceID string) ([]byte, string, error) {
ret := _m.Called(invoiceID)
var r0 []byte
if rf, ok := ret.Get(0).(func(string) []byte); ok {
r0 = rf(invoiceID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
var r1 string
if rf, ok := ret.Get(1).(func(string) string); ok {
r1 = rf(invoiceID)
} else {
r1 = ret.Get(1).(string)
}
var r2 error
if rf, ok := ret.Get(2).(func(string) error); ok {
r2 = rf(invoiceID)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// GetSelfHostedInvoices provides a mock function with given fields:
func (_m *CloudInterface) GetSelfHostedInvoices() ([]*model.Invoice, error) {
ret := _m.Called()
var r0 []*model.Invoice
if rf, ok := ret.Get(0).(func() []*model.Invoice); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Invoice)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSelfHostedProducts provides a mock function with given fields: userID
func (_m *CloudInterface) GetSelfHostedProducts(userID string) ([]*model.Product, error) {
ret := _m.Called(userID)
var r0 []*model.Product
if rf, ok := ret.Get(0).(func(string) []*model.Product); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Product)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSubscription provides a mock function with given fields: userID
func (_m *CloudInterface) GetSubscription(userID string) (*model.Subscription, error) {
ret := _m.Called(userID)
var r0 *model.Subscription
if rf, ok := ret.Get(0).(func(string) *model.Subscription); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Subscription)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// HandleLicenseChange provides a mock function with given fields:
func (_m *CloudInterface) HandleLicenseChange() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// InvalidateCaches provides a mock function with given fields:
func (_m *CloudInterface) InvalidateCaches() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// RequestCloudTrial provides a mock function with given fields: userID, subscriptionID, newValidBusinessEmail
func (_m *CloudInterface) RequestCloudTrial(userID string, subscriptionID string, newValidBusinessEmail string) (*model.Subscription, error) {
ret := _m.Called(userID, subscriptionID, newValidBusinessEmail)
var r0 *model.Subscription
if rf, ok := ret.Get(0).(func(string, string, string) *model.Subscription); ok {
r0 = rf(userID, subscriptionID, newValidBusinessEmail)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Subscription)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string) error); ok {
r1 = rf(userID, subscriptionID, newValidBusinessEmail)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SelfHostedSignupAvailable provides a mock function with given fields:
func (_m *CloudInterface) SelfHostedSignupAvailable() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// SelfServeDeleteWorkspace provides a mock function with given fields: userID, deletionRequest
func (_m *CloudInterface) SelfServeDeleteWorkspace(userID string, deletionRequest *model.WorkspaceDeletionRequest) error {
ret := _m.Called(userID, deletionRequest)
var r0 error
if rf, ok := ret.Get(0).(func(string, *model.WorkspaceDeletionRequest) error); ok {
r0 = rf(userID, deletionRequest)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateCloudCustomer provides a mock function with given fields: userID, customerInfo
func (_m *CloudInterface) UpdateCloudCustomer(userID string, customerInfo *model.CloudCustomerInfo) (*model.CloudCustomer, error) {
ret := _m.Called(userID, customerInfo)
var r0 *model.CloudCustomer
if rf, ok := ret.Get(0).(func(string, *model.CloudCustomerInfo) *model.CloudCustomer); ok {
r0 = rf(userID, customerInfo)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.CloudCustomer)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, *model.CloudCustomerInfo) error); ok {
r1 = rf(userID, customerInfo)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateCloudCustomerAddress provides a mock function with given fields: userID, address
func (_m *CloudInterface) UpdateCloudCustomerAddress(userID string, address *model.Address) (*model.CloudCustomer, error) {
ret := _m.Called(userID, address)
var r0 *model.CloudCustomer
if rf, ok := ret.Get(0).(func(string, *model.Address) *model.CloudCustomer); ok {
r0 = rf(userID, address)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.CloudCustomer)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, *model.Address) error); ok {
r1 = rf(userID, address)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ValidateBusinessEmail provides a mock function with given fields: userID, email
func (_m *CloudInterface) ValidateBusinessEmail(userID string, email string) error {
ret := _m.Called(userID, email)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(userID, email)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// CloudJobInterface is an autogenerated mock type for the CloudJobInterface type
type CloudJobInterface struct {
mock.Mock
}
// MakeScheduler provides a mock function with given fields:
func (_m *CloudJobInterface) MakeScheduler() model.Scheduler {
ret := _m.Called()
var r0 model.Scheduler
if rf, ok := ret.Get(0).(func() model.Scheduler); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Scheduler)
}
}
return r0
}
// MakeWorker provides a mock function with given fields:
func (_m *CloudJobInterface) MakeWorker() model.Worker {
ret := _m.Called()
var r0 model.Worker
if rf, ok := ret.Get(0).(func() model.Worker); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Worker)
}
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
einterfaces "github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
mock "github.com/stretchr/testify/mock"
model "github.com/mattermost/mattermost-server/v6/model"
)
// ClusterInterface is an autogenerated mock type for the ClusterInterface type
type ClusterInterface struct {
mock.Mock
}
// ConfigChanged provides a mock function with given fields: previousConfig, newConfig, sendToOtherServer
func (_m *ClusterInterface) ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError {
ret := _m.Called(previousConfig, newConfig, sendToOtherServer)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(*model.Config, *model.Config, bool) *model.AppError); ok {
r0 = rf(previousConfig, newConfig, sendToOtherServer)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// GetClusterId provides a mock function with given fields:
func (_m *ClusterInterface) GetClusterId() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// GetClusterInfos provides a mock function with given fields:
func (_m *ClusterInterface) GetClusterInfos() []*model.ClusterInfo {
ret := _m.Called()
var r0 []*model.ClusterInfo
if rf, ok := ret.Get(0).(func() []*model.ClusterInfo); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ClusterInfo)
}
}
return r0
}
// GetClusterStats provides a mock function with given fields:
func (_m *ClusterInterface) GetClusterStats() ([]*model.ClusterStats, *model.AppError) {
ret := _m.Called()
var r0 []*model.ClusterStats
if rf, ok := ret.Get(0).(func() []*model.ClusterStats); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ClusterStats)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func() *model.AppError); ok {
r1 = rf()
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetLogs provides a mock function with given fields: page, perPage
func (_m *ClusterInterface) GetLogs(page int, perPage int) ([]string, *model.AppError) {
ret := _m.Called(page, perPage)
var r0 []string
if rf, ok := ret.Get(0).(func(int, int) []string); ok {
r0 = rf(page, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(int, int) *model.AppError); ok {
r1 = rf(page, perPage)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetMyClusterInfo provides a mock function with given fields:
func (_m *ClusterInterface) GetMyClusterInfo() *model.ClusterInfo {
ret := _m.Called()
var r0 *model.ClusterInfo
if rf, ok := ret.Get(0).(func() *model.ClusterInfo); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ClusterInfo)
}
}
return r0
}
// GetPluginStatuses provides a mock function with given fields:
func (_m *ClusterInterface) GetPluginStatuses() (model.PluginStatuses, *model.AppError) {
ret := _m.Called()
var r0 model.PluginStatuses
if rf, ok := ret.Get(0).(func() model.PluginStatuses); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.PluginStatuses)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func() *model.AppError); ok {
r1 = rf()
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// HealthScore provides a mock function with given fields:
func (_m *ClusterInterface) HealthScore() int {
ret := _m.Called()
var r0 int
if rf, ok := ret.Get(0).(func() int); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int)
}
return r0
}
// IsLeader provides a mock function with given fields:
func (_m *ClusterInterface) IsLeader() bool {
ret := _m.Called()
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// NotifyMsg provides a mock function with given fields: buf
func (_m *ClusterInterface) NotifyMsg(buf []byte) {
_m.Called(buf)
}
// QueryLogs provides a mock function with given fields: page, perPage
func (_m *ClusterInterface) QueryLogs(page int, perPage int) (map[string][]string, *model.AppError) {
ret := _m.Called(page, perPage)
var r0 map[string][]string
if rf, ok := ret.Get(0).(func(int, int) map[string][]string); ok {
r0 = rf(page, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string][]string)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(int, int) *model.AppError); ok {
r1 = rf(page, perPage)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// RegisterClusterMessageHandler provides a mock function with given fields: event, crm
func (_m *ClusterInterface) RegisterClusterMessageHandler(event model.ClusterEvent, crm einterfaces.ClusterMessageHandler) {
_m.Called(event, crm)
}
// SendClusterMessage provides a mock function with given fields: msg
func (_m *ClusterInterface) SendClusterMessage(msg *model.ClusterMessage) {
_m.Called(msg)
}
// SendClusterMessageToNode provides a mock function with given fields: nodeID, msg
func (_m *ClusterInterface) SendClusterMessageToNode(nodeID string, msg *model.ClusterMessage) error {
ret := _m.Called(nodeID, msg)
var r0 error
if rf, ok := ret.Get(0).(func(string, *model.ClusterMessage) error); ok {
r0 = rf(nodeID, msg)
} else {
r0 = ret.Error(0)
}
return r0
}
// StartInterNodeCommunication provides a mock function with given fields:
func (_m *ClusterInterface) StartInterNodeCommunication() {
_m.Called()
}
// StopInterNodeCommunication provides a mock function with given fields:
func (_m *ClusterInterface) StopInterNodeCommunication() {
_m.Called()
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// ClusterMessageHandler is an autogenerated mock type for the ClusterMessageHandler type
type ClusterMessageHandler struct {
mock.Mock
}
// Execute provides a mock function with given fields: msg
func (_m *ClusterMessageHandler) Execute(msg *model.ClusterMessage) {
_m.Called(msg)
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// ComplianceInterface is an autogenerated mock type for the ComplianceInterface type
type ComplianceInterface struct {
mock.Mock
}
// RunComplianceJob provides a mock function with given fields: job
func (_m *ComplianceInterface) RunComplianceJob(job *model.Compliance) *model.AppError {
ret := _m.Called(job)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(*model.Compliance) *model.AppError); ok {
r0 = rf(job)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// StartComplianceDailyJob provides a mock function with given fields:
func (_m *ComplianceInterface) StartComplianceDailyJob() {
_m.Called()
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// DataRetentionInterface is an autogenerated mock type for the DataRetentionInterface type
type DataRetentionInterface struct {
mock.Mock
}
// AddChannelsToPolicy provides a mock function with given fields: policyID, channelIDs
func (_m *DataRetentionInterface) AddChannelsToPolicy(policyID string, channelIDs []string) *model.AppError {
ret := _m.Called(policyID, channelIDs)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(string, []string) *model.AppError); ok {
r0 = rf(policyID, channelIDs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// AddTeamsToPolicy provides a mock function with given fields: policyID, teamIDs
func (_m *DataRetentionInterface) AddTeamsToPolicy(policyID string, teamIDs []string) *model.AppError {
ret := _m.Called(policyID, teamIDs)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(string, []string) *model.AppError); ok {
r0 = rf(policyID, teamIDs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// CreatePolicy provides a mock function with given fields: policy
func (_m *DataRetentionInterface) CreatePolicy(policy *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError) {
ret := _m.Called(policy)
var r0 *model.RetentionPolicyWithTeamAndChannelCounts
if rf, ok := ret.Get(0).(func(*model.RetentionPolicyWithTeamAndChannelIDs) *model.RetentionPolicyWithTeamAndChannelCounts); ok {
r0 = rf(policy)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RetentionPolicyWithTeamAndChannelCounts)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(*model.RetentionPolicyWithTeamAndChannelIDs) *model.AppError); ok {
r1 = rf(policy)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// DeletePolicy provides a mock function with given fields: policyID
func (_m *DataRetentionInterface) DeletePolicy(policyID string) *model.AppError {
ret := _m.Called(policyID)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(string) *model.AppError); ok {
r0 = rf(policyID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// GetChannelPoliciesForUser provides a mock function with given fields: userID, offset, limit
func (_m *DataRetentionInterface) GetChannelPoliciesForUser(userID string, offset int, limit int) (*model.RetentionPolicyForChannelList, *model.AppError) {
ret := _m.Called(userID, offset, limit)
var r0 *model.RetentionPolicyForChannelList
if rf, ok := ret.Get(0).(func(string, int, int) *model.RetentionPolicyForChannelList); ok {
r0 = rf(userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RetentionPolicyForChannelList)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(string, int, int) *model.AppError); ok {
r1 = rf(userID, offset, limit)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetChannelsForPolicy provides a mock function with given fields: policyID, offset, limit
func (_m *DataRetentionInterface) GetChannelsForPolicy(policyID string, offset int, limit int) (*model.ChannelsWithCount, *model.AppError) {
ret := _m.Called(policyID, offset, limit)
var r0 *model.ChannelsWithCount
if rf, ok := ret.Get(0).(func(string, int, int) *model.ChannelsWithCount); ok {
r0 = rf(policyID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ChannelsWithCount)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(string, int, int) *model.AppError); ok {
r1 = rf(policyID, offset, limit)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetGlobalPolicy provides a mock function with given fields:
func (_m *DataRetentionInterface) GetGlobalPolicy() (*model.GlobalRetentionPolicy, *model.AppError) {
ret := _m.Called()
var r0 *model.GlobalRetentionPolicy
if rf, ok := ret.Get(0).(func() *model.GlobalRetentionPolicy); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.GlobalRetentionPolicy)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func() *model.AppError); ok {
r1 = rf()
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetPolicies provides a mock function with given fields: offset, limit
func (_m *DataRetentionInterface) GetPolicies(offset int, limit int) (*model.RetentionPolicyWithTeamAndChannelCountsList, *model.AppError) {
ret := _m.Called(offset, limit)
var r0 *model.RetentionPolicyWithTeamAndChannelCountsList
if rf, ok := ret.Get(0).(func(int, int) *model.RetentionPolicyWithTeamAndChannelCountsList); ok {
r0 = rf(offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RetentionPolicyWithTeamAndChannelCountsList)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(int, int) *model.AppError); ok {
r1 = rf(offset, limit)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetPoliciesCount provides a mock function with given fields:
func (_m *DataRetentionInterface) GetPoliciesCount() (int64, *model.AppError) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func() *model.AppError); ok {
r1 = rf()
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetPolicy provides a mock function with given fields: policyID
func (_m *DataRetentionInterface) GetPolicy(policyID string) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError) {
ret := _m.Called(policyID)
var r0 *model.RetentionPolicyWithTeamAndChannelCounts
if rf, ok := ret.Get(0).(func(string) *model.RetentionPolicyWithTeamAndChannelCounts); ok {
r0 = rf(policyID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RetentionPolicyWithTeamAndChannelCounts)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(string) *model.AppError); ok {
r1 = rf(policyID)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetTeamPoliciesForUser provides a mock function with given fields: userID, offset, limit
func (_m *DataRetentionInterface) GetTeamPoliciesForUser(userID string, offset int, limit int) (*model.RetentionPolicyForTeamList, *model.AppError) {
ret := _m.Called(userID, offset, limit)
var r0 *model.RetentionPolicyForTeamList
if rf, ok := ret.Get(0).(func(string, int, int) *model.RetentionPolicyForTeamList); ok {
r0 = rf(userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RetentionPolicyForTeamList)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(string, int, int) *model.AppError); ok {
r1 = rf(userID, offset, limit)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetTeamsForPolicy provides a mock function with given fields: policyID, offset, limit
func (_m *DataRetentionInterface) GetTeamsForPolicy(policyID string, offset int, limit int) (*model.TeamsWithCount, *model.AppError) {
ret := _m.Called(policyID, offset, limit)
var r0 *model.TeamsWithCount
if rf, ok := ret.Get(0).(func(string, int, int) *model.TeamsWithCount); ok {
r0 = rf(policyID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TeamsWithCount)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(string, int, int) *model.AppError); ok {
r1 = rf(policyID, offset, limit)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// PatchPolicy provides a mock function with given fields: patch
func (_m *DataRetentionInterface) PatchPolicy(patch *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, *model.AppError) {
ret := _m.Called(patch)
var r0 *model.RetentionPolicyWithTeamAndChannelCounts
if rf, ok := ret.Get(0).(func(*model.RetentionPolicyWithTeamAndChannelIDs) *model.RetentionPolicyWithTeamAndChannelCounts); ok {
r0 = rf(patch)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RetentionPolicyWithTeamAndChannelCounts)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(*model.RetentionPolicyWithTeamAndChannelIDs) *model.AppError); ok {
r1 = rf(patch)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// RemoveChannelsFromPolicy provides a mock function with given fields: policyID, channelIDs
func (_m *DataRetentionInterface) RemoveChannelsFromPolicy(policyID string, channelIDs []string) *model.AppError {
ret := _m.Called(policyID, channelIDs)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(string, []string) *model.AppError); ok {
r0 = rf(policyID, channelIDs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// RemoveTeamsFromPolicy provides a mock function with given fields: policyID, teamIDs
func (_m *DataRetentionInterface) RemoveTeamsFromPolicy(policyID string, teamIDs []string) *model.AppError {
ret := _m.Called(policyID, teamIDs)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(string, []string) *model.AppError); ok {
r0 = rf(policyID, teamIDs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// DataRetentionJobInterface is an autogenerated mock type for the DataRetentionJobInterface type
type DataRetentionJobInterface struct {
mock.Mock
}
// MakeScheduler provides a mock function with given fields:
func (_m *DataRetentionJobInterface) MakeScheduler() model.Scheduler {
ret := _m.Called()
var r0 model.Scheduler
if rf, ok := ret.Get(0).(func() model.Scheduler); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Scheduler)
}
}
return r0
}
// MakeWorker provides a mock function with given fields:
func (_m *DataRetentionJobInterface) MakeWorker() model.Worker {
ret := _m.Called()
var r0 model.Worker
if rf, ok := ret.Get(0).(func() model.Worker); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Worker)
}
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// ElasticsearchAggregatorInterface is an autogenerated mock type for the ElasticsearchAggregatorInterface type
type ElasticsearchAggregatorInterface struct {
mock.Mock
}
// MakeScheduler provides a mock function with given fields:
func (_m *ElasticsearchAggregatorInterface) MakeScheduler() model.Scheduler {
ret := _m.Called()
var r0 model.Scheduler
if rf, ok := ret.Get(0).(func() model.Scheduler); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Scheduler)
}
}
return r0
}
// MakeWorker provides a mock function with given fields:
func (_m *ElasticsearchAggregatorInterface) MakeWorker() model.Worker {
ret := _m.Called()
var r0 model.Worker
if rf, ok := ret.Get(0).(func() model.Worker); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Worker)
}
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// ElasticsearchIndexerInterface is an autogenerated mock type for the ElasticsearchIndexerInterface type
type ElasticsearchIndexerInterface struct {
mock.Mock
}
// MakeWorker provides a mock function with given fields:
func (_m *ElasticsearchIndexerInterface) MakeWorker() model.Worker {
ret := _m.Called()
var r0 model.Worker
if rf, ok := ret.Get(0).(func() model.Worker); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Worker)
}
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// IndexerJobInterface is an autogenerated mock type for the IndexerJobInterface type
type IndexerJobInterface struct {
mock.Mock
}
// MakeWorker provides a mock function with given fields:
func (_m *IndexerJobInterface) MakeWorker() model.Worker {
ret := _m.Called()
var r0 model.Worker
if rf, ok := ret.Get(0).(func() model.Worker); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Worker)
}
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
request "github.com/mattermost/mattermost-server/v6/server/channels/app/request"
mock "github.com/stretchr/testify/mock"
)
// LdapInterface is an autogenerated mock type for the LdapInterface type
type LdapInterface struct {
mock.Mock
}
// CheckPassword provides a mock function with given fields: id, password
func (_m *LdapInterface) CheckPassword(id string, password string) *model.AppError {
ret := _m.Called(id, password)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(string, string) *model.AppError); ok {
r0 = rf(id, password)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// CheckPasswordAuthData provides a mock function with given fields: authData, password
func (_m *LdapInterface) CheckPasswordAuthData(authData string, password string) *model.AppError {
ret := _m.Called(authData, password)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(string, string) *model.AppError); ok {
r0 = rf(authData, password)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// CheckProviderAttributes provides a mock function with given fields: LS, ouser, patch
func (_m *LdapInterface) CheckProviderAttributes(LS *model.LdapSettings, ouser *model.User, patch *model.UserPatch) string {
ret := _m.Called(LS, ouser, patch)
var r0 string
if rf, ok := ret.Get(0).(func(*model.LdapSettings, *model.User, *model.UserPatch) string); ok {
r0 = rf(LS, ouser, patch)
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// DoLogin provides a mock function with given fields: c, id, password
func (_m *LdapInterface) DoLogin(c *request.Context, id string, password string) (*model.User, *model.AppError) {
ret := _m.Called(c, id, password)
var r0 *model.User
if rf, ok := ret.Get(0).(func(*request.Context, string, string) *model.User); ok {
r0 = rf(c, id, password)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.User)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(*request.Context, string, string) *model.AppError); ok {
r1 = rf(c, id, password)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// FirstLoginSync provides a mock function with given fields: c, user, userAuthService, userAuthData, email
func (_m *LdapInterface) FirstLoginSync(c *request.Context, user *model.User, userAuthService string, userAuthData string, email string) *model.AppError {
ret := _m.Called(c, user, userAuthService, userAuthData, email)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(*request.Context, *model.User, string, string, string) *model.AppError); ok {
r0 = rf(c, user, userAuthService, userAuthData, email)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// GetADLdapIdFromSAMLId provides a mock function with given fields: authData
func (_m *LdapInterface) GetADLdapIdFromSAMLId(authData string) string {
ret := _m.Called(authData)
var r0 string
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(authData)
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// GetAllGroupsPage provides a mock function with given fields: page, perPage, opts
func (_m *LdapInterface) GetAllGroupsPage(page int, perPage int, opts model.LdapGroupSearchOpts) ([]*model.Group, int, *model.AppError) {
ret := _m.Called(page, perPage, opts)
var r0 []*model.Group
if rf, ok := ret.Get(0).(func(int, int, model.LdapGroupSearchOpts) []*model.Group); ok {
r0 = rf(page, perPage, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Group)
}
}
var r1 int
if rf, ok := ret.Get(1).(func(int, int, model.LdapGroupSearchOpts) int); ok {
r1 = rf(page, perPage, opts)
} else {
r1 = ret.Get(1).(int)
}
var r2 *model.AppError
if rf, ok := ret.Get(2).(func(int, int, model.LdapGroupSearchOpts) *model.AppError); ok {
r2 = rf(page, perPage, opts)
} else {
if ret.Get(2) != nil {
r2 = ret.Get(2).(*model.AppError)
}
}
return r0, r1, r2
}
// GetAllLdapUsers provides a mock function with given fields:
func (_m *LdapInterface) GetAllLdapUsers() ([]*model.User, *model.AppError) {
ret := _m.Called()
var r0 []*model.User
if rf, ok := ret.Get(0).(func() []*model.User); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func() *model.AppError); ok {
r1 = rf()
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetGroup provides a mock function with given fields: groupUID
func (_m *LdapInterface) GetGroup(groupUID string) (*model.Group, *model.AppError) {
ret := _m.Called(groupUID)
var r0 *model.Group
if rf, ok := ret.Get(0).(func(string) *model.Group); ok {
r0 = rf(groupUID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(string) *model.AppError); ok {
r1 = rf(groupUID)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetSAMLIdFromADLdapId provides a mock function with given fields: authData
func (_m *LdapInterface) GetSAMLIdFromADLdapId(authData string) string {
ret := _m.Called(authData)
var r0 string
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(authData)
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// GetUser provides a mock function with given fields: id
func (_m *LdapInterface) GetUser(id string) (*model.User, *model.AppError) {
ret := _m.Called(id)
var r0 *model.User
if rf, ok := ret.Get(0).(func(string) *model.User); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.User)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(string) *model.AppError); ok {
r1 = rf(id)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetUserAttributes provides a mock function with given fields: id, attributes
func (_m *LdapInterface) GetUserAttributes(id string, attributes []string) (map[string]string, *model.AppError) {
ret := _m.Called(id, attributes)
var r0 map[string]string
if rf, ok := ret.Get(0).(func(string, []string) map[string]string); ok {
r0 = rf(id, attributes)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]string)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(string, []string) *model.AppError); ok {
r1 = rf(id, attributes)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetVendorNameAndVendorVersion provides a mock function with given fields:
func (_m *LdapInterface) GetVendorNameAndVendorVersion() (string, string) {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
var r1 string
if rf, ok := ret.Get(1).(func() string); ok {
r1 = rf()
} else {
r1 = ret.Get(1).(string)
}
return r0, r1
}
// MigrateIDAttribute provides a mock function with given fields: toAttribute
func (_m *LdapInterface) MigrateIDAttribute(toAttribute string) error {
ret := _m.Called(toAttribute)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(toAttribute)
} else {
r0 = ret.Error(0)
}
return r0
}
// RunTest provides a mock function with given fields:
func (_m *LdapInterface) RunTest() *model.AppError {
ret := _m.Called()
var r0 *model.AppError
if rf, ok := ret.Get(0).(func() *model.AppError); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// StartSynchronizeJob provides a mock function with given fields: waitForJobToFinish, includeRemovedMembers
func (_m *LdapInterface) StartSynchronizeJob(waitForJobToFinish bool, includeRemovedMembers bool) (*model.Job, *model.AppError) {
ret := _m.Called(waitForJobToFinish, includeRemovedMembers)
var r0 *model.Job
if rf, ok := ret.Get(0).(func(bool, bool) *model.Job); ok {
r0 = rf(waitForJobToFinish, includeRemovedMembers)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Job)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(bool, bool) *model.AppError); ok {
r1 = rf(waitForJobToFinish, includeRemovedMembers)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// SwitchToLdap provides a mock function with given fields: userID, ldapID, ldapPassword
func (_m *LdapInterface) SwitchToLdap(userID string, ldapID string, ldapPassword string) *model.AppError {
ret := _m.Called(userID, ldapID, ldapPassword)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(string, string, string) *model.AppError); ok {
r0 = rf(userID, ldapID, ldapPassword)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// UpdateProfilePictureIfNecessary provides a mock function with given fields: _a0, _a1, _a2
func (_m *LdapInterface) UpdateProfilePictureIfNecessary(_a0 request.CTX, _a1 model.User, _a2 model.Session) {
_m.Called(_a0, _a1, _a2)
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// LdapSyncInterface is an autogenerated mock type for the LdapSyncInterface type
type LdapSyncInterface struct {
mock.Mock
}
// MakeScheduler provides a mock function with given fields:
func (_m *LdapSyncInterface) MakeScheduler() model.Scheduler {
ret := _m.Called()
var r0 model.Scheduler
if rf, ok := ret.Get(0).(func() model.Scheduler); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Scheduler)
}
}
return r0
}
// MakeWorker provides a mock function with given fields:
func (_m *LdapSyncInterface) MakeWorker() model.Worker {
ret := _m.Called()
var r0 model.Worker
if rf, ok := ret.Get(0).(func() model.Worker); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Worker)
}
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// LicenseInterface is an autogenerated mock type for the LicenseInterface type
type LicenseInterface struct {
mock.Mock
}
// CanStartTrial provides a mock function with given fields:
func (_m *LicenseInterface) CanStartTrial() (bool, error) {
ret := _m.Called()
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPrevTrial provides a mock function with given fields:
func (_m *LicenseInterface) GetPrevTrial() (*model.License, error) {
ret := _m.Called()
var r0 *model.License
if rf, ok := ret.Get(0).(func() *model.License); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.License)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
context "context"
mock "github.com/stretchr/testify/mock"
model "github.com/mattermost/mattermost-server/v6/model"
)
// MessageExportInterface is an autogenerated mock type for the MessageExportInterface type
type MessageExportInterface struct {
mock.Mock
}
// RunExport provides a mock function with given fields: format, since, limit
func (_m *MessageExportInterface) RunExport(format string, since int64, limit int) (int64, *model.AppError) {
ret := _m.Called(format, since, limit)
var r0 int64
if rf, ok := ret.Get(0).(func(string, int64, int) int64); ok {
r0 = rf(format, since, limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(string, int64, int) *model.AppError); ok {
r1 = rf(format, since, limit)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// StartSynchronizeJob provides a mock function with given fields: ctx, exportFromTimestamp
func (_m *MessageExportInterface) StartSynchronizeJob(ctx context.Context, exportFromTimestamp int64) (*model.Job, *model.AppError) {
ret := _m.Called(ctx, exportFromTimestamp)
var r0 *model.Job
if rf, ok := ret.Get(0).(func(context.Context, int64) *model.Job); ok {
r0 = rf(ctx, exportFromTimestamp)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Job)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(context.Context, int64) *model.AppError); ok {
r1 = rf(ctx, exportFromTimestamp)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// MessageExportJobInterface is an autogenerated mock type for the MessageExportJobInterface type
type MessageExportJobInterface struct {
mock.Mock
}
// MakeScheduler provides a mock function with given fields:
func (_m *MessageExportJobInterface) MakeScheduler() model.Scheduler {
ret := _m.Called()
var r0 model.Scheduler
if rf, ok := ret.Get(0).(func() model.Scheduler); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Scheduler)
}
}
return r0
}
// MakeWorker provides a mock function with given fields:
func (_m *MessageExportJobInterface) MakeWorker() model.Worker {
ret := _m.Called()
var r0 model.Worker
if rf, ok := ret.Get(0).(func() model.Worker); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Worker)
}
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
logr "github.com/mattermost/logr/v2"
mock "github.com/stretchr/testify/mock"
model "github.com/mattermost/mattermost-server/v6/model"
sql "database/sql"
)
// MetricsInterface is an autogenerated mock type for the MetricsInterface type
type MetricsInterface struct {
mock.Mock
}
// AddMemCacheHitCounter provides a mock function with given fields: cacheName, amount
func (_m *MetricsInterface) AddMemCacheHitCounter(cacheName string, amount float64) {
_m.Called(cacheName, amount)
}
// AddMemCacheMissCounter provides a mock function with given fields: cacheName, amount
func (_m *MetricsInterface) AddMemCacheMissCounter(cacheName string, amount float64) {
_m.Called(cacheName, amount)
}
// DecrementJobActive provides a mock function with given fields: jobType
func (_m *MetricsInterface) DecrementJobActive(jobType string) {
_m.Called(jobType)
}
// DecrementWebSocketBroadcastBufferSize provides a mock function with given fields: hub, amount
func (_m *MetricsInterface) DecrementWebSocketBroadcastBufferSize(hub string, amount float64) {
_m.Called(hub, amount)
}
// DecrementWebSocketBroadcastUsersRegistered provides a mock function with given fields: hub, amount
func (_m *MetricsInterface) DecrementWebSocketBroadcastUsersRegistered(hub string, amount float64) {
_m.Called(hub, amount)
}
// GetLoggerMetricsCollector provides a mock function with given fields:
func (_m *MetricsInterface) GetLoggerMetricsCollector() logr.MetricsCollector {
ret := _m.Called()
var r0 logr.MetricsCollector
if rf, ok := ret.Get(0).(func() logr.MetricsCollector); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(logr.MetricsCollector)
}
}
return r0
}
// IncrementChannelIndexCounter provides a mock function with given fields:
func (_m *MetricsInterface) IncrementChannelIndexCounter() {
_m.Called()
}
// IncrementClusterEventType provides a mock function with given fields: eventType
func (_m *MetricsInterface) IncrementClusterEventType(eventType model.ClusterEvent) {
_m.Called(eventType)
}
// IncrementClusterRequest provides a mock function with given fields:
func (_m *MetricsInterface) IncrementClusterRequest() {
_m.Called()
}
// IncrementEtagHitCounter provides a mock function with given fields: route
func (_m *MetricsInterface) IncrementEtagHitCounter(route string) {
_m.Called(route)
}
// IncrementEtagMissCounter provides a mock function with given fields: route
func (_m *MetricsInterface) IncrementEtagMissCounter(route string) {
_m.Called(route)
}
// IncrementFileIndexCounter provides a mock function with given fields:
func (_m *MetricsInterface) IncrementFileIndexCounter() {
_m.Called()
}
// IncrementFilesSearchCounter provides a mock function with given fields:
func (_m *MetricsInterface) IncrementFilesSearchCounter() {
_m.Called()
}
// IncrementHTTPError provides a mock function with given fields:
func (_m *MetricsInterface) IncrementHTTPError() {
_m.Called()
}
// IncrementHTTPRequest provides a mock function with given fields:
func (_m *MetricsInterface) IncrementHTTPRequest() {
_m.Called()
}
// IncrementJobActive provides a mock function with given fields: jobType
func (_m *MetricsInterface) IncrementJobActive(jobType string) {
_m.Called(jobType)
}
// IncrementLogin provides a mock function with given fields:
func (_m *MetricsInterface) IncrementLogin() {
_m.Called()
}
// IncrementLoginFail provides a mock function with given fields:
func (_m *MetricsInterface) IncrementLoginFail() {
_m.Called()
}
// IncrementMemCacheHitCounter provides a mock function with given fields: cacheName
func (_m *MetricsInterface) IncrementMemCacheHitCounter(cacheName string) {
_m.Called(cacheName)
}
// IncrementMemCacheHitCounterSession provides a mock function with given fields:
func (_m *MetricsInterface) IncrementMemCacheHitCounterSession() {
_m.Called()
}
// IncrementMemCacheInvalidationCounter provides a mock function with given fields: cacheName
func (_m *MetricsInterface) IncrementMemCacheInvalidationCounter(cacheName string) {
_m.Called(cacheName)
}
// IncrementMemCacheInvalidationCounterSession provides a mock function with given fields:
func (_m *MetricsInterface) IncrementMemCacheInvalidationCounterSession() {
_m.Called()
}
// IncrementMemCacheMissCounter provides a mock function with given fields: cacheName
func (_m *MetricsInterface) IncrementMemCacheMissCounter(cacheName string) {
_m.Called(cacheName)
}
// IncrementMemCacheMissCounterSession provides a mock function with given fields:
func (_m *MetricsInterface) IncrementMemCacheMissCounterSession() {
_m.Called()
}
// IncrementPostBroadcast provides a mock function with given fields:
func (_m *MetricsInterface) IncrementPostBroadcast() {
_m.Called()
}
// IncrementPostCreate provides a mock function with given fields:
func (_m *MetricsInterface) IncrementPostCreate() {
_m.Called()
}
// IncrementPostFileAttachment provides a mock function with given fields: count
func (_m *MetricsInterface) IncrementPostFileAttachment(count int) {
_m.Called(count)
}
// IncrementPostIndexCounter provides a mock function with given fields:
func (_m *MetricsInterface) IncrementPostIndexCounter() {
_m.Called()
}
// IncrementPostSentEmail provides a mock function with given fields:
func (_m *MetricsInterface) IncrementPostSentEmail() {
_m.Called()
}
// IncrementPostSentPush provides a mock function with given fields:
func (_m *MetricsInterface) IncrementPostSentPush() {
_m.Called()
}
// IncrementPostsSearchCounter provides a mock function with given fields:
func (_m *MetricsInterface) IncrementPostsSearchCounter() {
_m.Called()
}
// IncrementRemoteClusterConnStateChangeCounter provides a mock function with given fields: remoteID, online
func (_m *MetricsInterface) IncrementRemoteClusterConnStateChangeCounter(remoteID string, online bool) {
_m.Called(remoteID, online)
}
// IncrementRemoteClusterMsgErrorsCounter provides a mock function with given fields: remoteID, timeout
func (_m *MetricsInterface) IncrementRemoteClusterMsgErrorsCounter(remoteID string, timeout bool) {
_m.Called(remoteID, timeout)
}
// IncrementRemoteClusterMsgReceivedCounter provides a mock function with given fields: remoteID
func (_m *MetricsInterface) IncrementRemoteClusterMsgReceivedCounter(remoteID string) {
_m.Called(remoteID)
}
// IncrementRemoteClusterMsgSentCounter provides a mock function with given fields: remoteID
func (_m *MetricsInterface) IncrementRemoteClusterMsgSentCounter(remoteID string) {
_m.Called(remoteID)
}
// IncrementUserIndexCounter provides a mock function with given fields:
func (_m *MetricsInterface) IncrementUserIndexCounter() {
_m.Called()
}
// IncrementWebSocketBroadcast provides a mock function with given fields: eventType
func (_m *MetricsInterface) IncrementWebSocketBroadcast(eventType string) {
_m.Called(eventType)
}
// IncrementWebSocketBroadcastBufferSize provides a mock function with given fields: hub, amount
func (_m *MetricsInterface) IncrementWebSocketBroadcastBufferSize(hub string, amount float64) {
_m.Called(hub, amount)
}
// IncrementWebSocketBroadcastUsersRegistered provides a mock function with given fields: hub, amount
func (_m *MetricsInterface) IncrementWebSocketBroadcastUsersRegistered(hub string, amount float64) {
_m.Called(hub, amount)
}
// IncrementWebhookPost provides a mock function with given fields:
func (_m *MetricsInterface) IncrementWebhookPost() {
_m.Called()
}
// IncrementWebsocketEvent provides a mock function with given fields: eventType
func (_m *MetricsInterface) IncrementWebsocketEvent(eventType string) {
_m.Called(eventType)
}
// IncrementWebsocketReconnectEvent provides a mock function with given fields: eventType
func (_m *MetricsInterface) IncrementWebsocketReconnectEvent(eventType string) {
_m.Called(eventType)
}
// ObserveAPIEndpointDuration provides a mock function with given fields: endpoint, method, statusCode, elapsed
func (_m *MetricsInterface) ObserveAPIEndpointDuration(endpoint string, method string, statusCode string, elapsed float64) {
_m.Called(endpoint, method, statusCode, elapsed)
}
// ObserveClusterRequestDuration provides a mock function with given fields: elapsed
func (_m *MetricsInterface) ObserveClusterRequestDuration(elapsed float64) {
_m.Called(elapsed)
}
// ObserveEnabledUsers provides a mock function with given fields: users
func (_m *MetricsInterface) ObserveEnabledUsers(users int64) {
_m.Called(users)
}
// ObserveFilesSearchDuration provides a mock function with given fields: elapsed
func (_m *MetricsInterface) ObserveFilesSearchDuration(elapsed float64) {
_m.Called(elapsed)
}
// ObservePluginAPIDuration provides a mock function with given fields: pluginID, apiName, success, elapsed
func (_m *MetricsInterface) ObservePluginAPIDuration(pluginID string, apiName string, success bool, elapsed float64) {
_m.Called(pluginID, apiName, success, elapsed)
}
// ObservePluginHookDuration provides a mock function with given fields: pluginID, hookName, success, elapsed
func (_m *MetricsInterface) ObservePluginHookDuration(pluginID string, hookName string, success bool, elapsed float64) {
_m.Called(pluginID, hookName, success, elapsed)
}
// ObservePluginMultiHookDuration provides a mock function with given fields: elapsed
func (_m *MetricsInterface) ObservePluginMultiHookDuration(elapsed float64) {
_m.Called(elapsed)
}
// ObservePluginMultiHookIterationDuration provides a mock function with given fields: pluginID, elapsed
func (_m *MetricsInterface) ObservePluginMultiHookIterationDuration(pluginID string, elapsed float64) {
_m.Called(pluginID, elapsed)
}
// ObservePostsSearchDuration provides a mock function with given fields: elapsed
func (_m *MetricsInterface) ObservePostsSearchDuration(elapsed float64) {
_m.Called(elapsed)
}
// ObserveRemoteClusterClockSkew provides a mock function with given fields: remoteID, skew
func (_m *MetricsInterface) ObserveRemoteClusterClockSkew(remoteID string, skew float64) {
_m.Called(remoteID, skew)
}
// ObserveRemoteClusterPingDuration provides a mock function with given fields: remoteID, elapsed
func (_m *MetricsInterface) ObserveRemoteClusterPingDuration(remoteID string, elapsed float64) {
_m.Called(remoteID, elapsed)
}
// ObserveStoreMethodDuration provides a mock function with given fields: method, success, elapsed
func (_m *MetricsInterface) ObserveStoreMethodDuration(method string, success string, elapsed float64) {
_m.Called(method, success, elapsed)
}
// Register provides a mock function with given fields:
func (_m *MetricsInterface) Register() {
_m.Called()
}
// RegisterDBCollector provides a mock function with given fields: db, name
func (_m *MetricsInterface) RegisterDBCollector(db *sql.DB, name string) {
_m.Called(db, name)
}
// SetReplicaLagAbsolute provides a mock function with given fields: node, value
func (_m *MetricsInterface) SetReplicaLagAbsolute(node string, value float64) {
_m.Called(node, value)
}
// SetReplicaLagTime provides a mock function with given fields: node, value
func (_m *MetricsInterface) SetReplicaLagTime(node string, value float64) {
_m.Called(node, value)
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// MfaInterface is an autogenerated mock type for the MfaInterface type
type MfaInterface struct {
mock.Mock
}
// Activate provides a mock function with given fields: user, token
func (_m *MfaInterface) Activate(user *model.User, token string) *model.AppError {
ret := _m.Called(user, token)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(*model.User, string) *model.AppError); ok {
r0 = rf(user, token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// Deactivate provides a mock function with given fields: userID
func (_m *MfaInterface) Deactivate(userID string) *model.AppError {
ret := _m.Called(userID)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(string) *model.AppError); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// GenerateSecret provides a mock function with given fields: user
func (_m *MfaInterface) GenerateSecret(user *model.User) (string, []byte, *model.AppError) {
ret := _m.Called(user)
var r0 string
if rf, ok := ret.Get(0).(func(*model.User) string); ok {
r0 = rf(user)
} else {
r0 = ret.Get(0).(string)
}
var r1 []byte
if rf, ok := ret.Get(1).(func(*model.User) []byte); ok {
r1 = rf(user)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).([]byte)
}
}
var r2 *model.AppError
if rf, ok := ret.Get(2).(func(*model.User) *model.AppError); ok {
r2 = rf(user)
} else {
if ret.Get(2) != nil {
r2 = ret.Get(2).(*model.AppError)
}
}
return r0, r1, r2
}
// ValidateToken provides a mock function with given fields: secret, token
func (_m *MfaInterface) ValidateToken(secret string, token string) (bool, *model.AppError) {
ret := _m.Called(secret, token)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string) bool); ok {
r0 = rf(secret, token)
} else {
r0 = ret.Get(0).(bool)
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(string, string) *model.AppError); ok {
r1 = rf(secret, token)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// NotificationInterface is an autogenerated mock type for the NotificationInterface type
type NotificationInterface struct {
mock.Mock
}
// CheckLicense provides a mock function with given fields:
func (_m *NotificationInterface) CheckLicense() *model.AppError {
ret := _m.Called()
var r0 *model.AppError
if rf, ok := ret.Get(0).(func() *model.AppError); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// GetNotificationMessage provides a mock function with given fields: ack, userID
func (_m *NotificationInterface) GetNotificationMessage(ack *model.PushNotificationAck, userID string) (*model.PushNotification, *model.AppError) {
ret := _m.Called(ack, userID)
var r0 *model.PushNotification
if rf, ok := ret.Get(0).(func(*model.PushNotificationAck, string) *model.PushNotification); ok {
r0 = rf(ack, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PushNotification)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(*model.PushNotificationAck, string) *model.AppError); ok {
r1 = rf(ack, userID)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
io "io"
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// OAuthProvider is an autogenerated mock type for the OAuthProvider type
type OAuthProvider struct {
mock.Mock
}
// GetSSOSettings provides a mock function with given fields: config, service
func (_m *OAuthProvider) GetSSOSettings(config *model.Config, service string) (*model.SSOSettings, error) {
ret := _m.Called(config, service)
var r0 *model.SSOSettings
if rf, ok := ret.Get(0).(func(*model.Config, string) *model.SSOSettings); ok {
r0 = rf(config, service)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SSOSettings)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Config, string) error); ok {
r1 = rf(config, service)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUserFromIdToken provides a mock function with given fields: idToken
func (_m *OAuthProvider) GetUserFromIdToken(idToken string) (*model.User, error) {
ret := _m.Called(idToken)
var r0 *model.User
if rf, ok := ret.Get(0).(func(string) *model.User); ok {
r0 = rf(idToken)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(idToken)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUserFromJSON provides a mock function with given fields: data, tokenUser
func (_m *OAuthProvider) GetUserFromJSON(data io.Reader, tokenUser *model.User) (*model.User, error) {
ret := _m.Called(data, tokenUser)
var r0 *model.User
if rf, ok := ret.Get(0).(func(io.Reader, *model.User) *model.User); ok {
r0 = rf(data, tokenUser)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(io.Reader, *model.User) error); ok {
r1 = rf(data, tokenUser)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// IsSameUser provides a mock function with given fields: dbUser, oAuthUser
func (_m *OAuthProvider) IsSameUser(dbUser *model.User, oAuthUser *model.User) bool {
ret := _m.Called(dbUser, oAuthUser)
var r0 bool
if rf, ok := ret.Get(0).(func(*model.User, *model.User) bool); ok {
r0 = rf(dbUser, oAuthUser)
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// ResendInvitationEmailJobInterface is an autogenerated mock type for the ResendInvitationEmailJobInterface type
type ResendInvitationEmailJobInterface struct {
mock.Mock
}
// MakeScheduler provides a mock function with given fields:
func (_m *ResendInvitationEmailJobInterface) MakeScheduler() model.Scheduler {
ret := _m.Called()
var r0 model.Scheduler
if rf, ok := ret.Get(0).(func() model.Scheduler); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Scheduler)
}
}
return r0
}
// MakeWorker provides a mock function with given fields:
func (_m *ResendInvitationEmailJobInterface) MakeWorker() model.Worker {
ret := _m.Called()
var r0 model.Worker
if rf, ok := ret.Get(0).(func() model.Worker); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Worker)
}
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make einterfaces-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
request "github.com/mattermost/mattermost-server/v6/server/channels/app/request"
mock "github.com/stretchr/testify/mock"
)
// SamlInterface is an autogenerated mock type for the SamlInterface type
type SamlInterface struct {
mock.Mock
}
// BuildRequest provides a mock function with given fields: relayState
func (_m *SamlInterface) BuildRequest(relayState string) (*model.SamlAuthRequest, *model.AppError) {
ret := _m.Called(relayState)
var r0 *model.SamlAuthRequest
if rf, ok := ret.Get(0).(func(string) *model.SamlAuthRequest); ok {
r0 = rf(relayState)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SamlAuthRequest)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(string) *model.AppError); ok {
r1 = rf(relayState)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// CheckProviderAttributes provides a mock function with given fields: SS, ouser, patch
func (_m *SamlInterface) CheckProviderAttributes(SS *model.SamlSettings, ouser *model.User, patch *model.UserPatch) string {
ret := _m.Called(SS, ouser, patch)
var r0 string
if rf, ok := ret.Get(0).(func(*model.SamlSettings, *model.User, *model.UserPatch) string); ok {
r0 = rf(SS, ouser, patch)
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// ConfigureSP provides a mock function with given fields:
func (_m *SamlInterface) ConfigureSP() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// DoLogin provides a mock function with given fields: c, encodedXML, relayState
func (_m *SamlInterface) DoLogin(c *request.Context, encodedXML string, relayState map[string]string) (*model.User, *model.AppError) {
ret := _m.Called(c, encodedXML, relayState)
var r0 *model.User
if rf, ok := ret.Get(0).(func(*request.Context, string, map[string]string) *model.User); ok {
r0 = rf(c, encodedXML, relayState)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.User)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(*request.Context, string, map[string]string) *model.AppError); ok {
r1 = rf(c, encodedXML, relayState)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetMetadata provides a mock function with given fields:
func (_m *SamlInterface) GetMetadata() (string, *model.AppError) {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func() *model.AppError); ok {
r1 = rf()
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package einterfaces
import (
"io"
"github.com/mattermost/mattermost-server/v6/model"
)
type OAuthProvider interface {
GetUserFromJSON(data io.Reader, tokenUser *model.User) (*model.User, error)
GetSSOSettings(config *model.Config, service string) (*model.SSOSettings, error)
GetUserFromIdToken(idToken string) (*model.User, error)
IsSameUser(dbUser, oAuthUser *model.User) bool
}
var oauthProviders = make(map[string]OAuthProvider)
func RegisterOAuthProvider(name string, newProvider OAuthProvider) {
oauthProviders[name] = newProvider
}
func GetOAuthProvider(name string) OAuthProvider {
provider, ok := oauthProviders[name]
if ok {
return provider
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package active_users
import (
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
const schedFreq = 10 * time.Minute
func MakeScheduler(jobServer *jobs.JobServer) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
return *cfg.MetricsSettings.Enable
}
return jobs.NewPeriodicScheduler(jobServer, model.JobTypeActiveUsers, schedFreq, isEnabled)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package active_users
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const (
JobName = "ActiveUsers"
)
func MakeWorker(jobServer *jobs.JobServer, store store.Store, getMetrics func() einterfaces.MetricsInterface) model.Worker {
isEnabled := func(cfg *model.Config) bool {
return *cfg.MetricsSettings.Enable
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
count, err := store.User().Count(model.UserCountOptions{IncludeDeleted: false})
if err != nil {
return err
}
if getMetrics() != nil {
getMetrics().ObserveEnabledUsers(count)
}
return nil
}
worker := jobs.NewSimpleWorker(JobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package jobs
import (
"crypto/rand"
"math/big"
"time"
"github.com/mattermost/mattermost-server/v6/model"
)
type PeriodicScheduler struct {
jobs *JobServer
period time.Duration
jobType string
enabledFunc func(cfg *model.Config) bool
}
func NewPeriodicScheduler(jobs *JobServer, jobType string, period time.Duration, enabledFunc func(cfg *model.Config) bool) *PeriodicScheduler {
return &PeriodicScheduler{
period: period,
jobType: jobType,
enabledFunc: enabledFunc,
jobs: jobs,
}
}
func (scheduler *PeriodicScheduler) Enabled(cfg *model.Config) bool {
return scheduler.enabledFunc(cfg)
}
func (scheduler *PeriodicScheduler) NextScheduleTime(_ *model.Config, _ time.Time /* pendingJobs */, _ bool /* lastSuccessfulJob */, _ *model.Job) *time.Time {
nextTime := time.Now().Add(getRandomDelay(jitterRange)).Add(scheduler.period)
return &nextTime
}
func (scheduler *PeriodicScheduler) ScheduleJob(_ *model.Config /* pendingJobs */, _ bool /* lastSuccessfulJob */, _ *model.Job) (*model.Job, *model.AppError) {
return scheduler.jobs.CreateJob(scheduler.jobType, nil)
}
type DailyScheduler struct {
jobs *JobServer
startTimeFunc func(cfg *model.Config) *time.Time
jobType string
enabledFunc func(cfg *model.Config) bool
}
func NewDailyScheduler(jobs *JobServer, jobType string, startTimeFunc func(cfg *model.Config) *time.Time, enabledFunc func(cfg *model.Config) bool) *DailyScheduler {
return &DailyScheduler{
startTimeFunc: startTimeFunc,
jobType: jobType,
enabledFunc: enabledFunc,
jobs: jobs,
}
}
func (scheduler *DailyScheduler) Enabled(cfg *model.Config) bool {
return scheduler.enabledFunc(cfg)
}
func (scheduler *DailyScheduler) NextScheduleTime(cfg *model.Config, now time.Time /* pendingJobs */, _ bool /* lastSuccessfulJob */, _ *model.Job) *time.Time {
scheduledTime := scheduler.startTimeFunc(cfg)
if scheduledTime == nil {
return nil
}
return GenerateNextStartDateTime(now, *scheduledTime)
}
func (scheduler *DailyScheduler) ScheduleJob(_ *model.Config /* pendingJobs */, _ bool /* lastSuccessfulJob */, _ *model.Job) (*model.Job, *model.AppError) {
return scheduler.jobs.CreateJob(scheduler.jobType, nil)
}
const jitterRange = 2000 // milliseconds
func getRandomDelay(limit int64) time.Duration {
num, err := rand.Int(rand.Reader, big.NewInt(limit))
if err != nil {
return time.Millisecond
}
return time.Millisecond * time.Duration(num.Int64())
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package jobs
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SimpleWorker struct {
name string
stop chan bool
stopped chan bool
jobs chan model.Job
jobServer *JobServer
execute func(job *model.Job) error
isEnabled func(cfg *model.Config) bool
}
func NewSimpleWorker(name string, jobServer *JobServer, execute func(job *model.Job) error, isEnabled func(cfg *model.Config) bool) *SimpleWorker {
worker := SimpleWorker{
name: name,
stop: make(chan bool, 1),
stopped: make(chan bool, 1),
jobs: make(chan model.Job),
jobServer: jobServer,
execute: execute,
isEnabled: isEnabled,
}
return &worker
}
func (worker *SimpleWorker) Run() {
mlog.Debug("Worker started", mlog.String("worker", worker.name))
defer func() {
mlog.Debug("Worker finished", mlog.String("worker", worker.name))
worker.stopped <- true
}()
for {
select {
case <-worker.stop:
mlog.Debug("Worker received stop signal", mlog.String("worker", worker.name))
return
case job := <-worker.jobs:
mlog.Debug("Worker received a new candidate job.", mlog.String("worker", worker.name))
worker.DoJob(&job)
}
}
}
func (worker *SimpleWorker) Stop() {
mlog.Debug("Worker stopping", mlog.String("worker", worker.name))
worker.stop <- true
<-worker.stopped
}
func (worker *SimpleWorker) JobChannel() chan<- model.Job {
return worker.jobs
}
func (worker *SimpleWorker) IsEnabled(cfg *model.Config) bool {
return worker.isEnabled(cfg)
}
func (worker *SimpleWorker) DoJob(job *model.Job) {
if claimed, err := worker.jobServer.ClaimJob(job); err != nil {
mlog.Warn("SimpleWorker experienced an error while trying to claim job",
mlog.String("worker", worker.name),
mlog.String("job_id", job.Id),
mlog.Err(err))
return
} else if !claimed {
return
}
var appErr *model.AppError
// We get the job again because ClaimJob changes the job status.
job, appErr = worker.jobServer.GetJob(job.Id)
if appErr != nil {
mlog.Error("SimpleWorker: job execution error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(appErr))
worker.setJobError(job, appErr)
}
err := worker.execute(job)
if err != nil {
mlog.Error("SimpleWorker: job execution error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
worker.setJobError(job, model.NewAppError("DoJob", "app.job.error", nil, "", http.StatusInternalServerError).Wrap(err))
return
}
mlog.Info("SimpleWorker: Job is complete", mlog.String("worker", worker.name), mlog.String("job_id", job.Id))
worker.setJobSuccess(job)
}
func (worker *SimpleWorker) setJobSuccess(job *model.Job) {
if err := worker.jobServer.SetJobProgress(job, 100); err != nil {
mlog.Error("Worker: Failed to update progress for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
worker.setJobError(job, err)
}
if err := worker.jobServer.SetJobSuccess(job); err != nil {
mlog.Error("SimpleWorker: Failed to set success for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
worker.setJobError(job, err)
}
}
func (worker *SimpleWorker) setJobError(job *model.Job, appError *model.AppError) {
if err := worker.jobServer.SetJobError(job, appError); err != nil {
mlog.Error("SimpleWorker: Failed to set job error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package expirynotify
import (
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
const schedFreq = 10 * time.Minute
func MakeScheduler(jobServer *jobs.JobServer) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
return *cfg.ServiceSettings.ExtendSessionLengthWithActivity
}
return jobs.NewPeriodicScheduler(jobServer, model.JobTypeExpiryNotify, schedFreq, isEnabled)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package expirynotify
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
const (
JobName = "ExpiryNotify"
)
func MakeWorker(jobServer *jobs.JobServer, notifySessionsExpired func() error) model.Worker {
isEnabled := func(cfg *model.Config) bool {
return *cfg.ServiceSettings.ExtendSessionLengthWithActivity
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
return notifySessionsExpired()
}
return jobs.NewSimpleWorker(JobName, jobServer, execute, isEnabled)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package export_delete
import (
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
const schedFreq = 24 * time.Hour
func MakeScheduler(jobServer *jobs.JobServer) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
return *cfg.ExportSettings.Directory != "" && *cfg.ExportSettings.RetentionDays > 0
}
return jobs.NewPeriodicScheduler(jobServer, model.JobTypeExportDelete, schedFreq, isEnabled)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package export_delete
import (
"path/filepath"
"time"
"github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/platform/services/configservice"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const jobName = "ExportDelete"
type AppIface interface {
configservice.ConfigService
ListDirectory(path string) ([]string, *model.AppError)
FileModTime(path string) (time.Time, *model.AppError)
RemoveFile(path string) *model.AppError
}
func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker {
isEnabled := func(cfg *model.Config) bool {
return *cfg.ExportSettings.Directory != "" && *cfg.ExportSettings.RetentionDays > 0
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
exportPath := *app.Config().ExportSettings.Directory
retentionTime := time.Duration(*app.Config().ExportSettings.RetentionDays) * 24 * time.Hour
exports, appErr := app.ListDirectory(exportPath)
if appErr != nil {
return appErr
}
errors := merror.New()
for i := range exports {
filename := filepath.Base(exports[i])
modTime, appErr := app.FileModTime(filepath.Join(exportPath, filename))
if appErr != nil {
mlog.Debug("Worker: Failed to get file modification time",
mlog.Err(appErr), mlog.String("export", exports[i]))
errors.Append(appErr)
continue
}
if time.Now().After(modTime.Add(retentionTime)) {
// remove file data from storage.
if appErr := app.RemoveFile(exports[i]); appErr != nil {
mlog.Debug("Worker: Failed to remove file",
mlog.Err(appErr), mlog.String("export", exports[i]))
errors.Append(appErr)
continue
}
}
}
if err := errors.ErrorOrNil(); err != nil {
mlog.Warn("Worker: errors occurred", mlog.String("job-name", jobName), mlog.Err(err))
}
return nil
}
worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package export_process
import (
"context"
"io"
"path/filepath"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/platform/services/configservice"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const jobName = "ExportProcess"
type AppIface interface {
configservice.ConfigService
WriteFile(fr io.Reader, path string) (int64, *model.AppError)
WriteFileContext(ctx context.Context, fr io.Reader, path string) (int64, *model.AppError)
BulkExport(ctx request.CTX, writer io.Writer, outPath string, job *model.Job, opts model.BulkExportOpts) *model.AppError
Log() *mlog.Logger
}
func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker {
isEnabled := func(cfg *model.Config) bool { return true }
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
opts := model.BulkExportOpts{
CreateArchive: true,
}
includeAttachments, ok := job.Data["include_attachments"]
if ok && includeAttachments == "true" {
opts.IncludeAttachments = true
}
outPath := *app.Config().ExportSettings.Directory
exportFilename := job.Id + "_export.zip"
rd, wr := io.Pipe()
go func() {
_, appErr := app.WriteFileContext(context.Background(), rd, filepath.Join(outPath, exportFilename))
if appErr != nil {
// we close the reader here to prevent a deadlock when the bulk exporter tries to
// write into the pipe while app.WriteFile has already returned. The error will be
// returned by the writer part of the pipe when app.BulkExport tries to call
// wr.Write() on it.
rd.CloseWithError(appErr) // CloseWithError never returns an error
}
}()
logger := app.Log().With(mlog.String("job_id", job.Id))
appErr := app.BulkExport(request.EmptyContext(logger), wr, outPath, job, opts)
wr.Close() // Close never returns an error
if appErr != nil {
return appErr
}
return nil
}
worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package extract_content
import (
"strconv"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var ignoredFiles = map[string]bool{
"png": true, "jpg": true, "jpeg": true, "gif": true, "wmv": true,
"mpg": true, "mpeg": true, "mp3": true, "mp4": true, "ogg": true,
"ogv": true, "mov": true, "apk": true, "svg": true, "webm": true,
"mkv": true,
}
const jobName = "ExtractContent"
type AppIface interface {
ExtractContentFromFileInfo(fileInfo *model.FileInfo) error
}
func MakeWorker(jobServer *jobs.JobServer, app AppIface, store store.Store) model.Worker {
isEnabled := func(cfg *model.Config) bool {
return true
}
execute := func(job *model.Job) error {
jobServer.HandleJobPanic(job)
var err error
var fromTS int64 = 0
var toTS int64 = model.GetMillis()
if fromStr, ok := job.Data["from"]; ok {
if fromTS, err = strconv.ParseInt(fromStr, 10, 64); err != nil {
return err
}
fromTS *= 1000
}
if toStr, ok := job.Data["to"]; ok {
if toTS, err = strconv.ParseInt(toStr, 10, 64); err != nil {
return err
}
toTS *= 1000
}
var nFiles int
var nErrs int
for {
opts := model.GetFileInfosOptions{
Since: fromTS,
SortBy: model.FileinfoSortByCreated,
IncludeDeleted: false,
}
fileInfos, err := store.FileInfo().GetWithOptions(0, 1000, &opts)
if err != nil {
return err
}
if len(fileInfos) == 0 {
break
}
for _, fileInfo := range fileInfos {
if !ignoredFiles[fileInfo.Extension] {
mlog.Debug("extracting file", mlog.String("filename", fileInfo.Name), mlog.String("filepath", fileInfo.Path))
err = app.ExtractContentFromFileInfo(fileInfo)
if err != nil {
mlog.Warn("Failed to extract file content", mlog.Err(err), mlog.String("file_info_id", fileInfo.Id))
nErrs++
}
nFiles++
}
}
lastFileInfo := fileInfos[len(fileInfos)-1]
if lastFileInfo.CreateAt > toTS {
break
}
fromTS = lastFileInfo.CreateAt + 1
}
job.Data["errors"] = strconv.Itoa(nErrs)
job.Data["processed"] = strconv.Itoa(nFiles)
if err := jobServer.UpdateInProgressJobData(job); err != nil {
mlog.Error("Worker: Failed to update job data", mlog.String("worker", model.JobTypeExtractContent), mlog.String("job_id", job.Id), mlog.Err(err))
}
return nil
}
worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package hosted_purchase_screening
import (
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
const schedFreq = 24 * time.Hour
func MakeScheduler(jobServer *jobs.JobServer, license *model.License) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
return model.BuildEnterpriseReady == "true" && license == nil
}
return jobs.NewPeriodicScheduler(jobServer, model.JobTypeHostedPurchaseScreening, schedFreq, isEnabled)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package hosted_purchase_screening
import (
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
const (
JobName = "HostedPurchaseScreening"
// 3 days matches the expecation given in portal purchase flow.
waitForScreeningDuration = 3 * 24 * time.Hour
)
type ScreenTimeStore interface {
GetByName(string) (*model.System, error)
PermanentDeleteByName(name string) (*model.System, error)
}
func MakeWorker(jobServer *jobs.JobServer, license *model.License, screenTimeStore ScreenTimeStore) model.Worker {
isEnabled := func(_ *model.Config) bool {
return !license.IsCloud()
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
now := time.Now()
screenTimeValue, err := screenTimeStore.GetByName(model.SystemHostedPurchaseNeedsScreening)
if err != nil {
return err
}
screenTime, err := strconv.ParseInt(screenTimeValue.Value, 10, 64)
if err != nil {
return err
}
if now.After(time.UnixMilli(screenTime).Add(waitForScreeningDuration)) {
screenTimeStore.PermanentDeleteByName(model.SystemHostedPurchaseNeedsScreening)
}
return nil
}
worker := jobs.NewSimpleWorker(JobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package import_delete
import (
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
const schedFreq = 24 * time.Hour
func MakeScheduler(jobServer *jobs.JobServer) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
return *cfg.ImportSettings.Directory != "" && *cfg.ImportSettings.RetentionDays > 0
}
return jobs.NewPeriodicScheduler(jobServer, model.JobTypeImportDelete, schedFreq, isEnabled)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package import_delete
import (
"errors"
"path/filepath"
"time"
"github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/configservice"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const jobName = "ImportDelete"
type AppIface interface {
configservice.ConfigService
ListDirectory(path string) ([]string, *model.AppError)
FileModTime(path string) (time.Time, *model.AppError)
RemoveFile(path string) *model.AppError
}
func MakeWorker(jobServer *jobs.JobServer, app AppIface, s store.Store) model.Worker {
isEnabled := func(cfg *model.Config) bool {
return *cfg.ImportSettings.Directory != "" && *cfg.ImportSettings.RetentionDays > 0
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
importPath := *app.Config().ImportSettings.Directory
retentionTime := time.Duration(*app.Config().ImportSettings.RetentionDays) * 24 * time.Hour
imports, appErr := app.ListDirectory(importPath)
if appErr != nil {
return appErr
}
multipleErrors := merror.New()
for i := range imports {
filename := filepath.Base(imports[i])
modTime, appErr := app.FileModTime(filepath.Join(importPath, filename))
if appErr != nil {
mlog.Debug("Worker: Failed to get file modification time",
mlog.Err(appErr), mlog.String("import", imports[i]))
multipleErrors.Append(appErr)
continue
}
if time.Now().After(modTime.Add(retentionTime)) {
// expected format if uploaded through the API is
// ${uploadID}_${filename}${model.IncompleteUploadSuffix}
minLen := 26 + 1 + len(model.IncompleteUploadSuffix)
// check if it's an incomplete upload and attempt to delete its session.
if len(filename) > minLen && filepath.Ext(filename) == model.IncompleteUploadSuffix {
uploadID := filename[:26]
if storeErr := s.UploadSession().Delete(uploadID); storeErr != nil {
mlog.Debug("Worker: Failed to delete UploadSession",
mlog.Err(storeErr), mlog.String("upload_id", uploadID))
multipleErrors.Append(storeErr)
continue
}
} else {
// check if fileinfo exists and if so delete it.
filePath := filepath.Join(imports[i])
info, storeErr := s.FileInfo().GetByPath(filePath)
var nfErr *store.ErrNotFound
if storeErr != nil && !errors.As(storeErr, &nfErr) {
mlog.Debug("Worker: Failed to get FileInfo",
mlog.Err(storeErr), mlog.String("path", filePath))
multipleErrors.Append(storeErr)
continue
} else if storeErr == nil {
if storeErr = s.FileInfo().PermanentDelete(info.Id); storeErr != nil {
mlog.Debug("Worker: Failed to delete FileInfo",
mlog.Err(storeErr), mlog.String("file_id", info.Id))
multipleErrors.Append(storeErr)
continue
}
}
}
// remove file data from storage.
if appErr := app.RemoveFile(imports[i]); appErr != nil {
mlog.Debug("Worker: Failed to remove file",
mlog.Err(appErr), mlog.String("import", imports[i]))
multipleErrors.Append(appErr)
continue
}
}
}
if err := multipleErrors.ErrorOrNil(); err != nil {
mlog.Warn("Worker: errors occurred", mlog.String("job-name", jobName), mlog.Err(err))
}
return nil
}
worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package import_process
import (
"archive/zip"
"io"
"net/http"
"path/filepath"
"runtime"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/platform/services/configservice"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const jobName = "ImportProcess"
type AppIface interface {
configservice.ConfigService
RemoveFile(path string) *model.AppError
FileExists(path string) (bool, *model.AppError)
FileSize(path string) (int64, *model.AppError)
FileReader(path string) (filestore.ReadCloseSeeker, *model.AppError)
BulkImportWithPath(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int, importPath string) (*model.AppError, int)
Log() *mlog.Logger
}
func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker {
appContext := request.EmptyContext(app.Log())
isEnabled := func(cfg *model.Config) bool {
return true
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
importFileName, ok := job.Data["import_file"]
if !ok {
return model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.missing_file", nil, "", http.StatusBadRequest)
}
importFilePath := filepath.Join(*app.Config().ImportSettings.Directory, importFileName)
if ok, err := app.FileExists(importFilePath); err != nil {
return err
} else if !ok {
return model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.file_exists", nil, "", http.StatusBadRequest)
}
importFileSize, appErr := app.FileSize(importFilePath)
if appErr != nil {
return appErr
}
importFile, appErr := app.FileReader(importFilePath)
if appErr != nil {
return appErr
}
defer importFile.Close()
// The import is a long running operation, try to cancel any timeouts attached to the reader.
type TimeoutCanceler interface{ CancelTimeout() bool }
if tc, ok := importFile.(TimeoutCanceler); ok {
if !tc.CancelTimeout() {
appContext.Logger().Warn("Could not cancel the timeout for the file reader. The import may fail due to a timeout.")
}
}
importZipReader, err := zip.NewReader(importFile.(io.ReaderAt), importFileSize)
if err != nil {
return model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.open_file", nil, "", http.StatusInternalServerError).Wrap(err)
}
// find JSONL import file.
var jsonFile io.ReadCloser
for _, f := range importZipReader.File {
if filepath.Ext(f.Name) != ".jsonl" {
continue
}
// avoid "zip slip"
if strings.Contains(f.Name, "..") {
return model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.open_file", nil, "jsonFilePath contains path traversal", http.StatusForbidden)
}
jsonFile, err = f.Open()
if err != nil {
return model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.open_file", nil, "", http.StatusInternalServerError).Wrap(err)
}
defer jsonFile.Close()
break
}
if jsonFile == nil {
return model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.missing_jsonl", nil, "jsonFile was nil", http.StatusBadRequest)
}
// do the actual import.
appErr, lineNumber := app.BulkImportWithPath(appContext, jsonFile, importZipReader, false, runtime.NumCPU(), model.ExportDataDir)
if appErr != nil {
job.Data["line_number"] = strconv.Itoa(lineNumber)
return appErr
}
// remove import file when done.
if appErr := app.RemoveFile(importFilePath); appErr != nil {
return appErr
}
return nil
}
worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package jobs
import (
"context"
"errors"
"fmt"
"net/http"
"runtime/pprof"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
CancelWatcherPollingInterval = 5000
)
func (srv *JobServer) CreateJob(jobType string, jobData map[string]string) (*model.Job, *model.AppError) {
job := model.Job{
Id: model.NewId(),
Type: jobType,
CreateAt: model.GetMillis(),
Status: model.JobStatusPending,
Data: jobData,
}
if err := job.IsValid(); err != nil {
return nil, err
}
if srv.workers.Get(job.Type) == nil {
return nil, model.NewAppError("Job.IsValid", "model.job.is_valid.type.app_error", nil, "id="+job.Id, http.StatusBadRequest)
}
if _, err := srv.Store.Job().Save(&job); err != nil {
return nil, model.NewAppError("CreateJob", "app.job.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &job, nil
}
func (srv *JobServer) GetJob(id string) (*model.Job, *model.AppError) {
job, err := srv.Store.Job().Get(id)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetJob", "app.job.get.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetJob", "app.job.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return job, nil
}
func (srv *JobServer) ClaimJob(job *model.Job) (bool, *model.AppError) {
updated, err := srv.Store.Job().UpdateStatusOptimistically(job.Id, model.JobStatusPending, model.JobStatusInProgress)
if err != nil {
return false, model.NewAppError("ClaimJob", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if updated && srv.metrics != nil {
srv.metrics.IncrementJobActive(job.Type)
}
return updated, nil
}
func (srv *JobServer) SetJobProgress(job *model.Job, progress int64) *model.AppError {
job.Status = model.JobStatusInProgress
job.Progress = progress
if _, err := srv.Store.Job().UpdateOptimistically(job, model.JobStatusInProgress); err != nil {
return model.NewAppError("SetJobProgress", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (srv *JobServer) SetJobWarning(job *model.Job) *model.AppError {
if _, err := srv.Store.Job().UpdateStatus(job.Id, model.JobStatusWarning); err != nil {
return model.NewAppError("SetJobWarning", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (srv *JobServer) SetJobSuccess(job *model.Job) *model.AppError {
if _, err := srv.Store.Job().UpdateStatus(job.Id, model.JobStatusSuccess); err != nil {
return model.NewAppError("SetJobSuccess", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if srv.metrics != nil {
srv.metrics.DecrementJobActive(job.Type)
}
return nil
}
func (srv *JobServer) SetJobError(job *model.Job, jobError *model.AppError) *model.AppError {
if jobError == nil {
_, err := srv.Store.Job().UpdateStatus(job.Id, model.JobStatusError)
if err != nil {
return model.NewAppError("SetJobError", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if srv.metrics != nil {
srv.metrics.DecrementJobActive(job.Type)
}
return nil
}
job.Status = model.JobStatusError
job.Progress = -1
if job.Data == nil {
job.Data = make(map[string]string)
}
job.Data["error"] = jobError.Message
if jobError.DetailedError != "" {
job.Data["error"] += " — " + jobError.DetailedError
}
if wrapped := jobError.Unwrap(); wrapped != nil {
job.Data["error"] += " — " + wrapped.Error()
}
updated, err := srv.Store.Job().UpdateOptimistically(job, model.JobStatusInProgress)
if err != nil {
return model.NewAppError("SetJobError", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if updated && srv.metrics != nil {
srv.metrics.DecrementJobActive(job.Type)
}
if !updated {
updated, err = srv.Store.Job().UpdateOptimistically(job, model.JobStatusCancelRequested)
if err != nil {
return model.NewAppError("SetJobError", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if !updated {
return model.NewAppError("SetJobError", "jobs.set_job_error.update.error", nil, "id="+job.Id, http.StatusInternalServerError)
}
}
return nil
}
func (srv *JobServer) SetJobCanceled(job *model.Job) *model.AppError {
if _, err := srv.Store.Job().UpdateStatus(job.Id, model.JobStatusCanceled); err != nil {
return model.NewAppError("SetJobCanceled", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if srv.metrics != nil {
srv.metrics.DecrementJobActive(job.Type)
}
return nil
}
func (srv *JobServer) SetJobPending(job *model.Job) *model.AppError {
if _, err := srv.Store.Job().UpdateStatus(job.Id, model.JobStatusPending); err != nil {
return model.NewAppError("SetJobPending", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if srv.metrics != nil {
srv.metrics.DecrementJobActive(job.Type)
}
return nil
}
func (srv *JobServer) UpdateInProgressJobData(job *model.Job) *model.AppError {
job.Status = model.JobStatusInProgress
job.LastActivityAt = model.GetMillis()
if _, err := srv.Store.Job().UpdateOptimistically(job, model.JobStatusInProgress); err != nil {
return model.NewAppError("UpdateInProgressJobData", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
// HandleJobPanic is used to handle panics during the execution of a job. It logs the panic and sets the status for the job.
// After handling, the method repanics! This method is supposed to be `defer`'d at the start of the job.
func (srv *JobServer) HandleJobPanic(job *model.Job) {
r := recover()
if r == nil {
return
}
sb := &strings.Builder{}
pprof.Lookup("goroutine").WriteTo(sb, 2)
mlog.Error("Unhandled panic in job", mlog.Any("panic", r), mlog.Any("job", job), mlog.String("stack", sb.String()))
rerr, ok := r.(error)
if !ok {
rerr = fmt.Errorf("job panic: %v", r)
}
appErr := srv.SetJobError(job, model.NewAppError("HandleJobPanic", "app.job.update.app_error", nil, "", http.StatusInternalServerError)).Wrap(rerr)
if appErr != nil {
mlog.Error("Failed to set the job status to 'failed'", mlog.Err(appErr), mlog.Any("job", job))
}
panic(r)
}
func (srv *JobServer) RequestCancellation(jobId string) *model.AppError {
updated, err := srv.Store.Job().UpdateStatusOptimistically(jobId, model.JobStatusPending, model.JobStatusCanceled)
if err != nil {
return model.NewAppError("RequestCancellation", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if updated {
if srv.metrics != nil {
job, err := srv.GetJob(jobId)
if err != nil {
return model.NewAppError("RequestCancellation", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
srv.metrics.DecrementJobActive(job.Type)
}
return nil
}
updated, err = srv.Store.Job().UpdateStatusOptimistically(jobId, model.JobStatusInProgress, model.JobStatusCancelRequested)
if err != nil {
return model.NewAppError("RequestCancellation", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if updated {
return nil
}
return model.NewAppError("RequestCancellation", "jobs.request_cancellation.status.error", nil, "id="+jobId, http.StatusInternalServerError)
}
func (srv *JobServer) CancellationWatcher(ctx context.Context, jobId string, cancelChan chan struct{}) {
for {
select {
case <-ctx.Done():
mlog.Debug("CancellationWatcher for Job Aborting as job has finished.", mlog.String("job_id", jobId))
return
case <-time.After(CancelWatcherPollingInterval * time.Millisecond):
mlog.Debug("CancellationWatcher for Job started polling.", mlog.String("job_id", jobId))
jobStatus, err := srv.Store.Job().Get(jobId)
if err != nil {
mlog.Warn("Error getting job", mlog.String("job_id", jobId), mlog.Err(err))
continue
}
if jobStatus.Status == model.JobStatusCancelRequested {
close(cancelChan)
return
}
}
}
}
func GenerateNextStartDateTime(now time.Time, nextStartTime time.Time) *time.Time {
nextTime := time.Date(now.Year(), now.Month(), now.Day(), nextStartTime.Hour(), nextStartTime.Minute(), 0, 0, time.Local)
if !now.Before(nextTime) {
nextTime = nextTime.AddDate(0, 0, 1)
}
return &nextTime
}
func (srv *JobServer) CheckForPendingJobsByType(jobType string) (bool, *model.AppError) {
count, err := srv.Store.Job().GetCountByStatusAndType(model.JobStatusPending, jobType)
if err != nil {
return false, model.NewAppError("CheckForPendingJobsByType", "app.job.get_count_by_status_and_type.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return count > 0, nil
}
func (srv *JobServer) GetJobsByTypeAndStatus(jobType string, status string) ([]*model.Job, *model.AppError) {
jobs, err := srv.Store.Job().GetAllByTypeAndStatus(jobType, status)
if err != nil {
return nil, model.NewAppError("GetJobsByTypeAndStatus", "app.job.get_all_jobs_by_type_and_status.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return jobs, nil
}
func (srv *JobServer) GetLastSuccessfulJobByType(jobType string) (*model.Job, *model.AppError) {
statuses := []string{model.JobStatusSuccess}
if jobType == model.JobTypeMessageExport {
statuses = []string{model.JobStatusWarning, model.JobStatusSuccess}
}
job, err := srv.Store.Job().GetNewestJobByStatusesAndType(statuses, jobType)
var nfErr *store.ErrNotFound
if err != nil && !errors.As(err, &nfErr) {
return nil, model.NewAppError("GetLastSuccessfulJobByType", "app.job.get_newest_job_by_status_and_type.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return job, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package jobs
import (
"math/rand"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// Default polling interval for jobs termination.
// (Defining as `var` rather than `const` allows tests to lower the interval.)
var DefaultWatcherPollingInterval = 15000
type Watcher struct {
srv *JobServer
workers *Workers
stop chan struct{}
stopped chan struct{}
pollingInterval int
}
func (srv *JobServer) MakeWatcher(workers *Workers, pollingInterval int) *Watcher {
return &Watcher{
stop: make(chan struct{}),
stopped: make(chan struct{}),
pollingInterval: pollingInterval,
workers: workers,
srv: srv,
}
}
func (watcher *Watcher) Start() {
mlog.Debug("Watcher Started")
// Delay for some random number of milliseconds before starting to ensure that multiple
// instances of the jobserver don't poll at a time too close to each other.
rand.Seed(time.Now().UTC().UnixNano())
<-time.After(time.Duration(rand.Intn(watcher.pollingInterval)) * time.Millisecond)
defer func() {
mlog.Debug("Watcher Finished")
close(watcher.stopped)
}()
for {
select {
case <-watcher.stop:
mlog.Debug("Watcher: Received stop signal")
return
case <-time.After(time.Duration(watcher.pollingInterval) * time.Millisecond):
watcher.PollAndNotify()
}
}
}
func (watcher *Watcher) Stop() {
mlog.Debug("Watcher Stopping")
close(watcher.stop)
<-watcher.stopped
watcher.stop = make(chan struct{})
watcher.stopped = make(chan struct{})
}
func (watcher *Watcher) PollAndNotify() {
jobs, err := watcher.srv.Store.Job().GetAllByStatus(model.JobStatusPending)
if err != nil {
mlog.Error("Error occurred getting all pending statuses.", mlog.Err(err))
return
}
for _, job := range jobs {
worker := watcher.workers.Get(job.Type)
if worker != nil {
select {
case worker.JobChannel() <- *job:
default:
}
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package last_accessible_file
import (
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const schedFreq = 2 * time.Hour
func MakeScheduler(jobServer *jobs.JobServer, license *model.License) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
enabled := license != nil && *license.Features.Cloud
mlog.Debug("Scheduler: isEnabled: "+strconv.FormatBool(enabled), mlog.String("scheduler", model.JobTypeLastAccessibleFile))
return enabled
}
return jobs.NewPeriodicScheduler(jobServer, model.JobTypeLastAccessibleFile, schedFreq, isEnabled)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package last_accessible_file
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
const (
JobName = "LastAccessibleFile"
)
type AppIface interface {
ComputeLastAccessibleFileTime() error
}
func MakeWorker(jobServer *jobs.JobServer, license *model.License, app AppIface) model.Worker {
isEnabled := func(_ *model.Config) bool {
return license != nil && *license.Features.Cloud
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
return app.ComputeLastAccessibleFileTime()
}
worker := jobs.NewSimpleWorker(JobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package last_accessible_post
import (
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const schedFreq = 30 * time.Minute
func MakeScheduler(jobServer *jobs.JobServer, license *model.License) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
enabled := license != nil && *license.Features.Cloud
mlog.Debug("Scheduler: isEnabled: "+strconv.FormatBool(enabled), mlog.String("scheduler", model.JobTypeLastAccessiblePost))
return enabled
}
return jobs.NewPeriodicScheduler(jobServer, model.JobTypeLastAccessiblePost, schedFreq, isEnabled)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package last_accessible_post
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
const (
JobName = "LastAccessiblePost"
)
type AppIface interface {
ComputeLastAccessiblePostTime() error
}
func MakeWorker(jobServer *jobs.JobServer, license *model.License, app AppIface) model.Worker {
isEnabled := func(_ *model.Config) bool {
return license != nil && license.Features != nil && *license.Features.Cloud
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
return app.ComputeLastAccessiblePostTime()
}
worker := jobs.NewSimpleWorker(JobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package migrations
import (
"encoding/json"
"io"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type AdvancedPermissionsPhase2Progress struct {
CurrentTable string `json:"current_table"`
LastTeamId string `json:"last_team_id"`
LastChannelId string `json:"last_channel_id"`
LastUserId string `json:"last_user"`
}
func (p *AdvancedPermissionsPhase2Progress) ToJSON() string {
b, _ := json.Marshal(p)
return string(b)
}
func AdvancedPermissionsPhase2ProgressFromJSON(data io.Reader) *AdvancedPermissionsPhase2Progress {
var o *AdvancedPermissionsPhase2Progress
err := json.NewDecoder(data).Decode(&o)
if err != nil {
mlog.Warn("Error decoding advanced permissions phase 2 progress", mlog.Err(err))
}
return o
}
func (p *AdvancedPermissionsPhase2Progress) IsValid() bool {
if !model.IsValidId(p.LastChannelId) {
return false
}
if !model.IsValidId(p.LastTeamId) {
return false
}
if !model.IsValidId(p.LastUserId) {
return false
}
switch p.CurrentTable {
case "TeamMembers":
case "ChannelMembers":
default:
return false
}
return true
}
func (worker *Worker) runAdvancedPermissionsPhase2Migration(lastDone string) (bool, string, *model.AppError) {
var progress *AdvancedPermissionsPhase2Progress
if lastDone == "" {
// Haven't started the migration yet.
progress = &AdvancedPermissionsPhase2Progress{
CurrentTable: "TeamMembers",
LastChannelId: strings.Repeat("0", 26),
LastTeamId: strings.Repeat("0", 26),
LastUserId: strings.Repeat("0", 26),
}
} else {
err := json.NewDecoder(strings.NewReader(lastDone)).Decode(&progress)
if err != nil {
return false, "", model.NewAppError("MigrationsWorker.runAdvancedPermissionsPhase2Migration", "migrations.worker.run_advanced_permissions_phase_2_migration.invalid_progress", map[string]any{"lastDone": lastDone}, "", http.StatusInternalServerError).Wrap(err)
}
if !progress.IsValid() {
return false, "", model.NewAppError("MigrationsWorker.runAdvancedPermissionsPhase2Migration", "migrations.worker.run_advanced_permissions_phase_2_migration.invalid_progress", map[string]any{"progress": progress.ToJSON()}, "", http.StatusInternalServerError)
}
}
if progress.CurrentTable == "TeamMembers" {
// Run a TeamMembers migration batch.
result, err := worker.store.Team().MigrateTeamMembers(progress.LastTeamId, progress.LastUserId)
if err != nil {
return false, progress.ToJSON(), model.NewAppError("MigrationsWorker.runAdvancedPermissionsPhase2Migration", "app.team.migrate_team_members.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if result == nil {
// We haven't progressed. That means that we've reached the end of this stage of the migration, and should now advance to the next stage.
progress.LastUserId = strings.Repeat("0", 26)
progress.CurrentTable = "ChannelMembers"
return false, progress.ToJSON(), nil
}
progress.LastTeamId = result["TeamId"]
progress.LastUserId = result["UserId"]
} else if progress.CurrentTable == "ChannelMembers" {
// Run a ChannelMembers migration batch.
data, err := worker.store.Channel().MigrateChannelMembers(progress.LastChannelId, progress.LastUserId)
if err != nil {
return false, progress.ToJSON(), model.NewAppError("MigrationsWorker.runAdvancedPermissionsPhase2Migration", "app.channel.migrate_channel_members.select.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if data == nil {
// We haven't progressed. That means we've reached the end of this final stage of the migration.
return true, progress.ToJSON(), nil
}
progress.LastChannelId = data["ChannelId"]
progress.LastUserId = data["UserId"]
}
return false, progress.ToJSON(), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package migrations
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const (
MigrationStateUnscheduled = "unscheduled"
MigrationStateInProgress = "in_progress"
MigrationStateCompleted = "completed"
JobDataKeyMigration = "migration_key"
JobDataKeyMigrationLastDone = "last_done"
)
func MakeMigrationsList() []string {
return []string{
model.MigrationKeyAdvancedPermissionsPhase2,
}
}
func GetMigrationState(migration string, store store.Store) (string, *model.Job, *model.AppError) {
if _, err := store.System().GetByName(migration); err == nil {
return MigrationStateCompleted, nil, nil
}
jobs, err := store.Job().GetAllByType(model.JobTypeMigrations)
if err != nil {
return "", nil, model.NewAppError("GetMigrationState", "app.job.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, job := range jobs {
if key, ok := job.Data[JobDataKeyMigration]; ok {
if key != migration {
continue
}
switch job.Status {
case model.JobStatusInProgress, model.JobStatusPending:
return MigrationStateInProgress, job, nil
default:
return MigrationStateUnscheduled, job, nil
}
}
}
return MigrationStateUnscheduled, nil, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package migrations
import (
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
MigrationJobWedgedTimeoutMilliseconds = 3600000 // 1 hour
)
type Scheduler struct {
jobServer *jobs.JobServer
store store.Store
allMigrationsCompleted bool
}
func MakeScheduler(jobServer *jobs.JobServer, store store.Store) model.Scheduler {
return &Scheduler{jobServer, store, false}
}
func (scheduler *Scheduler) Enabled(_ *model.Config) bool {
return true
}
//nolint:unparam
func (scheduler *Scheduler) NextScheduleTime(cfg *model.Config, now time.Time, pendingJobs bool, lastSuccessfulJob *model.Job) *time.Time {
if scheduler.allMigrationsCompleted {
return nil
}
nextTime := time.Now().Add(60 * time.Second)
return &nextTime
}
//nolint:unparam
func (scheduler *Scheduler) ScheduleJob(cfg *model.Config, pendingJobs bool, lastSuccessfulJob *model.Job) (*model.Job, *model.AppError) {
mlog.Debug("Scheduling Job", mlog.String("scheduler", model.JobTypeMigrations))
// Work through the list of migrations in order. Schedule the first one that isn't done (assuming it isn't in progress already).
for _, key := range MakeMigrationsList() {
state, job, err := GetMigrationState(key, scheduler.store)
if err != nil {
mlog.Error("Failed to determine status of migration: ", mlog.String("scheduler", model.JobTypeMigrations), mlog.String("migration_key", key), mlog.Err(err))
return nil, nil
}
if state == MigrationStateInProgress {
// Check the migration job isn't wedged.
if job != nil && job.LastActivityAt < model.GetMillis()-MigrationJobWedgedTimeoutMilliseconds && job.CreateAt < model.GetMillis()-MigrationJobWedgedTimeoutMilliseconds {
mlog.Warn("Job appears to be wedged. Rescheduling another instance.", mlog.String("scheduler", model.JobTypeMigrations), mlog.String("wedged_job_id", job.Id), mlog.String("migration_key", key))
if err := scheduler.jobServer.SetJobError(job, nil); err != nil {
mlog.Error("Worker: Failed to set job error", mlog.String("scheduler", model.JobTypeMigrations), mlog.String("job_id", job.Id), mlog.Err(err))
}
return scheduler.createJob(key, job)
}
return nil, nil
}
if state == MigrationStateCompleted {
// This migration is done. Continue to check the next.
continue
}
if state == MigrationStateUnscheduled {
mlog.Debug("Scheduling a new job for migration.", mlog.String("scheduler", model.JobTypeMigrations), mlog.String("migration_key", key))
return scheduler.createJob(key, job)
}
mlog.Error("Unknown migration state. Not doing anything.", mlog.String("migration_state", state))
return nil, nil
}
// If we reached here, then there aren't any migrations left to run.
scheduler.allMigrationsCompleted = true
mlog.Debug("All migrations are complete.", mlog.String("scheduler", model.JobTypeMigrations))
return nil, nil
}
func (scheduler *Scheduler) createJob(migrationKey string, lastJob *model.Job) (*model.Job, *model.AppError) {
var lastDone string
if lastJob != nil {
lastDone = lastJob.Data[JobDataKeyMigrationLastDone]
}
data := map[string]string{
JobDataKeyMigration: migrationKey,
JobDataKeyMigrationLastDone: lastDone,
}
job, err := scheduler.jobServer.CreateJob(model.JobTypeMigrations, data)
if err != nil {
return nil, err
}
return job, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package migrations
import (
"context"
"net/http"
"sync/atomic"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
TimeBetweenBatches = 100
)
type Worker struct {
name string
stop chan struct{}
stopped chan bool
jobs chan model.Job
jobServer *jobs.JobServer
store store.Store
closed int32
}
func MakeWorker(jobServer *jobs.JobServer, store store.Store) model.Worker {
worker := Worker{
name: "Migrations",
stop: make(chan struct{}),
stopped: make(chan bool, 1),
jobs: make(chan model.Job),
jobServer: jobServer,
store: store,
}
return &worker
}
func (worker *Worker) Run() {
// Set to open if closed before. We are not bothered about multiple opens.
if atomic.CompareAndSwapInt32(&worker.closed, 1, 0) {
worker.stop = make(chan struct{})
}
mlog.Debug("Worker started", mlog.String("worker", worker.name))
defer func() {
mlog.Debug("Worker finished", mlog.String("worker", worker.name))
worker.stopped <- true
}()
for {
select {
case <-worker.stop:
mlog.Debug("Worker received stop signal", mlog.String("worker", worker.name))
return
case job := <-worker.jobs:
mlog.Debug("Worker received a new candidate job.", mlog.String("worker", worker.name))
worker.DoJob(&job)
}
}
}
func (worker *Worker) Stop() {
// Set to close, and if already closed before, then return.
if !atomic.CompareAndSwapInt32(&worker.closed, 0, 1) {
return
}
mlog.Debug("Worker stopping", mlog.String("worker", worker.name))
close(worker.stop)
<-worker.stopped
}
func (worker *Worker) JobChannel() chan<- model.Job {
return worker.jobs
}
func (worker *Worker) IsEnabled(_ *model.Config) bool {
return true
}
func (worker *Worker) DoJob(job *model.Job) {
defer worker.jobServer.HandleJobPanic(job)
if claimed, err := worker.jobServer.ClaimJob(job); err != nil {
mlog.Info("Worker experienced an error while trying to claim job",
mlog.String("worker", worker.name),
mlog.String("job_id", job.Id),
mlog.String("error", err.Error()))
return
} else if !claimed {
return
}
cancelCtx, cancelCancelWatcher := context.WithCancel(context.Background())
cancelWatcherChan := make(chan struct{}, 1)
go worker.jobServer.CancellationWatcher(cancelCtx, job.Id, cancelWatcherChan)
defer cancelCancelWatcher()
for {
select {
case <-cancelWatcherChan:
mlog.Debug("Worker: Job has been canceled via CancellationWatcher", mlog.String("worker", worker.name), mlog.String("job_id", job.Id))
worker.setJobCanceled(job)
return
case <-worker.stop:
mlog.Debug("Worker: Job has been canceled via Worker Stop", mlog.String("worker", worker.name), mlog.String("job_id", job.Id))
worker.setJobCanceled(job)
return
case <-time.After(TimeBetweenBatches * time.Millisecond):
done, progress, err := worker.runMigration(job.Data[JobDataKeyMigration], job.Data[JobDataKeyMigrationLastDone])
if err != nil {
mlog.Error("Worker: Failed to run migration", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
worker.setJobError(job, err)
return
} else if done {
mlog.Info("Worker: Job is complete", mlog.String("worker", worker.name), mlog.String("job_id", job.Id))
worker.setJobSuccess(job)
return
} else {
job.Data[JobDataKeyMigrationLastDone] = progress
if err := worker.jobServer.UpdateInProgressJobData(job); err != nil {
mlog.Error("Worker: Failed to update migration status data for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
worker.setJobError(job, err)
return
}
}
}
}
}
func (worker *Worker) setJobSuccess(job *model.Job) {
if err := worker.jobServer.SetJobSuccess(job); err != nil {
mlog.Error("Worker: Failed to set success for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
worker.setJobError(job, err)
}
}
func (worker *Worker) setJobError(job *model.Job, appError *model.AppError) {
if err := worker.jobServer.SetJobError(job, appError); err != nil {
mlog.Error("Worker: Failed to set job error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
}
}
func (worker *Worker) setJobCanceled(job *model.Job) {
if err := worker.jobServer.SetJobCanceled(job); err != nil {
mlog.Error("Worker: Failed to mark job as canceled", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
}
}
// Return parameters:
// - whether the migration is completed on this run (true) or still incomplete (false).
// - the updated lastDone string for the migration.
// - any error which may have occurred while running the migration.
func (worker *Worker) runMigration(key string, lastDone string) (bool, string, *model.AppError) {
var done bool
var progress string
var err *model.AppError
switch key {
case model.MigrationKeyAdvancedPermissionsPhase2:
done, progress, err = worker.runAdvancedPermissionsPhase2Migration(lastDone)
default:
return false, "", model.NewAppError("MigrationsWorker.runMigration", "migrations.worker.run_migration.unknown_key", map[string]any{"key": key}, "", http.StatusInternalServerError)
}
if done {
if nErr := worker.store.System().Save(&model.System{Name: key, Value: "true"}); nErr != nil {
return false, "", model.NewAppError("runMigration", "migrations.system.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return done, progress, err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package notify_admin
import (
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const installPluginSchedFreq = 24 * time.Hour
func MakeInstallPluginScheduler(jobServer *jobs.JobServer, license *model.License, jobType string) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
enabled := jobType == model.JobTypeInstallPluginNotifyAdmin
mlog.Debug("Scheduler: isEnabled: "+strconv.FormatBool(enabled), mlog.String("scheduler", jobType))
return enabled
}
return jobs.NewPeriodicScheduler(jobServer, jobType, installPluginSchedFreq, isEnabled)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package notify_admin
import (
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const schedFreq = 24 * time.Hour
func MakeScheduler(jobServer *jobs.JobServer, license *model.License, jobType string) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
enabled := license != nil && *license.Features.Cloud
mlog.Debug("Scheduler: isEnabled: "+strconv.FormatBool(enabled), mlog.String("scheduler", jobType))
return enabled
}
return jobs.NewPeriodicScheduler(jobServer, jobType, schedFreq, isEnabled)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package notify_admin
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
const (
UpgradeNotifyJobName = "UpgradeNotifyAdmin"
TrialNotifyJobName = "TrialNotifyAdmin"
InstallNotifyJobName = "InstallNotifyAdmin"
)
type AppIface interface {
DoCheckForAdminNotifications(trial bool) *model.AppError
}
func MakeUpgradeNotifyWorker(jobServer *jobs.JobServer, license *model.License, app AppIface) model.Worker {
isEnabled := func(_ *model.Config) bool {
return license != nil && license.Features != nil && *license.Features.Cloud
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
appErr := app.DoCheckForAdminNotifications(false)
if appErr != nil {
return appErr
}
return nil
}
worker := jobs.NewSimpleWorker(UpgradeNotifyJobName, jobServer, execute, isEnabled)
return worker
}
func MakeTrialNotifyWorker(jobServer *jobs.JobServer, license *model.License, app AppIface) model.Worker {
isEnabled := func(_ *model.Config) bool {
return license != nil && license.Features != nil && *license.Features.Cloud
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
appErr := app.DoCheckForAdminNotifications(true)
if appErr != nil {
return appErr
}
return nil
}
worker := jobs.NewSimpleWorker(TrialNotifyJobName, jobServer, execute, isEnabled)
return worker
}
func MakeInstallPluginNotifyWorker(jobServer *jobs.JobServer, app AppIface) model.Worker {
isEnabled := func(_ *model.Config) bool {
return true
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
appErr := app.DoCheckForAdminNotifications(false)
if appErr != nil {
return appErr
}
return nil
}
worker := jobs.NewSimpleWorker(InstallNotifyJobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package product_notices
import (
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
)
type Scheduler struct {
*jobs.PeriodicScheduler
}
func (scheduler *Scheduler) NextScheduleTime(cfg *model.Config, now time.Time, pendingJobs bool, lastSuccessfulJob *model.Job) *time.Time {
nextTime := time.Now().Add(time.Duration(*cfg.AnnouncementSettings.NoticesFetchFrequency) * time.Second)
return &nextTime
}
func MakeScheduler(jobServer *jobs.JobServer) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
return *cfg.AnnouncementSettings.AdminNoticesEnabled || *cfg.AnnouncementSettings.UserNoticesEnabled
}
return &Scheduler{PeriodicScheduler: jobs.NewPeriodicScheduler(jobServer, model.JobTypeProductNotices, 0, isEnabled)}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package product_notices
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const jobName = "ProductNotices"
type AppIface interface {
UpdateProductNotices() *model.AppError
}
func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker {
isEnabled := func(cfg *model.Config) bool {
return *cfg.AnnouncementSettings.AdminNoticesEnabled || *cfg.AnnouncementSettings.UserNoticesEnabled
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
if err := app.UpdateProductNotices(); err != nil {
mlog.Error("Worker: Failed to fetch product notices", mlog.String("worker", model.JobTypeProductNotices), mlog.String("job_id", job.Id), mlog.Err(err))
return err
}
return nil
}
worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled)
return worker
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package resend_invitation_email
import (
"encoding/json"
"net/http"
"os"
"strconv"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/configservice"
"github.com/mattermost/mattermost-server/v6/server/platform/services/telemetry"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const FourtyEightHoursInMillis int64 = 172800000
type AppIface interface {
configservice.ConfigService
GetUserByEmail(email string) (*model.User, *model.AppError)
GetTeamMembersByIds(teamID string, userIDs []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, *model.AppError)
InviteNewUsersToTeamGracefully(memberInvite *model.MemberInvite, teamID, senderId string, reminderInterval string) ([]*model.EmailInviteWithError, *model.AppError)
}
type ResendInvitationEmailWorker struct {
name string
stop chan bool
stopped chan bool
jobs chan model.Job
jobServer *jobs.JobServer
app AppIface
store store.Store
telemetryService *telemetry.TelemetryService
}
func MakeWorker(jobServer *jobs.JobServer, app AppIface, store store.Store, telemetryService *telemetry.TelemetryService) model.Worker {
worker := ResendInvitationEmailWorker{
name: model.JobTypeResendInvitationEmail,
stop: make(chan bool, 1),
stopped: make(chan bool, 1),
jobs: make(chan model.Job),
jobServer: jobServer,
app: app,
store: store,
telemetryService: telemetryService,
}
return &worker
}
func (rseworker *ResendInvitationEmailWorker) Run() {
mlog.Debug("Worker started", mlog.String("worker", rseworker.name))
defer func() {
mlog.Debug("Worker finished", mlog.String("worker", rseworker.name))
rseworker.stopped <- true
}()
for {
select {
case <-rseworker.stop:
mlog.Debug("Worker received stop signal", mlog.String("worker", rseworker.name))
return
case job := <-rseworker.jobs:
mlog.Debug("Worker received a new candidate job.", mlog.String("worker", rseworker.name))
rseworker.DoJob(&job)
}
}
}
func (rseworker *ResendInvitationEmailWorker) IsEnabled(cfg *model.Config) bool {
return *cfg.ServiceSettings.EnableEmailInvitations
}
func (rseworker *ResendInvitationEmailWorker) Stop() {
mlog.Debug("Worker stopping", mlog.String("worker", rseworker.name))
rseworker.stop <- true
<-rseworker.stopped
}
func (rseworker *ResendInvitationEmailWorker) JobChannel() chan<- model.Job {
return rseworker.jobs
}
func (rseworker *ResendInvitationEmailWorker) DoJob(job *model.Job) {
defer rseworker.jobServer.HandleJobPanic(job)
elapsedTimeSinceSchedule, DurationInMillis := rseworker.GetDurations(job)
if elapsedTimeSinceSchedule > DurationInMillis {
rseworker.ResendEmails(job, "48")
rseworker.TearDown(job)
}
}
func (rseworker *ResendInvitationEmailWorker) setJobSuccess(job *model.Job) {
if err := rseworker.jobServer.SetJobSuccess(job); err != nil {
mlog.Error("Worker: Failed to set success for job", mlog.String("worker", rseworker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
rseworker.setJobError(job, err)
}
}
func (rseworker *ResendInvitationEmailWorker) setJobError(job *model.Job, appError *model.AppError) {
if err := rseworker.jobServer.SetJobError(job, appError); err != nil {
mlog.Error("Worker: Failed to set job error", mlog.String("worker", rseworker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
}
}
func (rseworker *ResendInvitationEmailWorker) cleanEmailData(emailStringData string) ([]string, error) {
// emailStringData looks like this ["user1@gmail.com","user2@gmail.com"]
emails := []string{}
err := json.Unmarshal([]byte(emailStringData), &emails)
if err != nil {
return nil, err
}
return emails, nil
}
func (rseworker *ResendInvitationEmailWorker) cleanChannelsData(channelStringData string) ([]string, error) {
// channelStringData looks like this ["uuuiiiiidddd","uuuiiiiidddd"]
channels := []string{}
err := json.Unmarshal([]byte(channelStringData), &channels)
if err != nil {
return nil, err
}
return channels, nil
}
func (rseworker *ResendInvitationEmailWorker) removeAlreadyJoined(teamID string, emailList []string) []string {
var notJoinedYet []string
for _, email := range emailList {
// check if the user with this email is on the system already
user, appErr := rseworker.app.GetUserByEmail(email)
if appErr != nil {
notJoinedYet = append(notJoinedYet, email)
continue
}
// now we check if they are part of the team already
userID := []string{user.Id}
members, appErr := rseworker.app.GetTeamMembersByIds(teamID, userID, nil)
if len(members) == 0 || appErr != nil {
notJoinedYet = append(notJoinedYet, email)
}
}
return notJoinedYet
}
func (rseworker *ResendInvitationEmailWorker) GetDurations(job *model.Job) (int64, int64) {
scheduledAt, _ := strconv.ParseInt(job.Data["scheduledAt"], 10, 64)
now := model.GetMillis()
elapsedTimeSinceSchedule := now - scheduledAt
duration := os.Getenv("MM_RESEND_INVITATION_EMAIL_JOB_DURATION")
DurationInMillis, parseError := strconv.ParseInt(duration, 10, 64)
if parseError != nil {
// default to 48 hours
DurationInMillis = FourtyEightHoursInMillis
}
return elapsedTimeSinceSchedule, DurationInMillis
}
func (rseworker *ResendInvitationEmailWorker) TearDown(job *model.Job) {
rseworker.store.System().PermanentDeleteByName(job.Id)
rseworker.setJobSuccess(job)
}
func (rseworker *ResendInvitationEmailWorker) ResendEmails(job *model.Job, interval string) {
teamID := job.Data["teamID"]
emailListData := job.Data["emailList"]
channelListData := job.Data["channelList"]
emailList, err := rseworker.cleanEmailData(emailListData)
if err != nil {
appErr := model.NewAppError("worker: "+rseworker.name, "job_id: "+job.Id, nil, "", http.StatusInternalServerError).Wrap(err)
mlog.Error("Worker: Failed to clean emails string data", mlog.String("worker", rseworker.name), mlog.String("job_id", job.Id), mlog.String("error", appErr.Error()))
rseworker.setJobError(job, appErr)
}
channelList, err := rseworker.cleanChannelsData(channelListData)
if err != nil {
appErr := model.NewAppError("worker: "+rseworker.name, "job_id: "+job.Id, nil, "", http.StatusInternalServerError).Wrap(err)
mlog.Error("Worker: Failed to clean channel string data", mlog.String("worker", rseworker.name), mlog.String("job_id", job.Id), mlog.String("error", appErr.Error()))
rseworker.setJobError(job, appErr)
}
emailList = rseworker.removeAlreadyJoined(teamID, emailList)
memberInvite := model.MemberInvite{
Emails: emailList,
}
if len(channelList) > 0 {
memberInvite.ChannelIds = channelList
}
_, appErr := rseworker.app.InviteNewUsersToTeamGracefully(&memberInvite, teamID, job.Data["senderID"], interval)
if appErr != nil {
mlog.Error("Worker: Failed to send emails", mlog.String("worker", rseworker.name), mlog.String("job_id", job.Id), mlog.String("error", appErr.Error()))
rseworker.setJobError(job, appErr)
}
rseworker.telemetryService.SendTelemetry("track_invite_email_resend", map[string]any{interval: interval})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package jobs
import (
"errors"
"fmt"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type Schedulers struct {
stop chan bool
stopped chan bool
configChanged chan *model.Config
clusterLeaderChanged chan bool
listenerId string
jobs *JobServer
isLeader bool
running bool
schedulers map[string]model.Scheduler
nextRunTimes map[string]*time.Time
}
var (
ErrSchedulersNotRunning = errors.New("job schedulers are not running")
ErrSchedulersRunning = errors.New("job schedulers are running")
ErrSchedulersUninitialized = errors.New("job schedulers are not initialized")
)
func (schedulers *Schedulers) AddScheduler(name string, scheduler model.Scheduler) {
schedulers.schedulers[name] = scheduler
}
// Start starts the schedulers. This call is not safe for concurrent use.
// Synchronization should be implemented by the caller.
func (schedulers *Schedulers) Start() {
schedulers.stop = make(chan bool)
schedulers.stopped = make(chan bool)
schedulers.listenerId = schedulers.jobs.ConfigService.AddConfigListener(schedulers.handleConfigChange)
go func() {
mlog.Info("Starting schedulers.")
defer func() {
mlog.Info("Schedulers stopped.")
close(schedulers.stopped)
}()
now := time.Now()
for name, scheduler := range schedulers.schedulers {
if !scheduler.Enabled(schedulers.jobs.Config()) {
schedulers.nextRunTimes[name] = nil
} else {
schedulers.setNextRunTime(schedulers.jobs.Config(), name, now, false)
}
}
for {
timer := time.NewTimer(1 * time.Minute)
select {
case <-schedulers.stop:
mlog.Debug("Schedulers received stop signal.")
timer.Stop()
return
case now = <-timer.C:
cfg := schedulers.jobs.Config()
for name, nextTime := range schedulers.nextRunTimes {
if nextTime == nil {
continue
}
if time.Now().After(*nextTime) {
scheduler := schedulers.schedulers[name]
if scheduler == nil || !schedulers.isLeader || !scheduler.Enabled(cfg) {
continue
}
if _, err := schedulers.scheduleJob(cfg, name, scheduler); err != nil {
mlog.Error("Failed to schedule job", mlog.String("scheduler", name), mlog.Err(err))
continue
}
schedulers.setNextRunTime(cfg, name, now, true)
}
}
case newCfg := <-schedulers.configChanged:
for name, scheduler := range schedulers.schedulers {
if !schedulers.isLeader || !scheduler.Enabled(newCfg) {
schedulers.nextRunTimes[name] = nil
} else {
schedulers.setNextRunTime(newCfg, name, now, false)
}
}
case isLeader := <-schedulers.clusterLeaderChanged:
for name := range schedulers.schedulers {
schedulers.isLeader = isLeader
if !isLeader {
schedulers.nextRunTimes[name] = nil
} else {
schedulers.setNextRunTime(schedulers.jobs.Config(), name, now, false)
}
}
}
timer.Stop()
}
}()
schedulers.running = true
}
// Stop stops the schedulers. This call is not safe for concurrent use.
// Synchronization should be implemented by the caller.
func (schedulers *Schedulers) Stop() {
mlog.Info("Stopping schedulers.")
close(schedulers.stop)
<-schedulers.stopped
schedulers.jobs.ConfigService.RemoveConfigListener(schedulers.listenerId)
schedulers.listenerId = ""
schedulers.running = false
}
func (schedulers *Schedulers) setNextRunTime(cfg *model.Config, name string, now time.Time, pendingJobs bool) {
scheduler := schedulers.schedulers[name]
if !pendingJobs {
pj, err := schedulers.jobs.CheckForPendingJobsByType(name)
if err != nil {
mlog.Error("Failed to set next job run time", mlog.Err(err))
schedulers.nextRunTimes[name] = nil
return
}
pendingJobs = pj
}
lastSuccessfulJob, err := schedulers.jobs.GetLastSuccessfulJobByType(name)
if err != nil {
mlog.Error("Failed to set next job run time", mlog.Err(err))
schedulers.nextRunTimes[name] = nil
return
}
schedulers.nextRunTimes[name] = scheduler.NextScheduleTime(cfg, now, pendingJobs, lastSuccessfulJob)
mlog.Debug("Next run time for scheduler", mlog.String("scheduler_name", name), mlog.String("next_runtime", fmt.Sprintf("%v", schedulers.nextRunTimes[name])))
}
func (schedulers *Schedulers) scheduleJob(cfg *model.Config, name string, scheduler model.Scheduler) (*model.Job, *model.AppError) {
pendingJobs, err := schedulers.jobs.CheckForPendingJobsByType(name)
if err != nil {
return nil, err
}
lastSuccessfulJob, err2 := schedulers.jobs.GetLastSuccessfulJobByType(name)
if err2 != nil {
return nil, err
}
return scheduler.ScheduleJob(cfg, pendingJobs, lastSuccessfulJob)
}
func (schedulers *Schedulers) handleConfigChange(_, newConfig *model.Config) {
mlog.Debug("Schedulers received config change.")
select {
case schedulers.configChanged <- newConfig:
case <-schedulers.stop:
}
}
func (schedulers *Schedulers) handleClusterLeaderChange(isLeader bool) {
select {
case schedulers.clusterLeaderChanged <- isLeader:
default:
mlog.Debug("Sending cluster leader change message to schedulers failed.")
// Drain the buffered channel to make room for the latest change.
select {
case <-schedulers.clusterLeaderChanged:
default:
}
// Enqueue the latest change. This operation is safe due to this method
// being called under lock.
schedulers.clusterLeaderChanged <- isLeader
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package jobs
import (
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/configservice"
)
type JobServer struct {
ConfigService configservice.ConfigService
Store store.Store
metrics einterfaces.MetricsInterface
// mut is used to protect the following fields from concurrent access.
mut sync.Mutex
workers *Workers
schedulers *Schedulers
}
func NewJobServer(configService configservice.ConfigService, store store.Store, metrics einterfaces.MetricsInterface) *JobServer {
srv := &JobServer{
ConfigService: configService,
Store: store,
metrics: metrics,
}
srv.initWorkers()
srv.initSchedulers()
return srv
}
func (srv *JobServer) initWorkers() {
workers := NewWorkers(srv.ConfigService)
workers.Watcher = srv.MakeWatcher(workers, DefaultWatcherPollingInterval)
srv.workers = workers
}
func (srv *JobServer) initSchedulers() {
schedulers := &Schedulers{
configChanged: make(chan *model.Config),
clusterLeaderChanged: make(chan bool, 1),
jobs: srv,
isLeader: true,
schedulers: make(map[string]model.Scheduler),
nextRunTimes: make(map[string]*time.Time),
}
srv.schedulers = schedulers
}
func (srv *JobServer) Config() *model.Config {
return srv.ConfigService.Config()
}
func (srv *JobServer) RegisterJobType(name string, worker model.Worker, scheduler model.Scheduler) {
srv.mut.Lock()
defer srv.mut.Unlock()
if worker != nil {
srv.workers.AddWorker(name, worker)
}
if scheduler != nil {
srv.schedulers.AddScheduler(name, scheduler)
}
}
func (srv *JobServer) StartWorkers() error {
srv.mut.Lock()
defer srv.mut.Unlock()
if srv.workers == nil {
return ErrWorkersUninitialized
} else if srv.workers.running {
return ErrWorkersRunning
}
srv.workers.Start()
return nil
}
func (srv *JobServer) StartSchedulers() error {
srv.mut.Lock()
defer srv.mut.Unlock()
if srv.schedulers == nil {
return ErrSchedulersUninitialized
} else if srv.schedulers.running {
return ErrSchedulersRunning
}
srv.schedulers.Start()
return nil
}
func (srv *JobServer) StopWorkers() error {
srv.mut.Lock()
defer srv.mut.Unlock()
if srv.workers == nil {
return ErrWorkersUninitialized
} else if !srv.workers.running {
return ErrWorkersNotRunning
}
srv.workers.Stop()
return nil
}
func (srv *JobServer) StopSchedulers() error {
srv.mut.Lock()
defer srv.mut.Unlock()
if srv.schedulers == nil {
return ErrSchedulersUninitialized
} else if !srv.schedulers.running {
return ErrSchedulersNotRunning
}
srv.schedulers.Stop()
return nil
}
func (srv *JobServer) HandleClusterLeaderChange(isLeader bool) {
srv.mut.Lock()
defer srv.mut.Unlock()
if srv.schedulers != nil {
srv.schedulers.handleClusterLeaderChange(isLeader)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package jobs
import (
"errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/configservice"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type Workers struct {
ConfigService configservice.ConfigService
Watcher *Watcher
workers map[string]model.Worker
listenerId string
running bool
}
var (
ErrWorkersNotRunning = errors.New("job workers are not running")
ErrWorkersRunning = errors.New("job workers are running")
ErrWorkersUninitialized = errors.New("job workers are not initialized")
)
func NewWorkers(configService configservice.ConfigService) *Workers {
return &Workers{
ConfigService: configService,
workers: make(map[string]model.Worker),
}
}
func (workers *Workers) AddWorker(name string, worker model.Worker) {
workers.workers[name] = worker
}
func (workers *Workers) Get(name string) model.Worker {
return workers.workers[name]
}
// Start starts the workers. This call is not safe for concurrent use.
// Synchronization should be implemented by the caller.
func (workers *Workers) Start() {
mlog.Info("Starting workers")
for _, w := range workers.workers {
if w.IsEnabled(workers.ConfigService.Config()) {
go w.Run()
}
}
go workers.Watcher.Start()
workers.listenerId = workers.ConfigService.AddConfigListener(workers.handleConfigChange)
workers.running = true
}
func (workers *Workers) handleConfigChange(oldConfig *model.Config, newConfig *model.Config) {
mlog.Debug("Workers received config change.")
for _, w := range workers.workers {
if w.IsEnabled(oldConfig) && !w.IsEnabled(newConfig) {
w.Stop()
}
if !w.IsEnabled(oldConfig) && w.IsEnabled(newConfig) {
go w.Run()
}
}
}
// Stop stops the workers. This call is not safe for concurrent use.
// Synchronization should be implemented by the caller.
func (workers *Workers) Stop() {
workers.ConfigService.RemoveConfigListener(workers.listenerId)
workers.Watcher.Stop()
for _, w := range workers.workers {
if w.IsEnabled(workers.ConfigService.Config()) {
w.Stop()
}
}
workers.running = false
mlog.Info("Stopped workers")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package manualtesting
import (
"errors"
"hash/fnv"
"math/rand"
"net/http"
"net/url"
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/api4"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/slashcommands"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// TestEnvironment is a helper struct used for tests in manualtesting.
type TestEnvironment struct {
Params map[string][]string
Client *model.Client4
CreatedTeamID string
CreatedUserID string
Context *web.Context
Writer http.ResponseWriter
Request *http.Request
}
// Init adds manualtest endpoint to the API.
func Init(api4 *api4.API) {
api4.BaseRoutes.Root.Handle("/manualtest", api4.APIHandler(manualTest)).Methods("GET")
}
func manualTest(c *web.Context, w http.ResponseWriter, r *http.Request) {
// Let the world know
mlog.Info("Setting up for manual test...")
// URL Parameters
params, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
c.Err = model.NewAppError("/manual", "manaultesting.manual_test.parse.app_error", nil, "", http.StatusBadRequest)
return
}
// Grab a uuid (if available) to seed the random number generator so we don't get conflicts.
uid, ok := params["uid"]
if ok {
hasher := fnv.New32a()
hasher.Write([]byte(uid[0] + strconv.Itoa(int(time.Now().UTC().UnixNano()))))
hash := hasher.Sum32()
rand.Seed(int64(hash))
} else {
mlog.Debug("No uid in URL")
}
// Create a client for tests to use
client := model.NewAPIv4Client("http://localhost" + *c.App.Config().ServiceSettings.ListenAddress)
// Check for username parameter and create a user if present
username, ok1 := params["username"]
teamDisplayName, ok2 := params["teamname"]
var teamID string
var userID string
if ok1 && ok2 {
mlog.Info("Creating user and team")
// Create team for testing
team := &model.Team{
DisplayName: teamDisplayName[0],
Name: "zz" + utils.RandomName(utils.Range{Begin: 20, End: 20}, utils.LOWERCASE),
Email: "success+" + model.NewId() + "simulator.amazonses.com",
Type: model.TeamOpen,
}
createdTeam, err := c.App.Srv().Store().Team().Save(team)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &invErr):
c.Err = model.NewAppError("manualTest", "app.team.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr):
c.Err = appErr
default:
c.Err = model.NewAppError("manualTest", "app.team.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return
}
channel := &model.Channel{DisplayName: "Town Square", Name: "town-square", Type: model.ChannelTypeOpen, TeamId: createdTeam.Id}
if _, err := c.App.CreateChannel(c.AppContext, channel, false); err != nil {
c.Err = err
return
}
teamID = createdTeam.Id
// Create user for testing
user := &model.User{
Email: "success+" + model.NewId() + "simulator.amazonses.com",
Nickname: username[0],
Password: slashcommands.UserPassword}
user, _, err = client.CreateUser(user)
if err != nil {
var appErr *model.AppError
ok = errors.As(err, &appErr)
if ok {
c.Err = appErr
} else {
c.Err = model.NewAppError("manualTest", "app.user.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return
}
c.App.Srv().Store().User().VerifyEmail(user.Id, user.Email)
c.App.Srv().Store().Team().SaveMember(&model.TeamMember{TeamId: teamID, UserId: user.Id}, *c.App.Config().TeamSettings.MaxUsersPerTeam)
userID = user.Id
// Login as user to generate auth token
_, _, err = client.LoginById(user.Id, slashcommands.UserPassword)
if err != nil {
var appErr *model.AppError
ok = errors.As(err, &appErr)
if ok {
c.Err = appErr
} else {
c.Err = model.NewAppError("manualTest", "api.user.login.bot_login_forbidden.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return
}
// Respond with an auth token this can be overridden by a specific test as required
sessionCookie := &http.Cookie{
Name: model.SessionCookieToken,
Value: client.AuthToken,
Path: "/",
MaxAge: *c.App.Config().ServiceSettings.SessionLengthWebInHours * 60 * 60,
HttpOnly: true,
}
http.SetCookie(w, sessionCookie)
http.Redirect(w, r, "/channels/town-square", http.StatusTemporaryRedirect)
}
// Setup test environment
env := TestEnvironment{
Params: params,
Client: client,
CreatedTeamID: teamID,
CreatedUserID: userID,
Context: c,
Writer: w,
Request: r,
}
// Grab the test ID and pick the test
testname, ok := params["test"]
if !ok {
c.Err = model.NewAppError("/manual", "manaultesting.manual_test.parse.app_error", nil, "", http.StatusBadRequest)
return
}
switch testname[0] {
case "autolink":
c.Err = testAutoLink(env)
// ADD YOUR NEW TEST HERE!
case "general":
}
}
func getChannelID(a app.AppIface, channelname string, teamid string, userid string) (string, bool) {
// Grab all the channels
channels, err := a.Srv().Store().Channel().GetChannels(teamid, userid, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
})
if err != nil {
mlog.Debug("Unable to get channels")
return "", false
}
for _, channel := range channels {
if channel.Name == channelname {
return channel.Id, true
}
}
mlog.Debug("Could not find channel", mlog.String("Channel name", channelname), mlog.Int("Possibilities searched", len(channels)))
return "", false
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package manualtesting
import (
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const linkPostText = `
Some Links:
https://spinpunch.atlassian.net/issues/?filter=10101&jql=resolution%20in%20(Fixed%2C%20%22Won't%20Fix%22%2C%20Duplicate%2C%20%22Cannot%20Reproduce%22)%20AND%20Resolution%20%3D%20Fixed%20AND%20updated%20%3E%3D%20-7d%20ORDER%20BY%20updatedDate%20DESC
https://www.google.com.pk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=2&cad=rja&uact=8&ved=0CCUQFjAB&url=https%3A%2F%2Fdevelopers.google.com%2Fmaps%2Fdocumentation%2Fios%2Furlscheme&ei=HBFbVdSBN-WcygOG4oHIBw&usg=AFQjCNGI0Jg92Y7qNmyIpQyvYPut7vx5-Q&bvm=bv.93564037,d.bGg
http://www.google.com.pk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=4&cad=rja&uact=8&ved=0CC8QFjAD&url=http%3A%2F%2Fwww.quora.com%2FHow-long-will-a-Google-shortened-URL-be-available&ei=XRBbVbPLGYKcsAGqiIDQAw&usg=AFQjCNHY0Xi-GG4hgbrPUY_8Kg-55_-DNQ&bvm=bv.93564037,d.bGg
https://medium.com/@slackhq/11-useful-tips-for-getting-the-most-of-slack-5dfb3d1af77
`
func testAutoLink(env TestEnvironment) *model.AppError {
mlog.Info("Manual Auto Link Test")
channelID, ok := getChannelID(env.Context.App, model.DefaultChannelName, env.CreatedTeamID, env.CreatedUserID)
if !ok {
return model.NewAppError("/manualtest", "manaultesting.test_autolink.unable.app_error", nil, "", http.StatusInternalServerError)
}
post := &model.Post{
ChannelId: channelID,
Message: linkPostText}
_, _, err := env.Client.CreatePost(post)
var appErr *model.AppError
if ok = errors.As(err, &appErr); !ok {
appErr = model.NewAppError("/manualtest", "manaultesting.test_autolink.unable.app_error", nil, "", http.StatusInternalServerError)
}
return appErr
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package product
import (
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
)
type HooksManager struct {
registeredProducts sync.Map
metrics einterfaces.MetricsInterface
}
func NewHooksManager(metrics einterfaces.MetricsInterface) *HooksManager {
return &HooksManager{
metrics: metrics,
}
}
func (m *HooksManager) AddProduct(productID string, hooks any) error {
prod, err := plugin.NewAdapter(hooks)
if err != nil {
return err
}
rp := &plugin.RegisteredProduct{
ProductID: productID,
Adapter: prod,
}
m.registeredProducts.Store(productID, rp)
return nil
}
func (m *HooksManager) RemoveProduct(productID string) {
m.registeredProducts.Delete(productID)
}
func (m *HooksManager) RunMultiHook(hookRunnerFunc func(hooks plugin.Hooks) bool, hookId int) {
startTime := time.Now()
m.registeredProducts.Range(func(key, value any) bool {
rp := value.(*plugin.RegisteredProduct)
if !rp.Implements(hookId) {
return true
}
hookStartTime := time.Now()
result := hookRunnerFunc(rp.Adapter)
if m.metrics != nil {
elapsedTime := float64(time.Since(hookStartTime)) / float64(time.Second)
m.metrics.ObservePluginMultiHookIterationDuration(rp.ProductID, elapsedTime)
}
return result
})
if m.metrics != nil {
elapsedTime := float64(time.Since(startTime)) / float64(time.Second)
m.metrics.ObservePluginMultiHookDuration(elapsedTime)
}
}
func (m *HooksManager) HooksForProduct(id string) plugin.Hooks {
if value, ok := m.registeredProducts.Load(id); ok {
rp := value.(*plugin.RegisteredProduct)
return rp.Adapter
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package product
type Product interface {
Start() error
Stop() error
}
type Manifest struct {
Initializer func(map[ServiceKey]any) (Product, error)
Dependencies map[ServiceKey]struct{}
}
var products = make(map[string]Manifest)
func RegisterProduct(name string, m Manifest) {
products[name] = m
}
func GetProducts() map[string]Manifest {
return products
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package store
import (
"fmt"
"strings"
)
// ErrInvalidInput indicates an error that has occurred due to an invalid input.
type ErrInvalidInput struct {
Entity string // The entity which was sent as the input.
Field string // The field of the entity which was invalid.
Value any // The actual value of the field.
wrapped error // The original error
}
func NewErrInvalidInput(entity, field string, value any) *ErrInvalidInput {
return &ErrInvalidInput{
Entity: entity,
Field: field,
Value: value,
}
}
func (e *ErrInvalidInput) Error() string {
if e.wrapped != nil {
return fmt.Sprintf("invalid input: entity: %s field: %s value: %s error: %s", e.Entity, e.Field, e.Value, e.wrapped)
}
return fmt.Sprintf("invalid input: entity: %s field: %s value: %s", e.Entity, e.Field, e.Value)
}
func (e *ErrInvalidInput) Wrap(err error) *ErrInvalidInput {
e.wrapped = err
return e
}
func (e *ErrInvalidInput) Unwrap() error {
return e.wrapped
}
func (e *ErrInvalidInput) InvalidInputInfo() (entity string, field string, value any) {
entity = e.Entity
field = e.Field
value = e.Value
return
}
// ErrLimitExceeded indicates an error that has occurred because some value exceeded a limit.
type ErrLimitExceeded struct {
What string // What was the object that exceeded.
Count int // The value of the object.
meta string // Any additional metadata.
}
func NewErrLimitExceeded(what string, count int, meta string) *ErrLimitExceeded {
return &ErrLimitExceeded{
What: what,
Count: count,
meta: meta,
}
}
func (e *ErrLimitExceeded) Error() string {
return fmt.Sprintf("limit exceeded: what: %s count: %d metadata: %s", e.What, e.Count, e.meta)
}
// ErrConflict indicates a conflict that occurred.
type ErrConflict struct {
Resource string // The resource which created the conflict.
err error // Internal error.
meta string // Any additional metadata.
}
func NewErrConflict(resource string, err error, meta string) *ErrConflict {
return &ErrConflict{
Resource: resource,
err: err,
meta: meta,
}
}
func (e *ErrConflict) Error() string {
msg := e.Resource + "exists " + e.meta
if e.err != nil {
msg += " " + e.err.Error()
}
return msg
}
func (e *ErrConflict) Unwrap() error {
return e.err
}
// IsErrConflict allows easy type assertion without adding store as a dependency.
func (e *ErrConflict) IsErrConflict() bool {
return true
}
// ErrNotFound indicates that a resource was not found
type ErrNotFound struct {
resource string
ID string
wrapped error
}
func NewErrNotFound(resource, id string) *ErrNotFound {
return &ErrNotFound{
resource: resource,
ID: id,
}
}
func (e *ErrNotFound) Wrap(err error) *ErrNotFound {
e.wrapped = err
return e
}
func (e *ErrNotFound) Error() string {
if e.wrapped != nil {
return fmt.Sprintf("resource: %s id: %s error: %s", e.resource, e.ID, e.wrapped)
}
return fmt.Sprintf("resource: %s id: %s", e.resource, e.ID)
}
// IsErrNotFound allows easy type assertion without adding store as a dependency.
func (e *ErrNotFound) IsErrNotFound() bool {
return true
}
// ErrOutOfBounds indicates that the requested total numbers of rows
// was greater than the allowed limit.
type ErrOutOfBounds struct {
value int
}
func (e *ErrOutOfBounds) Error() string {
return fmt.Sprintf("invalid limit parameter: %d", e.value)
}
func NewErrOutOfBounds(value int) *ErrOutOfBounds {
return &ErrOutOfBounds{value: value}
}
// ErrNotImplemented indicates that some feature or requirement is not implemented yet.
type ErrNotImplemented struct {
detail string
}
func (e *ErrNotImplemented) Error() string {
return e.detail
}
func NewErrNotImplemented(detail string) *ErrNotImplemented {
return &ErrNotImplemented{detail: detail}
}
type ErrUniqueConstraint struct {
Columns []string
}
// NewErrUniqueConstraint creates a uniqueness constraint error for the given column(s).
//
// Examples:
//
// store.NewErrUniqueConstraint("DisplayName") // single column constraint
// store.NewErrUniqueConstraint("Name", "Source") // multi-column constraint
func NewErrUniqueConstraint(columns ...string) *ErrUniqueConstraint {
return &ErrUniqueConstraint{
Columns: columns,
}
}
func (e *ErrUniqueConstraint) Error() string {
var tmpl string
if len(e.Columns) > 1 {
tmpl = "unique constraint: (%s)"
} else {
tmpl = "unique constraint: %s"
}
return fmt.Sprintf(tmpl, strings.Join(e.Columns, ","))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type LocalCacheChannelStore struct {
store.ChannelStore
rootStore *LocalCacheStore
}
func (s *LocalCacheChannelStore) handleClusterInvalidateChannelMemberCounts(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.channelMemberCountsCache.Purge()
} else {
s.rootStore.channelMemberCountsCache.Remove(string(msg.Data))
}
}
func (s *LocalCacheChannelStore) handleClusterInvalidateChannelPinnedPostCount(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.channelPinnedPostCountsCache.Purge()
} else {
s.rootStore.channelPinnedPostCountsCache.Remove(string(msg.Data))
}
}
func (s *LocalCacheChannelStore) handleClusterInvalidateChannelGuestCounts(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.channelGuestCountCache.Purge()
} else {
s.rootStore.channelGuestCountCache.Remove(string(msg.Data))
}
}
func (s *LocalCacheChannelStore) handleClusterInvalidateChannelById(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.channelByIdCache.Purge()
} else {
s.rootStore.channelByIdCache.Remove(string(msg.Data))
}
}
func (s LocalCacheChannelStore) ClearCaches() {
s.rootStore.doClearCacheCluster(s.rootStore.channelMemberCountsCache)
s.rootStore.doClearCacheCluster(s.rootStore.channelPinnedPostCountsCache)
s.rootStore.doClearCacheCluster(s.rootStore.channelGuestCountCache)
s.rootStore.doClearCacheCluster(s.rootStore.channelByIdCache)
s.ChannelStore.ClearCaches()
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel Pinned Post Counts - Purge")
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel Member Counts - Purge")
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel Guest Count - Purge")
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel - Purge")
}
}
func (s LocalCacheChannelStore) InvalidatePinnedPostCount(channelId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.channelPinnedPostCountsCache, channelId)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel Pinned Post Counts - Remove by ChannelId")
}
}
func (s LocalCacheChannelStore) InvalidateMemberCount(channelId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.channelMemberCountsCache, channelId)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel Member Counts - Remove by ChannelId")
}
}
func (s LocalCacheChannelStore) InvalidateGuestCount(channelId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.channelGuestCountCache, channelId)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel Guests Count - Remove by channelId")
}
}
func (s LocalCacheChannelStore) InvalidateChannel(channelId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.channelByIdCache, channelId)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel - Remove by ChannelId")
}
}
func (s LocalCacheChannelStore) GetMemberCount(channelId string, allowFromCache bool) (int64, error) {
if allowFromCache {
var count int64
if err := s.rootStore.doStandardReadCache(s.rootStore.channelMemberCountsCache, channelId, &count); err == nil {
return count, nil
}
}
count, err := s.ChannelStore.GetMemberCount(channelId, allowFromCache)
if allowFromCache && err == nil {
s.rootStore.doStandardAddToCache(s.rootStore.channelMemberCountsCache, channelId, count)
}
return count, err
}
func (s LocalCacheChannelStore) GetGuestCount(channelId string, allowFromCache bool) (int64, error) {
if allowFromCache {
var count int64
if err := s.rootStore.doStandardReadCache(s.rootStore.channelGuestCountCache, channelId, &count); err == nil {
return count, nil
}
}
count, err := s.ChannelStore.GetGuestCount(channelId, allowFromCache)
if allowFromCache && err == nil {
s.rootStore.doStandardAddToCache(s.rootStore.channelGuestCountCache, channelId, count)
}
return count, err
}
func (s LocalCacheChannelStore) GetMemberCountFromCache(channelId string) int64 {
var count int64
if err := s.rootStore.doStandardReadCache(s.rootStore.channelMemberCountsCache, channelId, &count); err == nil {
return count
}
count, err := s.GetMemberCount(channelId, true)
if err != nil {
return 0
}
return count
}
func (s LocalCacheChannelStore) GetPinnedPostCount(channelId string, allowFromCache bool) (int64, error) {
if allowFromCache {
var count int64
if err := s.rootStore.doStandardReadCache(s.rootStore.channelPinnedPostCountsCache, channelId, &count); err == nil {
return count, nil
}
}
count, err := s.ChannelStore.GetPinnedPostCount(channelId, allowFromCache)
if err != nil {
return 0, err
}
if allowFromCache {
s.rootStore.doStandardAddToCache(s.rootStore.channelPinnedPostCountsCache, channelId, count)
}
return count, nil
}
func (s LocalCacheChannelStore) Get(id string, allowFromCache bool) (*model.Channel, error) {
if allowFromCache {
var cacheItem *model.Channel
if err := s.rootStore.doStandardReadCache(s.rootStore.channelByIdCache, id, &cacheItem); err == nil {
return cacheItem, nil
}
}
ch, err := s.ChannelStore.Get(id, allowFromCache)
if allowFromCache && err == nil {
s.rootStore.doStandardAddToCache(s.rootStore.channelByIdCache, id, ch)
}
return ch, err
}
func (s LocalCacheChannelStore) GetMany(ids []string, allowFromCache bool) (model.ChannelList, error) {
var foundChannels []*model.Channel
var channelsToQuery []string
if allowFromCache {
for _, id := range ids {
var ch *model.Channel
if err := s.rootStore.doStandardReadCache(s.rootStore.channelByIdCache, id, &ch); err == nil {
foundChannels = append(foundChannels, ch)
} else {
channelsToQuery = append(channelsToQuery, id)
}
}
}
if channelsToQuery == nil {
return foundChannels, nil
}
channels, err := s.ChannelStore.GetMany(channelsToQuery, allowFromCache)
if err != nil {
return nil, err
}
for _, ch := range channels {
s.rootStore.doStandardAddToCache(s.rootStore.channelByIdCache, ch.Id, ch)
}
return append(foundChannels, channels...), nil
}
func (s LocalCacheChannelStore) SaveMember(member *model.ChannelMember) (*model.ChannelMember, error) {
member, err := s.ChannelStore.SaveMember(member)
if err != nil {
return nil, err
}
s.InvalidateMemberCount(member.ChannelId)
return member, nil
}
func (s LocalCacheChannelStore) SaveMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
members, err := s.ChannelStore.SaveMultipleMembers(members)
if err != nil {
return nil, err
}
for _, member := range members {
s.InvalidateMemberCount(member.ChannelId)
}
return members, nil
}
func (s LocalCacheChannelStore) UpdateMember(member *model.ChannelMember) (*model.ChannelMember, error) {
member, err := s.ChannelStore.UpdateMember(member)
if err != nil {
return nil, err
}
s.InvalidateMemberCount(member.ChannelId)
return member, nil
}
func (s LocalCacheChannelStore) UpdateMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
members, err := s.ChannelStore.UpdateMultipleMembers(members)
if err != nil {
return nil, err
}
for _, member := range members {
s.InvalidateMemberCount(member.ChannelId)
}
return members, nil
}
func (s LocalCacheChannelStore) RemoveMember(channelId, userId string) error {
err := s.ChannelStore.RemoveMember(channelId, userId)
if err != nil {
return err
}
s.InvalidateMemberCount(channelId)
return nil
}
func (s LocalCacheChannelStore) RemoveMembers(channelId string, userIds []string) error {
err := s.ChannelStore.RemoveMembers(channelId, userIds)
if err != nil {
return err
}
s.InvalidateMemberCount(channelId)
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"context"
"sync"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
)
type LocalCacheEmojiStore struct {
store.EmojiStore
rootStore *LocalCacheStore
emojiByIdMut sync.Mutex
emojiByIdInvalidations map[string]bool
emojiByNameMut sync.Mutex
emojiByNameInvalidations map[string]bool
}
func (es *LocalCacheEmojiStore) handleClusterInvalidateEmojiById(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
es.rootStore.emojiCacheById.Purge()
} else {
es.emojiByIdMut.Lock()
es.emojiByIdInvalidations[string(msg.Data)] = true
es.emojiByIdMut.Unlock()
es.rootStore.emojiCacheById.Remove(string(msg.Data))
}
}
func (es *LocalCacheEmojiStore) handleClusterInvalidateEmojiIdByName(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
es.rootStore.emojiIdCacheByName.Purge()
} else {
es.emojiByNameMut.Lock()
es.emojiByNameInvalidations[string(msg.Data)] = true
es.emojiByNameMut.Unlock()
es.rootStore.emojiIdCacheByName.Remove(string(msg.Data))
}
}
func (es *LocalCacheEmojiStore) Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error) {
if allowFromCache {
if emoji, ok := es.getFromCacheById(id); ok {
return emoji, nil
}
}
// If it was invalidated, then we need to query master.
es.emojiByIdMut.Lock()
if es.emojiByIdInvalidations[id] {
// And then remove the key from the map.
ctx = sqlstore.WithMaster(ctx)
delete(es.emojiByIdInvalidations, id)
}
es.emojiByIdMut.Unlock()
emoji, err := es.EmojiStore.Get(ctx, id, allowFromCache)
if allowFromCache && err == nil {
es.addToCache(emoji)
}
return emoji, err
}
func (es *LocalCacheEmojiStore) GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error) {
if id, ok := model.GetSystemEmojiId(name); ok {
return es.Get(ctx, id, allowFromCache)
}
if allowFromCache {
if emoji, ok := es.getFromCacheByName(name); ok {
return emoji, nil
}
}
// If it was invalidated, then we need to query master.
es.emojiByNameMut.Lock()
if es.emojiByNameInvalidations[name] {
ctx = sqlstore.WithMaster(ctx)
// And then remove the key from the map.
delete(es.emojiByNameInvalidations, name)
}
es.emojiByNameMut.Unlock()
emoji, err := es.EmojiStore.GetByName(ctx, name, allowFromCache)
if allowFromCache && err == nil {
es.addToCache(emoji)
}
return emoji, err
}
func (es *LocalCacheEmojiStore) Delete(emoji *model.Emoji, time int64) error {
err := es.EmojiStore.Delete(emoji, time)
if err == nil {
es.removeFromCache(emoji)
}
return err
}
func (es *LocalCacheEmojiStore) addToCache(emoji *model.Emoji) {
es.rootStore.doStandardAddToCache(es.rootStore.emojiCacheById, emoji.Id, emoji)
es.rootStore.doStandardAddToCache(es.rootStore.emojiIdCacheByName, emoji.Name, emoji.Id)
}
func (es *LocalCacheEmojiStore) getFromCacheById(id string) (*model.Emoji, bool) {
var emoji *model.Emoji
if err := es.rootStore.doStandardReadCache(es.rootStore.emojiCacheById, id, &emoji); err == nil {
return emoji, true
}
return nil, false
}
func (es *LocalCacheEmojiStore) getFromCacheByName(name string) (*model.Emoji, bool) {
var emojiId string
if err := es.rootStore.doStandardReadCache(es.rootStore.emojiIdCacheByName, name, &emojiId); err == nil {
return es.getFromCacheById(emojiId)
}
return nil, false
}
func (es *LocalCacheEmojiStore) removeFromCache(emoji *model.Emoji) {
es.emojiByIdMut.Lock()
es.emojiByIdInvalidations[emoji.Id] = true
es.emojiByIdMut.Unlock()
es.rootStore.doInvalidateCacheCluster(es.rootStore.emojiCacheById, emoji.Id)
es.emojiByNameMut.Lock()
es.emojiByNameInvalidations[emoji.Name] = true
es.emojiByNameMut.Unlock()
es.rootStore.doInvalidateCacheCluster(es.rootStore.emojiIdCacheByName, emoji.Name)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type LocalCacheFileInfoStore struct {
store.FileInfoStore
rootStore *LocalCacheStore
}
func (s *LocalCacheFileInfoStore) handleClusterInvalidateFileInfo(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.fileInfoCache.Purge()
return
}
s.rootStore.fileInfoCache.Remove(string(msg.Data))
}
func (s LocalCacheFileInfoStore) GetForPost(postId string, readFromMaster, includeDeleted, allowFromCache bool) ([]*model.FileInfo, error) {
if !allowFromCache {
return s.FileInfoStore.GetForPost(postId, readFromMaster, includeDeleted, allowFromCache)
}
cacheKey := postId
if includeDeleted {
cacheKey += "_deleted"
}
var fileInfo []*model.FileInfo
if err := s.rootStore.doStandardReadCache(s.rootStore.fileInfoCache, cacheKey, &fileInfo); err == nil {
return fileInfo, nil
}
fileInfos, err := s.FileInfoStore.GetForPost(postId, readFromMaster, includeDeleted, allowFromCache)
if err != nil {
return nil, err
}
if len(fileInfos) > 0 {
s.rootStore.doStandardAddToCache(s.rootStore.fileInfoCache, cacheKey, fileInfos)
}
return fileInfos, nil
}
func (s LocalCacheFileInfoStore) ClearCaches() {
s.rootStore.fileInfoCache.Purge()
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("File Info Cache - Purge")
}
}
func (s LocalCacheFileInfoStore) InvalidateFileInfosForPostCache(postId string, deleted bool) {
cacheKey := postId
if deleted {
cacheKey += "_deleted"
}
s.rootStore.doInvalidateCacheCluster(s.rootStore.fileInfoCache, cacheKey)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("File Info Cache - Remove by PostId")
}
}
func (s LocalCacheFileInfoStore) GetStorageUsage(allowFromCache, includeDeleted bool) (int64, error) {
storageUsageKey := "storage_usage"
if includeDeleted {
storageUsageKey += "_deleted"
}
if !allowFromCache {
usage, err := s.FileInfoStore.GetStorageUsage(allowFromCache, includeDeleted)
if err != nil {
return 0, err
}
s.rootStore.doStandardAddToCache(s.rootStore.fileInfoCache, storageUsageKey, usage)
return usage, nil
}
var usage int64
if err := s.rootStore.doStandardReadCache(s.rootStore.fileInfoCache, storageUsageKey, &usage); err == nil {
return usage, nil
}
usage, err := s.FileInfoStore.GetStorageUsage(allowFromCache, includeDeleted)
if err != nil {
return 0, err
}
s.rootStore.doStandardAddToCache(s.rootStore.fileInfoCache, storageUsageKey, usage)
return usage, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"runtime"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/cache"
)
const (
ReactionCacheSize = 20000
ReactionCacheSec = 30 * 60
RoleCacheSize = 20000
RoleCacheSec = 30 * 60
SchemeCacheSize = 20000
SchemeCacheSec = 30 * 60
FileInfoCacheSize = 25000
FileInfoCacheSec = 30 * 60
ChannelGuestCountCacheSize = model.ChannelCacheSize
ChannelGuestCountCacheSec = 30 * 60
WebhookCacheSize = 25000
WebhookCacheSec = 15 * 60
EmojiCacheSize = 5000
EmojiCacheSec = 30 * 60
ChannelPinnedPostsCountsCacheSize = model.ChannelCacheSize
ChannelPinnedPostsCountsCacheSec = 30 * 60
ChannelMembersCountsCacheSize = model.ChannelCacheSize
ChannelMembersCountsCacheSec = 30 * 60
LastPostsCacheSize = 20000
LastPostsCacheSec = 30 * 60
PostsUsageCacheSize = 1
PostsUsageCacheSec = 30 * 60
TermsOfServiceCacheSize = 20000
TermsOfServiceCacheSec = 30 * 60
LastPostTimeCacheSize = 25000
LastPostTimeCacheSec = 15 * 60
UserProfileByIDCacheSize = 20000
UserProfileByIDSec = 30 * 60
ProfilesInChannelCacheSize = model.ChannelCacheSize
ProfilesInChannelCacheSec = 15 * 60
TeamCacheSize = 20000
TeamCacheSec = 30 * 60
ChannelCacheSec = 15 * 60 // 15 mins
)
var clearCacheMessageData = []byte("")
type LocalCacheStore struct {
store.Store
metrics einterfaces.MetricsInterface
cluster einterfaces.ClusterInterface
reaction LocalCacheReactionStore
reactionCache cache.Cache
fileInfo LocalCacheFileInfoStore
fileInfoCache cache.Cache
role LocalCacheRoleStore
roleCache cache.Cache
rolePermissionsCache cache.Cache
scheme LocalCacheSchemeStore
schemeCache cache.Cache
emoji *LocalCacheEmojiStore
emojiCacheById cache.Cache
emojiIdCacheByName cache.Cache
channel LocalCacheChannelStore
channelMemberCountsCache cache.Cache
channelGuestCountCache cache.Cache
channelPinnedPostCountsCache cache.Cache
channelByIdCache cache.Cache
webhook LocalCacheWebhookStore
webhookCache cache.Cache
post LocalCachePostStore
postLastPostsCache cache.Cache
lastPostTimeCache cache.Cache
postsUsageCache cache.Cache
user *LocalCacheUserStore
userProfileByIdsCache cache.Cache
profilesInChannelCache cache.Cache
team LocalCacheTeamStore
teamAllTeamIdsForUserCache cache.Cache
termsOfService LocalCacheTermsOfServiceStore
termsOfServiceCache cache.Cache
}
func NewLocalCacheLayer(baseStore store.Store, metrics einterfaces.MetricsInterface, cluster einterfaces.ClusterInterface, cacheProvider cache.Provider) (localCacheStore LocalCacheStore, err error) {
localCacheStore = LocalCacheStore{
Store: baseStore,
cluster: cluster,
metrics: metrics,
}
// Reactions
if localCacheStore.reactionCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: ReactionCacheSize,
Name: "Reaction",
DefaultExpiry: ReactionCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForReactions,
}); err != nil {
return
}
localCacheStore.reaction = LocalCacheReactionStore{ReactionStore: baseStore.Reaction(), rootStore: &localCacheStore}
// Roles
if localCacheStore.roleCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: RoleCacheSize,
Name: "Role",
DefaultExpiry: RoleCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForRoles,
Striped: true,
StripedBuckets: maxInt(runtime.NumCPU()-1, 1),
}); err != nil {
return
}
if localCacheStore.rolePermissionsCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: RoleCacheSize,
Name: "RolePermission",
DefaultExpiry: RoleCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForRolePermissions,
}); err != nil {
return
}
localCacheStore.role = LocalCacheRoleStore{RoleStore: baseStore.Role(), rootStore: &localCacheStore}
// Schemes
if localCacheStore.schemeCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: SchemeCacheSize,
Name: "Scheme",
DefaultExpiry: SchemeCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForSchemes,
}); err != nil {
return
}
localCacheStore.scheme = LocalCacheSchemeStore{SchemeStore: baseStore.Scheme(), rootStore: &localCacheStore}
// FileInfo
if localCacheStore.fileInfoCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: FileInfoCacheSize,
Name: "FileInfo",
DefaultExpiry: FileInfoCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForFileInfos,
}); err != nil {
return
}
localCacheStore.fileInfo = LocalCacheFileInfoStore{FileInfoStore: baseStore.FileInfo(), rootStore: &localCacheStore}
// Webhooks
if localCacheStore.webhookCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: WebhookCacheSize,
Name: "Webhook",
DefaultExpiry: WebhookCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForWebhooks,
}); err != nil {
return
}
localCacheStore.webhook = LocalCacheWebhookStore{WebhookStore: baseStore.Webhook(), rootStore: &localCacheStore}
// Emojis
if localCacheStore.emojiCacheById, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: EmojiCacheSize,
Name: "EmojiById",
DefaultExpiry: EmojiCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForEmojisById,
}); err != nil {
return
}
if localCacheStore.emojiIdCacheByName, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: EmojiCacheSize,
Name: "EmojiByName",
DefaultExpiry: EmojiCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForEmojisIdByName,
}); err != nil {
return
}
localCacheStore.emoji = &LocalCacheEmojiStore{
EmojiStore: baseStore.Emoji(),
rootStore: &localCacheStore,
emojiByIdInvalidations: make(map[string]bool),
emojiByNameInvalidations: make(map[string]bool),
}
// Channels
if localCacheStore.channelPinnedPostCountsCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: ChannelPinnedPostsCountsCacheSize,
Name: "ChannelPinnedPostsCounts",
DefaultExpiry: ChannelPinnedPostsCountsCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForChannelPinnedpostsCounts,
}); err != nil {
return
}
if localCacheStore.channelMemberCountsCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: ChannelMembersCountsCacheSize,
Name: "ChannelMemberCounts",
DefaultExpiry: ChannelMembersCountsCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForChannelMemberCounts,
}); err != nil {
return
}
if localCacheStore.channelGuestCountCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: ChannelGuestCountCacheSize,
Name: "ChannelGuestsCount",
DefaultExpiry: ChannelGuestCountCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForChannelGuestCount,
}); err != nil {
return
}
if localCacheStore.channelByIdCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: model.ChannelCacheSize,
Name: "channelById",
DefaultExpiry: ChannelCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForChannel,
}); err != nil {
return
}
localCacheStore.channel = LocalCacheChannelStore{ChannelStore: baseStore.Channel(), rootStore: &localCacheStore}
// Posts
if localCacheStore.postLastPostsCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: LastPostsCacheSize,
Name: "LastPost",
DefaultExpiry: LastPostsCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForLastPosts,
}); err != nil {
return
}
if localCacheStore.lastPostTimeCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: LastPostTimeCacheSize,
Name: "LastPostTime",
DefaultExpiry: LastPostTimeCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForLastPostTime,
}); err != nil {
return
}
if localCacheStore.postsUsageCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: PostsUsageCacheSize,
Name: "PostsUsage",
DefaultExpiry: PostsUsageCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForPostsUsage,
}); err != nil {
return
}
localCacheStore.post = LocalCachePostStore{PostStore: baseStore.Post(), rootStore: &localCacheStore}
// TOS
if localCacheStore.termsOfServiceCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: TermsOfServiceCacheSize,
Name: "TermsOfService",
DefaultExpiry: TermsOfServiceCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForTermsOfService,
}); err != nil {
return
}
localCacheStore.termsOfService = LocalCacheTermsOfServiceStore{TermsOfServiceStore: baseStore.TermsOfService(), rootStore: &localCacheStore}
// Users
if localCacheStore.userProfileByIdsCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: UserProfileByIDCacheSize,
Name: "UserProfileByIds",
DefaultExpiry: UserProfileByIDSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForProfileByIds,
Striped: true,
StripedBuckets: maxInt(runtime.NumCPU()-1, 1),
}); err != nil {
return
}
if localCacheStore.profilesInChannelCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: ProfilesInChannelCacheSize,
Name: "ProfilesInChannel",
DefaultExpiry: ProfilesInChannelCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForProfileInChannel,
}); err != nil {
return
}
localCacheStore.user = &LocalCacheUserStore{
UserStore: baseStore.User(),
rootStore: &localCacheStore,
userProfileByIdsInvalidations: make(map[string]bool),
}
// Teams
if localCacheStore.teamAllTeamIdsForUserCache, err = cacheProvider.NewCache(&cache.CacheOptions{
Size: TeamCacheSize,
Name: "Team",
DefaultExpiry: TeamCacheSec * time.Second,
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForTeams,
}); err != nil {
return
}
localCacheStore.team = LocalCacheTeamStore{TeamStore: baseStore.Team(), rootStore: &localCacheStore}
if cluster != nil {
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForReactions, localCacheStore.reaction.handleClusterInvalidateReaction)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForRoles, localCacheStore.role.handleClusterInvalidateRole)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForRolePermissions, localCacheStore.role.handleClusterInvalidateRolePermissions)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForSchemes, localCacheStore.scheme.handleClusterInvalidateScheme)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForFileInfos, localCacheStore.fileInfo.handleClusterInvalidateFileInfo)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForLastPostTime, localCacheStore.post.handleClusterInvalidateLastPostTime)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForPostsUsage, localCacheStore.post.handleClusterInvalidatePostsUsage)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForWebhooks, localCacheStore.webhook.handleClusterInvalidateWebhook)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForEmojisById, localCacheStore.emoji.handleClusterInvalidateEmojiById)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForEmojisIdByName, localCacheStore.emoji.handleClusterInvalidateEmojiIdByName)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannelPinnedpostsCounts, localCacheStore.channel.handleClusterInvalidateChannelPinnedPostCount)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannelMemberCounts, localCacheStore.channel.handleClusterInvalidateChannelMemberCounts)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannelGuestCount, localCacheStore.channel.handleClusterInvalidateChannelGuestCounts)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannel, localCacheStore.channel.handleClusterInvalidateChannelById)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForLastPosts, localCacheStore.post.handleClusterInvalidateLastPosts)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForTermsOfService, localCacheStore.termsOfService.handleClusterInvalidateTermsOfService)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForProfileByIds, localCacheStore.user.handleClusterInvalidateScheme)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForProfileInChannel, localCacheStore.user.handleClusterInvalidateProfilesInChannel)
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForTeams, localCacheStore.team.handleClusterInvalidateTeam)
}
return
}
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}
func (s LocalCacheStore) Reaction() store.ReactionStore {
return s.reaction
}
func (s LocalCacheStore) Role() store.RoleStore {
return s.role
}
func (s LocalCacheStore) Scheme() store.SchemeStore {
return s.scheme
}
func (s LocalCacheStore) FileInfo() store.FileInfoStore {
return s.fileInfo
}
func (s LocalCacheStore) Webhook() store.WebhookStore {
return s.webhook
}
func (s LocalCacheStore) Emoji() store.EmojiStore {
return s.emoji
}
func (s LocalCacheStore) Channel() store.ChannelStore {
return s.channel
}
func (s LocalCacheStore) Post() store.PostStore {
return s.post
}
func (s LocalCacheStore) TermsOfService() store.TermsOfServiceStore {
return s.termsOfService
}
func (s LocalCacheStore) User() store.UserStore {
return s.user
}
func (s LocalCacheStore) Team() store.TeamStore {
return s.team
}
func (s LocalCacheStore) DropAllTables() {
s.Invalidate()
s.Store.DropAllTables()
}
func (s *LocalCacheStore) doInvalidateCacheCluster(cache cache.Cache, key string) {
cache.Remove(key)
if s.cluster != nil {
msg := &model.ClusterMessage{
Event: cache.GetInvalidateClusterEvent(),
SendType: model.ClusterSendBestEffort,
Data: []byte(key),
}
s.cluster.SendClusterMessage(msg)
}
}
func (s *LocalCacheStore) doStandardAddToCache(cache cache.Cache, key string, value any) {
cache.SetWithDefaultExpiry(key, value)
}
func (s *LocalCacheStore) doStandardReadCache(cache cache.Cache, key string, value any) error {
err := cache.Get(key, value)
if err == nil {
if s.metrics != nil {
s.metrics.IncrementMemCacheHitCounter(cache.Name())
}
return nil
}
if s.metrics != nil {
s.metrics.IncrementMemCacheMissCounter(cache.Name())
}
return err
}
func (s *LocalCacheStore) doClearCacheCluster(cache cache.Cache) {
cache.Purge()
if s.cluster != nil {
msg := &model.ClusterMessage{
Event: cache.GetInvalidateClusterEvent(),
SendType: model.ClusterSendBestEffort,
Data: clearCacheMessageData,
}
s.cluster.SendClusterMessage(msg)
}
}
func (s *LocalCacheStore) Invalidate() {
s.doClearCacheCluster(s.reactionCache)
s.doClearCacheCluster(s.schemeCache)
s.doClearCacheCluster(s.roleCache)
s.doClearCacheCluster(s.fileInfoCache)
s.doClearCacheCluster(s.webhookCache)
s.doClearCacheCluster(s.emojiCacheById)
s.doClearCacheCluster(s.emojiIdCacheByName)
s.doClearCacheCluster(s.channelMemberCountsCache)
s.doClearCacheCluster(s.channelPinnedPostCountsCache)
s.doClearCacheCluster(s.channelGuestCountCache)
s.doClearCacheCluster(s.channelByIdCache)
s.doClearCacheCluster(s.postLastPostsCache)
s.doClearCacheCluster(s.termsOfServiceCache)
s.doClearCacheCluster(s.lastPostTimeCache)
s.doClearCacheCluster(s.userProfileByIdsCache)
s.doClearCacheCluster(s.profilesInChannelCache)
s.doClearCacheCluster(s.teamAllTeamIdsForUserCache)
s.doClearCacheCluster(s.rolePermissionsCache)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"fmt"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type LocalCachePostStore struct {
store.PostStore
rootStore *LocalCacheStore
}
func (s *LocalCachePostStore) handleClusterInvalidateLastPostTime(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.lastPostTimeCache.Purge()
} else {
s.rootStore.lastPostTimeCache.Remove(string(msg.Data))
}
}
func (s *LocalCachePostStore) handleClusterInvalidateLastPosts(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.postLastPostsCache.Purge()
} else {
s.rootStore.postLastPostsCache.Remove(string(msg.Data))
}
}
func (s *LocalCachePostStore) handleClusterInvalidatePostsUsage(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.postsUsageCache.Purge()
} else {
s.rootStore.postsUsageCache.Remove(string(msg.Data))
}
}
func (s LocalCachePostStore) ClearCaches() {
s.rootStore.doClearCacheCluster(s.rootStore.lastPostTimeCache)
s.rootStore.doClearCacheCluster(s.rootStore.postLastPostsCache)
s.rootStore.doClearCacheCluster(s.rootStore.postsUsageCache)
s.PostStore.ClearCaches()
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Last Post Time - Purge")
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Last Posts Cache - Purge")
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Posts Usage Cache - Purge")
}
}
func (s LocalCachePostStore) InvalidateLastPostTimeCache(channelId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.lastPostTimeCache, channelId)
// Keys are "{channelid}{limit}" and caching only occurs on limits of 30 and 60
s.rootStore.doInvalidateCacheCluster(s.rootStore.postLastPostsCache, channelId+"30")
s.rootStore.doInvalidateCacheCluster(s.rootStore.postLastPostsCache, channelId+"60")
s.PostStore.InvalidateLastPostTimeCache(channelId)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Last Post Time - Remove by Channel Id")
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Last Posts Cache - Remove by Channel Id")
}
}
func (s LocalCachePostStore) GetEtag(channelId string, allowFromCache, collapsedThreads bool) string {
if allowFromCache {
var lastTime int64
if err := s.rootStore.doStandardReadCache(s.rootStore.lastPostTimeCache, channelId, &lastTime); err == nil {
return fmt.Sprintf("%v.%v", model.CurrentVersion, lastTime)
}
}
result := s.PostStore.GetEtag(channelId, allowFromCache, collapsedThreads)
splittedResult := strings.Split(result, ".")
lastTime, _ := strconv.ParseInt((splittedResult[len(splittedResult)-1]), 10, 64)
s.rootStore.doStandardAddToCache(s.rootStore.lastPostTimeCache, channelId, lastTime)
return result
}
func (s LocalCachePostStore) GetPostsSince(options model.GetPostsSinceOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
if allowFromCache {
// If the last post in the channel's time is less than or equal to the time we are getting posts since,
// we can safely return no posts.
var lastTime int64
if err := s.rootStore.doStandardReadCache(s.rootStore.lastPostTimeCache, options.ChannelId, &lastTime); err == nil && lastTime <= options.Time {
list := model.NewPostList()
return list, nil
}
}
list, err := s.PostStore.GetPostsSince(options, allowFromCache, sanitizeOptions)
latestUpdate := options.Time
if err == nil {
for _, p := range list.ToSlice() {
if latestUpdate < p.UpdateAt {
latestUpdate = p.UpdateAt
}
}
s.rootStore.doStandardAddToCache(s.rootStore.lastPostTimeCache, options.ChannelId, latestUpdate)
}
return list, err
}
func (s LocalCachePostStore) GetPosts(options model.GetPostsOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
if !allowFromCache {
return s.PostStore.GetPosts(options, allowFromCache, sanitizeOptions)
}
offset := options.PerPage * options.Page
// Caching only occurs on limits of 30 and 60, the common limits requested by MM clients
if offset == 0 && (options.PerPage == 60 || options.PerPage == 30) {
var cacheItem *model.PostList
if err := s.rootStore.doStandardReadCache(s.rootStore.postLastPostsCache, fmt.Sprintf("%s%v", options.ChannelId, options.PerPage), &cacheItem); err == nil {
return cacheItem, nil
}
}
list, err := s.PostStore.GetPosts(options, false, sanitizeOptions)
if err != nil {
return nil, err
}
// Caching only occurs on limits of 30 and 60, the common limits requested by MM clients
if offset == 0 && (options.PerPage == 60 || options.PerPage == 30) {
s.rootStore.doStandardAddToCache(s.rootStore.postLastPostsCache, fmt.Sprintf("%s%v", options.ChannelId, options.PerPage), list)
}
return list, err
}
// AnalyticsPostCount looks up cache only when ExcludeDeleted and UsersPostsOnly are true and rest are falsy.
func (s LocalCachePostStore) AnalyticsPostCount(options *model.PostCountOptions) (int64, error) {
if !options.AllowFromCache || options.MustHaveFile || options.MustHaveHashtag || !options.UsersPostsOnly || !options.ExcludeDeleted || options.TeamId != "" {
return s.PostStore.AnalyticsPostCount(options)
}
// Currently cache only for app > usage > GetPostsUsage()
// Other filter combinations can be cached if required
cacheKey := "posts_usage"
var count int64
if err := s.rootStore.doStandardReadCache(s.rootStore.postsUsageCache, cacheKey, &count); err == nil {
return count, nil
}
count, err := s.PostStore.AnalyticsPostCount(options)
if err != nil {
return 0, err
}
s.rootStore.doStandardAddToCache(s.rootStore.postsUsageCache, cacheKey, count)
return count, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type LocalCacheReactionStore struct {
store.ReactionStore
rootStore *LocalCacheStore
}
func (s *LocalCacheReactionStore) handleClusterInvalidateReaction(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.reactionCache.Purge()
} else {
s.rootStore.reactionCache.Remove(string(msg.Data))
}
}
func (s LocalCacheReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) {
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.reactionCache, reaction.PostId)
return s.ReactionStore.Save(reaction)
}
func (s LocalCacheReactionStore) Delete(reaction *model.Reaction) (*model.Reaction, error) {
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.reactionCache, reaction.PostId)
return s.ReactionStore.Delete(reaction)
}
func (s LocalCacheReactionStore) GetForPost(postId string, allowFromCache bool) ([]*model.Reaction, error) {
if !allowFromCache {
return s.ReactionStore.GetForPost(postId, false)
}
var reaction []*model.Reaction
if err := s.rootStore.doStandardReadCache(s.rootStore.reactionCache, postId, &reaction); err == nil {
return reaction, nil
}
reaction, err := s.ReactionStore.GetForPost(postId, false)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.reactionCache, postId, reaction)
return reaction, nil
}
func (s LocalCacheReactionStore) DeleteAllWithEmojiName(emojiName string) error {
// This could be improved. Right now we just clear the whole
// cache because we don't have a way find what post Ids have this emoji name.
defer s.rootStore.doClearCacheCluster(s.rootStore.reactionCache)
return s.ReactionStore.DeleteAllWithEmojiName(emojiName)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"context"
"sort"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type LocalCacheRoleStore struct {
store.RoleStore
rootStore *LocalCacheStore
}
func (s *LocalCacheRoleStore) handleClusterInvalidateRole(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.roleCache.Purge()
} else {
s.rootStore.roleCache.Remove(string(msg.Data))
}
}
func (s *LocalCacheRoleStore) handleClusterInvalidateRolePermissions(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.rolePermissionsCache.Purge()
} else {
s.rootStore.rolePermissionsCache.Remove(string(msg.Data))
}
}
func (s LocalCacheRoleStore) Save(role *model.Role) (*model.Role, error) {
if role.Name != "" {
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.roleCache, role.Name)
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
}
return s.RoleStore.Save(role)
}
func (s LocalCacheRoleStore) GetByName(ctx context.Context, name string) (*model.Role, error) {
var role *model.Role
if err := s.rootStore.doStandardReadCache(s.rootStore.roleCache, name, &role); err == nil {
return role, nil
}
role, err := s.RoleStore.GetByName(ctx, name)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.roleCache, name, role)
return role, nil
}
func (s LocalCacheRoleStore) GetByNames(names []string) ([]*model.Role, error) {
var foundRoles []*model.Role
var rolesToQuery []string
for _, roleName := range names {
var role *model.Role
if err := s.rootStore.doStandardReadCache(s.rootStore.roleCache, roleName, &role); err == nil {
foundRoles = append(foundRoles, role)
} else {
rolesToQuery = append(rolesToQuery, roleName)
}
}
roles, err := s.RoleStore.GetByNames(rolesToQuery)
if err != nil {
return nil, err
}
for _, role := range roles {
s.rootStore.doStandardAddToCache(s.rootStore.roleCache, role.Name, role)
}
return append(foundRoles, roles...), nil
}
func (s LocalCacheRoleStore) Delete(roleId string) (*model.Role, error) {
role, err := s.RoleStore.Delete(roleId)
if err == nil {
s.rootStore.doInvalidateCacheCluster(s.rootStore.roleCache, role.Name)
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
}
return role, err
}
func (s LocalCacheRoleStore) PermanentDeleteAll() error {
defer s.rootStore.roleCache.Purge()
defer s.rootStore.doClearCacheCluster(s.rootStore.roleCache)
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
return s.RoleStore.PermanentDeleteAll()
}
func (s LocalCacheRoleStore) ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, error) {
sort.Strings(roleNames)
cacheKey := strings.Join(roleNames, "/")
var rolePermissionsMap map[string]*model.RolePermissions
if err := s.rootStore.doStandardReadCache(s.rootStore.rolePermissionsCache, cacheKey, &rolePermissionsMap); err == nil {
return rolePermissionsMap, nil
}
rolePermissionsMap, err := s.RoleStore.ChannelHigherScopedPermissions(roleNames)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.rolePermissionsCache, cacheKey, rolePermissionsMap)
return rolePermissionsMap, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type LocalCacheSchemeStore struct {
store.SchemeStore
rootStore *LocalCacheStore
}
func (s *LocalCacheSchemeStore) handleClusterInvalidateScheme(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.schemeCache.Purge()
} else {
s.rootStore.schemeCache.Remove(string(msg.Data))
}
}
func (s LocalCacheSchemeStore) Save(scheme *model.Scheme) (*model.Scheme, error) {
if scheme.Id != "" {
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.schemeCache, scheme.Id)
}
return s.SchemeStore.Save(scheme)
}
func (s LocalCacheSchemeStore) Get(schemeId string) (*model.Scheme, error) {
var scheme *model.Scheme
if err := s.rootStore.doStandardReadCache(s.rootStore.schemeCache, schemeId, &scheme); err == nil {
return scheme, nil
}
scheme, err := s.SchemeStore.Get(schemeId)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.schemeCache, schemeId, scheme)
return scheme, nil
}
func (s LocalCacheSchemeStore) Delete(schemeId string) (*model.Scheme, error) {
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.schemeCache, schemeId)
defer s.rootStore.doClearCacheCluster(s.rootStore.roleCache)
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
return s.SchemeStore.Delete(schemeId)
}
func (s LocalCacheSchemeStore) PermanentDeleteAll() error {
defer s.rootStore.doClearCacheCluster(s.rootStore.schemeCache)
defer s.rootStore.doClearCacheCluster(s.rootStore.roleCache)
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
return s.SchemeStore.PermanentDeleteAll()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type LocalCacheTeamStore struct {
store.TeamStore
rootStore *LocalCacheStore
}
func (s *LocalCacheTeamStore) handleClusterInvalidateTeam(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.teamAllTeamIdsForUserCache.Purge()
} else {
s.rootStore.teamAllTeamIdsForUserCache.Remove(string(msg.Data))
}
}
func (s LocalCacheTeamStore) ClearCaches() {
s.rootStore.teamAllTeamIdsForUserCache.Purge()
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("All Team Ids for User - Purge")
}
}
func (s LocalCacheTeamStore) InvalidateAllTeamIdsForUser(userId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.teamAllTeamIdsForUserCache, userId)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("All Team Ids for User - Remove by UserId")
}
}
func (s LocalCacheTeamStore) GetUserTeamIds(userID string, allowFromCache bool) ([]string, error) {
if !allowFromCache {
return s.TeamStore.GetUserTeamIds(userID, allowFromCache)
}
var userTeamIds []string
if err := s.rootStore.doStandardReadCache(s.rootStore.teamAllTeamIdsForUserCache, userID, &userTeamIds); err == nil {
return userTeamIds, nil
}
userTeamIds, err := s.TeamStore.GetUserTeamIds(userID, allowFromCache)
if err != nil {
return nil, err
}
if len(userTeamIds) > 0 {
s.rootStore.doStandardAddToCache(s.rootStore.teamAllTeamIdsForUserCache, userID, userTeamIds)
}
return userTeamIds, nil
}
func (s LocalCacheTeamStore) Update(team *model.Team) (*model.Team, error) {
var oldTeam *model.Team
var err error
if team.DeleteAt != 0 {
oldTeam, err = s.TeamStore.Get(team.Id)
if err != nil {
return nil, err
}
}
tm, err := s.TeamStore.Update(team)
if err != nil {
return nil, err
}
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
if oldTeam != nil && oldTeam.DeleteAt == 0 {
s.rootStore.doClearCacheCluster(s.rootStore.teamAllTeamIdsForUserCache)
}
return tm, err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const (
LatestKey = "latest"
)
type LocalCacheTermsOfServiceStore struct {
store.TermsOfServiceStore
rootStore *LocalCacheStore
}
func (s *LocalCacheTermsOfServiceStore) handleClusterInvalidateTermsOfService(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.termsOfServiceCache.Purge()
} else {
s.rootStore.termsOfServiceCache.Remove(string(msg.Data))
}
}
func (s LocalCacheTermsOfServiceStore) ClearCaches() {
s.rootStore.doClearCacheCluster(s.rootStore.termsOfServiceCache)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Terms Of Service - Purge")
}
}
func (s LocalCacheTermsOfServiceStore) Save(termsOfService *model.TermsOfService) (*model.TermsOfService, error) {
tos, err := s.TermsOfServiceStore.Save(termsOfService)
if err == nil {
s.rootStore.doStandardAddToCache(s.rootStore.termsOfServiceCache, tos.Id, tos)
s.rootStore.doInvalidateCacheCluster(s.rootStore.termsOfServiceCache, LatestKey)
}
return tos, err
}
func (s LocalCacheTermsOfServiceStore) GetLatest(allowFromCache bool) (*model.TermsOfService, error) {
if allowFromCache {
if len, err := s.rootStore.termsOfServiceCache.Len(); err == nil && len != 0 {
var cacheItem *model.TermsOfService
if err := s.rootStore.doStandardReadCache(s.rootStore.termsOfServiceCache, LatestKey, &cacheItem); err == nil {
return cacheItem, nil
}
}
}
termsOfService, err := s.TermsOfServiceStore.GetLatest(allowFromCache)
if allowFromCache && err == nil {
s.rootStore.doStandardAddToCache(s.rootStore.termsOfServiceCache, termsOfService.Id, termsOfService)
s.rootStore.doStandardAddToCache(s.rootStore.termsOfServiceCache, LatestKey, termsOfService)
}
return termsOfService, err
}
func (s LocalCacheTermsOfServiceStore) Get(id string, allowFromCache bool) (*model.TermsOfService, error) {
if allowFromCache {
var cacheItem *model.TermsOfService
if err := s.rootStore.doStandardReadCache(s.rootStore.termsOfServiceCache, id, &cacheItem); err == nil {
return cacheItem, nil
}
}
termsOfService, err := s.TermsOfServiceStore.Get(id, allowFromCache)
if allowFromCache && err == nil {
s.rootStore.doStandardAddToCache(s.rootStore.termsOfServiceCache, termsOfService.Id, termsOfService)
}
return termsOfService, err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"context"
"sort"
"sync"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
)
type LocalCacheUserStore struct {
store.UserStore
rootStore *LocalCacheStore
userProfileByIdsMut sync.Mutex
userProfileByIdsInvalidations map[string]bool
}
func (s *LocalCacheUserStore) handleClusterInvalidateScheme(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.userProfileByIdsCache.Purge()
} else {
s.userProfileByIdsMut.Lock()
s.userProfileByIdsInvalidations[string(msg.Data)] = true
s.userProfileByIdsMut.Unlock()
s.rootStore.userProfileByIdsCache.Remove(string(msg.Data))
}
}
func (s *LocalCacheUserStore) handleClusterInvalidateProfilesInChannel(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.profilesInChannelCache.Purge()
} else {
s.rootStore.profilesInChannelCache.Remove(string(msg.Data))
}
}
func (s *LocalCacheUserStore) ClearCaches() {
s.rootStore.userProfileByIdsCache.Purge()
s.rootStore.profilesInChannelCache.Purge()
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Profile By Ids - Purge")
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Profiles in Channel - Purge")
}
}
func (s *LocalCacheUserStore) InvalidateProfileCacheForUser(userId string) {
s.userProfileByIdsMut.Lock()
s.userProfileByIdsInvalidations[userId] = true
s.userProfileByIdsMut.Unlock()
s.rootStore.doInvalidateCacheCluster(s.rootStore.userProfileByIdsCache, userId)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Profile By Ids - Remove")
}
}
func (s *LocalCacheUserStore) InvalidateProfilesInChannelCacheByUser(userId string) {
keys, err := s.rootStore.profilesInChannelCache.Keys()
if err == nil {
for _, key := range keys {
var userMap map[string]*model.User
if err = s.rootStore.profilesInChannelCache.Get(key, &userMap); err == nil {
if _, userInCache := userMap[userId]; userInCache {
s.rootStore.doInvalidateCacheCluster(s.rootStore.profilesInChannelCache, key)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Profiles in Channel - Remove by User")
}
}
}
}
}
}
func (s *LocalCacheUserStore) InvalidateProfilesInChannelCache(channelID string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.profilesInChannelCache, channelID)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Profiles in Channel - Remove by Channel")
}
}
func (s *LocalCacheUserStore) GetAllProfilesInChannel(ctx context.Context, channelId string, allowFromCache bool) (map[string]*model.User, error) {
if allowFromCache {
var cachedMap map[string]*model.User
if err := s.rootStore.doStandardReadCache(s.rootStore.profilesInChannelCache, channelId, &cachedMap); err == nil {
return cachedMap, nil
}
}
userMap, err := s.UserStore.GetAllProfilesInChannel(ctx, channelId, allowFromCache)
if err != nil {
return nil, err
}
if allowFromCache {
s.rootStore.doStandardAddToCache(s.rootStore.profilesInChannelCache, channelId, model.UserMap(userMap))
}
return userMap, nil
}
func (s *LocalCacheUserStore) GetProfileByIds(ctx context.Context, userIds []string, options *store.UserGetByIdsOpts, allowFromCache bool) ([]*model.User, error) {
if !allowFromCache {
return s.UserStore.GetProfileByIds(ctx, userIds, options, false)
}
if options == nil {
options = &store.UserGetByIdsOpts{}
}
users := []*model.User{}
remainingUserIds := make([]string, 0)
fromMaster := false
for _, userId := range userIds {
var cacheItem *model.User
if err := s.rootStore.doStandardReadCache(s.rootStore.userProfileByIdsCache, userId, &cacheItem); err == nil {
if options.Since == 0 || cacheItem.UpdateAt > options.Since {
users = append(users, cacheItem)
}
} else {
// If it was invalidated, then we need to query master.
s.userProfileByIdsMut.Lock()
if s.userProfileByIdsInvalidations[userId] {
fromMaster = true
// And then remove the key from the map.
delete(s.userProfileByIdsInvalidations, userId)
}
s.userProfileByIdsMut.Unlock()
remainingUserIds = append(remainingUserIds, userId)
}
}
if len(remainingUserIds) > 0 {
if fromMaster {
ctx = sqlstore.WithMaster(ctx)
}
remainingUsers, err := s.UserStore.GetProfileByIds(ctx, remainingUserIds, options, false)
if err != nil {
return nil, err
}
for _, user := range remainingUsers {
s.rootStore.doStandardAddToCache(s.rootStore.userProfileByIdsCache, user.Id, user)
users = append(users, user)
}
}
return users, nil
}
// Get is a cache wrapper around the SqlStore method to get a user profile by id.
// It checks if the user entry is present in the cache, returning the entry from cache
// if it is present. Otherwise, it fetches the entry from the store and stores it in the
// cache.
func (s *LocalCacheUserStore) Get(ctx context.Context, id string) (*model.User, error) {
var cacheItem *model.User
if err := s.rootStore.doStandardReadCache(s.rootStore.userProfileByIdsCache, id, &cacheItem); err == nil {
if s.rootStore.metrics != nil {
s.rootStore.metrics.AddMemCacheHitCounter("Profile By Id", float64(1))
}
return cacheItem, nil
}
if s.rootStore.metrics != nil {
s.rootStore.metrics.AddMemCacheMissCounter("Profile By Id", float64(1))
}
// If it was invalidated, then we need to query master.
s.userProfileByIdsMut.Lock()
if s.userProfileByIdsInvalidations[id] {
ctx = sqlstore.WithMaster(ctx)
// And then remove the key from the map.
delete(s.userProfileByIdsInvalidations, id)
}
s.userProfileByIdsMut.Unlock()
user, err := s.UserStore.Get(ctx, id)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.userProfileByIdsCache, id, user)
return user, nil
}
// GetMany is a cache wrapper around the SqlStore method to get a user profiles by ids.
// It checks if the user entries are present in the cache, returning the entries from cache
// if it is present. Otherwise, it fetches the entries from the store and stores it in the
// cache.
func (s *LocalCacheUserStore) GetMany(ctx context.Context, ids []string) ([]*model.User, error) {
// we are doing a loop instead of caching the full set in the cache because the number of permutations that we can have
// in this func is making caching of the total set not beneficial.
var cachedUsers []*model.User
var notCachedUserIds []string
uniqIDs := dedup(ids)
fromMaster := false
for _, id := range uniqIDs {
var cachedUser *model.User
if err := s.rootStore.doStandardReadCache(s.rootStore.userProfileByIdsCache, id, &cachedUser); err == nil {
if s.rootStore.metrics != nil {
s.rootStore.metrics.AddMemCacheHitCounter("Profile By Id", float64(1))
}
cachedUsers = append(cachedUsers, cachedUser)
} else {
if s.rootStore.metrics != nil {
s.rootStore.metrics.AddMemCacheMissCounter("Profile By Id", float64(1))
}
// If it was invalidated, then we need to query master.
s.userProfileByIdsMut.Lock()
if s.userProfileByIdsInvalidations[id] {
fromMaster = true
// And then remove the key from the map.
delete(s.userProfileByIdsInvalidations, id)
}
s.userProfileByIdsMut.Unlock()
notCachedUserIds = append(notCachedUserIds, id)
}
}
if len(notCachedUserIds) > 0 {
if fromMaster {
ctx = sqlstore.WithMaster(ctx)
}
dbUsers, err := s.UserStore.GetMany(ctx, notCachedUserIds)
if err != nil {
return nil, err
}
for _, user := range dbUsers {
s.rootStore.doStandardAddToCache(s.rootStore.userProfileByIdsCache, user.Id, user)
cachedUsers = append(cachedUsers, user)
}
}
return cachedUsers, nil
}
func dedup(elements []string) []string {
if len(elements) == 0 {
return elements
}
sort.Strings(elements)
j := 0
for i := 1; i < len(elements); i++ {
if elements[j] == elements[i] {
continue
}
j++
// preserve the original data
// in[i], in[j] = in[j], in[i]
// only set what is required
elements[j] = elements[i]
}
return elements[:j+1]
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localcachelayer
import (
"bytes"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type LocalCacheWebhookStore struct {
store.WebhookStore
rootStore *LocalCacheStore
}
func (s *LocalCacheWebhookStore) handleClusterInvalidateWebhook(msg *model.ClusterMessage) {
if bytes.Equal(msg.Data, clearCacheMessageData) {
s.rootStore.webhookCache.Purge()
} else {
s.rootStore.webhookCache.Remove(string(msg.Data))
}
}
func (s LocalCacheWebhookStore) ClearCaches() {
s.rootStore.doClearCacheCluster(s.rootStore.webhookCache)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Webhook - Purge")
}
}
func (s LocalCacheWebhookStore) InvalidateWebhookCache(webhookId string) {
s.rootStore.doInvalidateCacheCluster(s.rootStore.webhookCache, webhookId)
if s.rootStore.metrics != nil {
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Webhook - Remove by WebhookId")
}
}
func (s LocalCacheWebhookStore) GetIncoming(id string, allowFromCache bool) (*model.IncomingWebhook, error) {
if !allowFromCache {
return s.WebhookStore.GetIncoming(id, allowFromCache)
}
var incomingWebhook *model.IncomingWebhook
if err := s.rootStore.doStandardReadCache(s.rootStore.webhookCache, id, &incomingWebhook); err == nil {
return incomingWebhook, nil
}
incomingWebhook, err := s.WebhookStore.GetIncoming(id, allowFromCache)
if err != nil {
return nil, err
}
s.rootStore.doStandardAddToCache(s.rootStore.webhookCache, id, incomingWebhook)
return incomingWebhook, nil
}
func (s LocalCacheWebhookStore) DeleteIncoming(webhookId string, time int64) error {
err := s.WebhookStore.DeleteIncoming(webhookId, time)
if err != nil {
return err
}
s.InvalidateWebhookCache(webhookId)
return nil
}
func (s LocalCacheWebhookStore) PermanentDeleteIncomingByUser(userId string) error {
err := s.WebhookStore.PermanentDeleteIncomingByUser(userId)
if err != nil {
return err
}
s.ClearCaches()
return nil
}
func (s LocalCacheWebhookStore) PermanentDeleteIncomingByChannel(channelId string) error {
err := s.WebhookStore.PermanentDeleteIncomingByChannel(channelId)
if err != nil {
return err
}
s.ClearCaches()
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Code generated by "make store-layers"
// DO NOT EDIT
package opentracinglayer
import (
"context"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/tracing"
"github.com/opentracing/opentracing-go/ext"
spanlog "github.com/opentracing/opentracing-go/log"
)
type OpenTracingLayer struct {
store.Store
AuditStore store.AuditStore
BotStore store.BotStore
ChannelStore store.ChannelStore
ChannelMemberHistoryStore store.ChannelMemberHistoryStore
ClusterDiscoveryStore store.ClusterDiscoveryStore
CommandStore store.CommandStore
CommandWebhookStore store.CommandWebhookStore
ComplianceStore store.ComplianceStore
DraftStore store.DraftStore
EmojiStore store.EmojiStore
FileInfoStore store.FileInfoStore
GroupStore store.GroupStore
JobStore store.JobStore
LicenseStore store.LicenseStore
LinkMetadataStore store.LinkMetadataStore
NotifyAdminStore store.NotifyAdminStore
OAuthStore store.OAuthStore
PluginStore store.PluginStore
PostStore store.PostStore
PostAcknowledgementStore store.PostAcknowledgementStore
PostPriorityStore store.PostPriorityStore
PreferenceStore store.PreferenceStore
ProductNoticesStore store.ProductNoticesStore
ReactionStore store.ReactionStore
RemoteClusterStore store.RemoteClusterStore
RetentionPolicyStore store.RetentionPolicyStore
RoleStore store.RoleStore
SchemeStore store.SchemeStore
SessionStore store.SessionStore
SharedChannelStore store.SharedChannelStore
StatusStore store.StatusStore
SystemStore store.SystemStore
TeamStore store.TeamStore
TermsOfServiceStore store.TermsOfServiceStore
ThreadStore store.ThreadStore
TokenStore store.TokenStore
TrueUpReviewStore store.TrueUpReviewStore
UploadSessionStore store.UploadSessionStore
UserStore store.UserStore
UserAccessTokenStore store.UserAccessTokenStore
UserTermsOfServiceStore store.UserTermsOfServiceStore
WebhookStore store.WebhookStore
}
func (s *OpenTracingLayer) Audit() store.AuditStore {
return s.AuditStore
}
func (s *OpenTracingLayer) Bot() store.BotStore {
return s.BotStore
}
func (s *OpenTracingLayer) Channel() store.ChannelStore {
return s.ChannelStore
}
func (s *OpenTracingLayer) ChannelMemberHistory() store.ChannelMemberHistoryStore {
return s.ChannelMemberHistoryStore
}
func (s *OpenTracingLayer) ClusterDiscovery() store.ClusterDiscoveryStore {
return s.ClusterDiscoveryStore
}
func (s *OpenTracingLayer) Command() store.CommandStore {
return s.CommandStore
}
func (s *OpenTracingLayer) CommandWebhook() store.CommandWebhookStore {
return s.CommandWebhookStore
}
func (s *OpenTracingLayer) Compliance() store.ComplianceStore {
return s.ComplianceStore
}
func (s *OpenTracingLayer) Draft() store.DraftStore {
return s.DraftStore
}
func (s *OpenTracingLayer) Emoji() store.EmojiStore {
return s.EmojiStore
}
func (s *OpenTracingLayer) FileInfo() store.FileInfoStore {
return s.FileInfoStore
}
func (s *OpenTracingLayer) Group() store.GroupStore {
return s.GroupStore
}
func (s *OpenTracingLayer) Job() store.JobStore {
return s.JobStore
}
func (s *OpenTracingLayer) License() store.LicenseStore {
return s.LicenseStore
}
func (s *OpenTracingLayer) LinkMetadata() store.LinkMetadataStore {
return s.LinkMetadataStore
}
func (s *OpenTracingLayer) NotifyAdmin() store.NotifyAdminStore {
return s.NotifyAdminStore
}
func (s *OpenTracingLayer) OAuth() store.OAuthStore {
return s.OAuthStore
}
func (s *OpenTracingLayer) Plugin() store.PluginStore {
return s.PluginStore
}
func (s *OpenTracingLayer) Post() store.PostStore {
return s.PostStore
}
func (s *OpenTracingLayer) PostAcknowledgement() store.PostAcknowledgementStore {
return s.PostAcknowledgementStore
}
func (s *OpenTracingLayer) PostPriority() store.PostPriorityStore {
return s.PostPriorityStore
}
func (s *OpenTracingLayer) Preference() store.PreferenceStore {
return s.PreferenceStore
}
func (s *OpenTracingLayer) ProductNotices() store.ProductNoticesStore {
return s.ProductNoticesStore
}
func (s *OpenTracingLayer) Reaction() store.ReactionStore {
return s.ReactionStore
}
func (s *OpenTracingLayer) RemoteCluster() store.RemoteClusterStore {
return s.RemoteClusterStore
}
func (s *OpenTracingLayer) RetentionPolicy() store.RetentionPolicyStore {
return s.RetentionPolicyStore
}
func (s *OpenTracingLayer) Role() store.RoleStore {
return s.RoleStore
}
func (s *OpenTracingLayer) Scheme() store.SchemeStore {
return s.SchemeStore
}
func (s *OpenTracingLayer) Session() store.SessionStore {
return s.SessionStore
}
func (s *OpenTracingLayer) SharedChannel() store.SharedChannelStore {
return s.SharedChannelStore
}
func (s *OpenTracingLayer) Status() store.StatusStore {
return s.StatusStore
}
func (s *OpenTracingLayer) System() store.SystemStore {
return s.SystemStore
}
func (s *OpenTracingLayer) Team() store.TeamStore {
return s.TeamStore
}
func (s *OpenTracingLayer) TermsOfService() store.TermsOfServiceStore {
return s.TermsOfServiceStore
}
func (s *OpenTracingLayer) Thread() store.ThreadStore {
return s.ThreadStore
}
func (s *OpenTracingLayer) Token() store.TokenStore {
return s.TokenStore
}
func (s *OpenTracingLayer) TrueUpReview() store.TrueUpReviewStore {
return s.TrueUpReviewStore
}
func (s *OpenTracingLayer) UploadSession() store.UploadSessionStore {
return s.UploadSessionStore
}
func (s *OpenTracingLayer) User() store.UserStore {
return s.UserStore
}
func (s *OpenTracingLayer) UserAccessToken() store.UserAccessTokenStore {
return s.UserAccessTokenStore
}
func (s *OpenTracingLayer) UserTermsOfService() store.UserTermsOfServiceStore {
return s.UserTermsOfServiceStore
}
func (s *OpenTracingLayer) Webhook() store.WebhookStore {
return s.WebhookStore
}
type OpenTracingLayerAuditStore struct {
store.AuditStore
Root *OpenTracingLayer
}
type OpenTracingLayerBotStore struct {
store.BotStore
Root *OpenTracingLayer
}
type OpenTracingLayerChannelStore struct {
store.ChannelStore
Root *OpenTracingLayer
}
type OpenTracingLayerChannelMemberHistoryStore struct {
store.ChannelMemberHistoryStore
Root *OpenTracingLayer
}
type OpenTracingLayerClusterDiscoveryStore struct {
store.ClusterDiscoveryStore
Root *OpenTracingLayer
}
type OpenTracingLayerCommandStore struct {
store.CommandStore
Root *OpenTracingLayer
}
type OpenTracingLayerCommandWebhookStore struct {
store.CommandWebhookStore
Root *OpenTracingLayer
}
type OpenTracingLayerComplianceStore struct {
store.ComplianceStore
Root *OpenTracingLayer
}
type OpenTracingLayerDraftStore struct {
store.DraftStore
Root *OpenTracingLayer
}
type OpenTracingLayerEmojiStore struct {
store.EmojiStore
Root *OpenTracingLayer
}
type OpenTracingLayerFileInfoStore struct {
store.FileInfoStore
Root *OpenTracingLayer
}
type OpenTracingLayerGroupStore struct {
store.GroupStore
Root *OpenTracingLayer
}
type OpenTracingLayerJobStore struct {
store.JobStore
Root *OpenTracingLayer
}
type OpenTracingLayerLicenseStore struct {
store.LicenseStore
Root *OpenTracingLayer
}
type OpenTracingLayerLinkMetadataStore struct {
store.LinkMetadataStore
Root *OpenTracingLayer
}
type OpenTracingLayerNotifyAdminStore struct {
store.NotifyAdminStore
Root *OpenTracingLayer
}
type OpenTracingLayerOAuthStore struct {
store.OAuthStore
Root *OpenTracingLayer
}
type OpenTracingLayerPluginStore struct {
store.PluginStore
Root *OpenTracingLayer
}
type OpenTracingLayerPostStore struct {
store.PostStore
Root *OpenTracingLayer
}
type OpenTracingLayerPostAcknowledgementStore struct {
store.PostAcknowledgementStore
Root *OpenTracingLayer
}
type OpenTracingLayerPostPriorityStore struct {
store.PostPriorityStore
Root *OpenTracingLayer
}
type OpenTracingLayerPreferenceStore struct {
store.PreferenceStore
Root *OpenTracingLayer
}
type OpenTracingLayerProductNoticesStore struct {
store.ProductNoticesStore
Root *OpenTracingLayer
}
type OpenTracingLayerReactionStore struct {
store.ReactionStore
Root *OpenTracingLayer
}
type OpenTracingLayerRemoteClusterStore struct {
store.RemoteClusterStore
Root *OpenTracingLayer
}
type OpenTracingLayerRetentionPolicyStore struct {
store.RetentionPolicyStore
Root *OpenTracingLayer
}
type OpenTracingLayerRoleStore struct {
store.RoleStore
Root *OpenTracingLayer
}
type OpenTracingLayerSchemeStore struct {
store.SchemeStore
Root *OpenTracingLayer
}
type OpenTracingLayerSessionStore struct {
store.SessionStore
Root *OpenTracingLayer
}
type OpenTracingLayerSharedChannelStore struct {
store.SharedChannelStore
Root *OpenTracingLayer
}
type OpenTracingLayerStatusStore struct {
store.StatusStore
Root *OpenTracingLayer
}
type OpenTracingLayerSystemStore struct {
store.SystemStore
Root *OpenTracingLayer
}
type OpenTracingLayerTeamStore struct {
store.TeamStore
Root *OpenTracingLayer
}
type OpenTracingLayerTermsOfServiceStore struct {
store.TermsOfServiceStore
Root *OpenTracingLayer
}
type OpenTracingLayerThreadStore struct {
store.ThreadStore
Root *OpenTracingLayer
}
type OpenTracingLayerTokenStore struct {
store.TokenStore
Root *OpenTracingLayer
}
type OpenTracingLayerTrueUpReviewStore struct {
store.TrueUpReviewStore
Root *OpenTracingLayer
}
type OpenTracingLayerUploadSessionStore struct {
store.UploadSessionStore
Root *OpenTracingLayer
}
type OpenTracingLayerUserStore struct {
store.UserStore
Root *OpenTracingLayer
}
type OpenTracingLayerUserAccessTokenStore struct {
store.UserAccessTokenStore
Root *OpenTracingLayer
}
type OpenTracingLayerUserTermsOfServiceStore struct {
store.UserTermsOfServiceStore
Root *OpenTracingLayer
}
type OpenTracingLayerWebhookStore struct {
store.WebhookStore
Root *OpenTracingLayer
}
func (s *OpenTracingLayerAuditStore) Get(user_id string, offset int, limit int) (model.Audits, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "AuditStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.AuditStore.Get(user_id, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerAuditStore) PermanentDeleteByUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "AuditStore.PermanentDeleteByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.AuditStore.PermanentDeleteByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerAuditStore) Save(audit *model.Audit) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "AuditStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.AuditStore.Save(audit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerBotStore) Get(userID string, includeDeleted bool) (*model.Bot, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "BotStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.BotStore.Get(userID, includeDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerBotStore) GetAll(options *model.BotGetOptions) ([]*model.Bot, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "BotStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.BotStore.GetAll(options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerBotStore) PermanentDelete(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "BotStore.PermanentDelete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.BotStore.PermanentDelete(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerBotStore) Save(bot *model.Bot) (*model.Bot, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "BotStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.BotStore.Save(bot)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerBotStore) Update(bot *model.Bot) (*model.Bot, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "BotStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.BotStore.Update(bot)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) AnalyticsDeletedTypeCount(teamID string, channelType model.ChannelType) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.AnalyticsDeletedTypeCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.AnalyticsDeletedTypeCount(teamID, channelType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) AnalyticsTypeCount(teamID string, channelType model.ChannelType) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.AnalyticsTypeCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.AnalyticsTypeCount(teamID, channelType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) Autocomplete(userID string, term string, includeDeleted bool, isGuest bool) (model.ChannelListWithTeamData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.Autocomplete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.Autocomplete(userID, term, includeDeleted, isGuest)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) AutocompleteInTeam(teamID string, userID string, term string, includeDeleted bool, isGuest bool) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.AutocompleteInTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.AutocompleteInTeam(teamID, userID, term, includeDeleted, isGuest)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) AutocompleteInTeamForSearch(teamID string, userID string, term string, includeDeleted bool) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.AutocompleteInTeamForSearch")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.AutocompleteInTeamForSearch(teamID, userID, term, includeDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) ClearAllCustomRoleAssignments() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.ClearAllCustomRoleAssignments")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.ClearAllCustomRoleAssignments()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) ClearCaches() {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.ClearCaches")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.ChannelStore.ClearCaches()
}
func (s *OpenTracingLayerChannelStore) ClearMembersForUserCache() {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.ClearMembersForUserCache")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.ChannelStore.ClearMembersForUserCache()
}
func (s *OpenTracingLayerChannelStore) ClearSidebarOnTeamLeave(userID string, teamID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.ClearSidebarOnTeamLeave")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.ClearSidebarOnTeamLeave(userID, teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) CountPostsAfter(channelID string, timestamp int64, userID string) (int, int, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.CountPostsAfter")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.ChannelStore.CountPostsAfter(channelID, timestamp, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerChannelStore) CountUrgentPostsAfter(channelID string, timestamp int64, userID string) (int, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.CountUrgentPostsAfter")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.CountUrgentPostsAfter(channelID, timestamp, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) CreateDirectChannel(userID *model.User, otherUserID *model.User, channelOptions ...model.ChannelOption) (*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.CreateDirectChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.CreateDirectChannel(userID, otherUserID, channelOptions...)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) CreateInitialSidebarCategories(userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.CreateInitialSidebarCategories")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.CreateInitialSidebarCategories(userID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) CreateSidebarCategory(userID string, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.CreateSidebarCategory")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.CreateSidebarCategory(userID, teamID, newCategory)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) Delete(channelID string, timestamp int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.Delete(channelID, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) DeleteSidebarCategory(categoryID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.DeleteSidebarCategory")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.DeleteSidebarCategory(categoryID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) DeleteSidebarChannelsByPreferences(preferences model.Preferences) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.DeleteSidebarChannelsByPreferences")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.DeleteSidebarChannelsByPreferences(preferences)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) Get(id string, allowFromCache bool) (*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.Get(id, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetAll(teamID string) ([]*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetAll(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetAllChannelMembersById(id string) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetAllChannelMembersById")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetAllChannelMembersById(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetAllChannelMembersForUser(userID string, allowFromCache bool, includeDeleted bool) (map[string]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetAllChannelMembersForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetAllChannelMembersForUser(userID, allowFromCache, includeDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetAllChannelMembersNotifyPropsForChannel(channelID string, allowFromCache bool) (map[string]model.StringMap, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetAllChannelMembersNotifyPropsForChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetAllChannelMembersNotifyPropsForChannel(channelID, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetAllChannels(page int, perPage int, opts store.ChannelSearchOpts) (model.ChannelListWithTeamData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetAllChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetAllChannels(page, perPage, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetAllChannelsCount(opts store.ChannelSearchOpts) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetAllChannelsCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetAllChannelsCount(opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetAllChannelsForExportAfter(limit int, afterID string) ([]*model.ChannelForExport, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetAllChannelsForExportAfter")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetAllChannelsForExportAfter(limit, afterID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetAllDirectChannelsForExportAfter(limit int, afterID string) ([]*model.DirectChannelForExport, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetAllDirectChannelsForExportAfter")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetAllDirectChannelsForExportAfter(limit, afterID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetByName(team_id string, name string, allowFromCache bool) (*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetByName(team_id, name, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetByNameIncludeDeleted(team_id string, name string, allowFromCache bool) (*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetByNameIncludeDeleted")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetByNameIncludeDeleted(team_id, name, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetByNames(team_id string, names []string, allowFromCache bool) ([]*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetByNames")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetByNames(team_id, names, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelCounts(teamID string, userID string) (*model.ChannelCounts, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelCounts")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannelCounts(teamID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelMembersForExport(userID string, teamID string) ([]*model.ChannelMemberForExport, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelMembersForExport")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannelMembersForExport(userID, teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelMembersTimezones(channelID string) ([]model.StringMap, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelMembersTimezones")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannelMembersTimezones(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelUnread(channelID string, userID string) (*model.ChannelUnread, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelUnread")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannelUnread(channelID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannels(teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannels(teamID, userID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelsBatchForIndexing(startTime int64, startChannelID string, limit int) ([]*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelsBatchForIndexing")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannelsBatchForIndexing(startTime, startChannelID, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelsByIds(channelIds []string, includeDeleted bool) ([]*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelsByIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannelsByIds(channelIds, includeDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelsByScheme(schemeID string, offset int, limit int) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelsByScheme")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannelsByScheme(schemeID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelsByUser(userID string, includeDeleted bool, lastDeleteAt int, pageSize int, fromChannelID string) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelsByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannelsByUser(userID, includeDeleted, lastDeleteAt, pageSize, fromChannelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelsWithCursor")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannelsWithCursor(teamId, userId, opts, afterChannelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetChannelsWithTeamDataByIds(channelIds []string, includeDeleted bool) ([]*model.ChannelWithTeamData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelsWithTeamDataByIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetChannelsWithTeamDataByIds(channelIds, includeDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetDeleted")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetDeleted(team_id, offset, limit, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetDeletedByName(team_id string, name string) (*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetDeletedByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetDeletedByName(team_id, name)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetFileCount(channelID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetFileCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetFileCount(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetForPost(postID string) (*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetForPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetForPost(postID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetGuestCount(channelID string, allowFromCache bool) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetGuestCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetGuestCount(channelID, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMany(ids []string, allowFromCache bool) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMany")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMany(ids, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMember(ctx context.Context, channelID string, userID string) (*model.ChannelMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMember(ctx, channelID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMemberCount(channelID string, allowFromCache bool) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMemberCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMemberCount(channelID, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMemberCountFromCache(channelID string) int64 {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMemberCountFromCache")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result := s.ChannelStore.GetMemberCountFromCache(channelID)
return result
}
func (s *OpenTracingLayerChannelStore) GetMemberCountsByGroup(ctx context.Context, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMemberCountsByGroup")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMemberCountsByGroup(ctx, channelID, includeTimezones)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMemberForPost(postID string, userID string) (*model.ChannelMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMemberForPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMemberForPost(postID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMembers(channelID string, offset int, limit int) (model.ChannelMembers, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMembers(channelID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMembersByChannelIds(channelIds []string, userID string) (model.ChannelMembers, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMembersByChannelIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMembersByChannelIds(channelIds, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMembersByIds(channelID string, userIds []string) (model.ChannelMembers, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMembersByIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMembersByIds(channelID, userIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMembersForUser(teamID string, userID string) (model.ChannelMembers, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMembersForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMembersForUser(teamID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMembersForUserWithCursor(userID string, teamID string, opts *store.ChannelMemberGraphQLSearchOpts) (model.ChannelMembers, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMembersForUserWithCursor")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMembersForUserWithCursor(userID, teamID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMembersForUserWithPagination(userID string, page int, perPage int) (model.ChannelMembersWithTeamData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMembersForUserWithPagination")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMembersForUserWithPagination(userID, page, perPage)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMembersInfoByChannelIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMembersInfoByChannelIds(channelIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetMoreChannels(teamID string, userID string, offset int, limit int) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMoreChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetMoreChannels(teamID, userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetPinnedPostCount(channelID string, allowFromCache bool) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetPinnedPostCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetPinnedPostCount(channelID, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetPinnedPosts(channelID string) (*model.PostList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetPinnedPosts")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetPinnedPosts(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetPrivateChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetPrivateChannelsForTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetPrivateChannelsForTeam(teamID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetPublicChannelsByIdsForTeam(teamID string, channelIds []string) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetPublicChannelsByIdsForTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetPublicChannelsByIdsForTeam(teamID, channelIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetPublicChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetPublicChannelsForTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetPublicChannelsForTeam(teamID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetSidebarCategories(userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetSidebarCategories")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetSidebarCategories(userID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetSidebarCategoriesForTeamForUser(userID string, teamID string) (*model.OrderedSidebarCategories, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetSidebarCategoriesForTeamForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetSidebarCategoriesForTeamForUser(userID, teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetSidebarCategory(categoryID string) (*model.SidebarCategoryWithChannels, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetSidebarCategory")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetSidebarCategory(categoryID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetSidebarCategoryOrder(userID string, teamID string) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetSidebarCategoryOrder")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetSidebarCategoryOrder(userID, teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetTeamChannels(teamID string) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetTeamChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetTeamChannels(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetTeamForChannel(channelID string) (*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetTeamForChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetTeamForChannel(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetTeamMembersForChannel(channelID string) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetTeamMembersForChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetTeamMembersForChannel(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetTopChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetTopChannelsForTeamSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetTopChannelsForTeamSince(teamID, userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetTopChannelsForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetTopChannelsForUserSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetTopChannelsForUserSince(userID, teamID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetTopInactiveChannelsForTeamSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetTopInactiveChannelsForTeamSince(teamID, userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetTopInactiveChannelsForUserSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetTopInactiveChannelsForUserSince(teamID, userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GroupSyncedChannelCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GroupSyncedChannelCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GroupSyncedChannelCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) IncrementMentionCount(channelID string, userIDs []string, isRoot bool, isUrgent bool) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.IncrementMentionCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.IncrementMentionCount(channelID, userIDs, isRoot, isUrgent)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) InvalidateAllChannelMembersForUser(userID string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.InvalidateAllChannelMembersForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.ChannelStore.InvalidateAllChannelMembersForUser(userID)
}
func (s *OpenTracingLayerChannelStore) InvalidateCacheForChannelMembersNotifyProps(channelID string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.InvalidateCacheForChannelMembersNotifyProps")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.ChannelStore.InvalidateCacheForChannelMembersNotifyProps(channelID)
}
func (s *OpenTracingLayerChannelStore) InvalidateChannel(id string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.InvalidateChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.ChannelStore.InvalidateChannel(id)
}
func (s *OpenTracingLayerChannelStore) InvalidateChannelByName(teamID string, name string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.InvalidateChannelByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.ChannelStore.InvalidateChannelByName(teamID, name)
}
func (s *OpenTracingLayerChannelStore) InvalidateGuestCount(channelID string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.InvalidateGuestCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.ChannelStore.InvalidateGuestCount(channelID)
}
func (s *OpenTracingLayerChannelStore) InvalidateMemberCount(channelID string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.InvalidateMemberCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.ChannelStore.InvalidateMemberCount(channelID)
}
func (s *OpenTracingLayerChannelStore) InvalidatePinnedPostCount(channelID string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.InvalidatePinnedPostCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.ChannelStore.InvalidatePinnedPostCount(channelID)
}
func (s *OpenTracingLayerChannelStore) IsUserInChannelUseCache(userID string, channelID string) bool {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.IsUserInChannelUseCache")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result := s.ChannelStore.IsUserInChannelUseCache(userID, channelID)
return result
}
func (s *OpenTracingLayerChannelStore) MigrateChannelMembers(fromChannelID string, fromUserID string) (map[string]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.MigrateChannelMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.MigrateChannelMembers(fromChannelID, fromUserID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) PermanentDelete(channelID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.PermanentDelete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.PermanentDelete(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) PermanentDeleteByTeam(teamID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.PermanentDeleteByTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.PermanentDeleteByTeam(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) PermanentDeleteMembersByChannel(channelID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.PermanentDeleteMembersByChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.PermanentDeleteMembersByChannel(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) PermanentDeleteMembersByUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.PermanentDeleteMembersByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.PermanentDeleteMembersByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) PostCountsByDuration(channelIDs []string, sinceUnixMillis int64, userID *string, duration model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.PostCountsByDuration")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.PostCountsByDuration(channelIDs, sinceUnixMillis, userID, duration, groupingLocation)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) RemoveAllDeactivatedMembers(channelID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.RemoveAllDeactivatedMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.RemoveAllDeactivatedMembers(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) RemoveMember(channelID string, userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.RemoveMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.RemoveMember(channelID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) RemoveMembers(channelID string, userIds []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.RemoveMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.RemoveMembers(channelID, userIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) ResetAllChannelSchemes() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.ResetAllChannelSchemes")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.ResetAllChannelSchemes()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) Restore(channelID string, timestamp int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.Restore")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.Restore(channelID, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) Save(channel *model.Channel, maxChannelsPerTeam int64) (*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.Save(channel, maxChannelsPerTeam)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) SaveDirectChannel(channel *model.Channel, member1 *model.ChannelMember, member2 *model.ChannelMember) (*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SaveDirectChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.SaveDirectChannel(channel, member1, member2)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) SaveMember(member *model.ChannelMember) (*model.ChannelMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SaveMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.SaveMember(member)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) SaveMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SaveMultipleMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.SaveMultipleMembers(members)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) SearchAllChannels(term string, opts store.ChannelSearchOpts) (model.ChannelListWithTeamData, int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SearchAllChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.ChannelStore.SearchAllChannels(term, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerChannelStore) SearchArchivedInTeam(teamID string, term string, userID string) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SearchArchivedInTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.SearchArchivedInTeam(teamID, term, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) SearchForUserInTeam(userID string, teamID string, term string, includeDeleted bool) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SearchForUserInTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.SearchForUserInTeam(userID, teamID, term, includeDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) SearchGroupChannels(userID string, term string) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SearchGroupChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.SearchGroupChannels(userID, term)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) SearchInTeam(teamID string, term string, includeDeleted bool) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SearchInTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.SearchInTeam(teamID, term, includeDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) SearchMore(userID string, teamID string, term string) (model.ChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SearchMore")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.SearchMore(userID, teamID, term)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) SetDeleteAt(channelID string, deleteAt int64, updateAt int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SetDeleteAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.SetDeleteAt(channelID, deleteAt, updateAt)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) SetShared(channelId string, shared bool) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.SetShared")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.SetShared(channelId, shared)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) Update(channel *model.Channel) (*model.Channel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.Update(channel)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) UpdateLastViewedAt(channelIds []string, userID string) (map[string]int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateLastViewedAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.UpdateLastViewedAt(channelIds, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, urgentMentionCount int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateLastViewedAtPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, mentionCountRoot, urgentMentionCount, setUnreadCountRoot)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) UpdateMember(member *model.ChannelMember) (*model.ChannelMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.UpdateMember(member)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) UpdateMemberNotifyProps(channelID string, userID string, props map[string]string) (*model.ChannelMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateMemberNotifyProps")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.UpdateMemberNotifyProps(channelID, userID, props)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) UpdateMembersRole(channelID string, userIDs []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateMembersRole")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.UpdateMembersRole(channelID, userIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) UpdateMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateMultipleMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.UpdateMultipleMembers(members)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) UpdateSidebarCategories(userID string, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateSidebarCategories")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.ChannelStore.UpdateSidebarCategories(userID, teamID, categories)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerChannelStore) UpdateSidebarCategoryOrder(userID string, teamID string, categoryOrder []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateSidebarCategoryOrder")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.UpdateSidebarCategoryOrder(userID, teamID, categoryOrder)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) UpdateSidebarChannelCategoryOnMove(channel *model.Channel, newTeamID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateSidebarChannelCategoryOnMove")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.UpdateSidebarChannelCategoryOnMove(channel, newTeamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) UpdateSidebarChannelsByPreferences(preferences model.Preferences) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateSidebarChannelsByPreferences")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.UpdateSidebarChannelsByPreferences(preferences)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) UserBelongsToChannels(userID string, channelIds []string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UserBelongsToChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.UserBelongsToChannels(userID, channelIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelMemberHistoryStore) DeleteOrphanedRows(limit int) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelMemberHistoryStore.DeleteOrphanedRows")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelMemberHistoryStore.DeleteOrphanedRows(limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelMemberHistoryStore) GetChannelsLeftSince(userID string, since int64) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelMemberHistoryStore.GetChannelsLeftSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelMemberHistoryStore.GetChannelsLeftSince(userID, since)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelID string) ([]*model.ChannelMemberHistoryResult, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelMemberHistoryStore.GetUsersInChannelDuring")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelMemberHistoryStore.GetUsersInChannelDuring(startTime, endTime, channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelMemberHistoryStore) LogJoinEvent(userID string, channelID string, joinTime int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelMemberHistoryStore.LogJoinEvent")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelMemberHistoryStore.LogJoinEvent(userID, channelID, joinTime)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelMemberHistoryStore) LogLeaveEvent(userID string, channelID string, leaveTime int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelMemberHistoryStore.LogLeaveEvent")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelMemberHistoryStore.LogLeaveEvent(userID, channelID, leaveTime)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelMemberHistoryStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelMemberHistoryStore.PermanentDeleteBatch")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelMemberHistoryStore.PermanentDeleteBatch(endTime, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelMemberHistoryStore.PermanentDeleteBatchForRetentionPolicies")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.ChannelMemberHistoryStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerClusterDiscoveryStore) Cleanup() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ClusterDiscoveryStore.Cleanup")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ClusterDiscoveryStore.Cleanup()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerClusterDiscoveryStore) Delete(discovery *model.ClusterDiscovery) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ClusterDiscoveryStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ClusterDiscoveryStore.Delete(discovery)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerClusterDiscoveryStore) Exists(discovery *model.ClusterDiscovery) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ClusterDiscoveryStore.Exists")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ClusterDiscoveryStore.Exists(discovery)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerClusterDiscoveryStore) GetAll(discoveryType string, clusterName string) ([]*model.ClusterDiscovery, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ClusterDiscoveryStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ClusterDiscoveryStore.GetAll(discoveryType, clusterName)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerClusterDiscoveryStore) Save(discovery *model.ClusterDiscovery) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ClusterDiscoveryStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ClusterDiscoveryStore.Save(discovery)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerClusterDiscoveryStore) SetLastPingAt(discovery *model.ClusterDiscovery) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ClusterDiscoveryStore.SetLastPingAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ClusterDiscoveryStore.SetLastPingAt(discovery)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerCommandStore) AnalyticsCommandCount(teamID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandStore.AnalyticsCommandCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.CommandStore.AnalyticsCommandCount(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerCommandStore) Delete(commandID string, timestamp int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.CommandStore.Delete(commandID, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerCommandStore) Get(id string) (*model.Command, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.CommandStore.Get(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerCommandStore) GetByTeam(teamID string) ([]*model.Command, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandStore.GetByTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.CommandStore.GetByTeam(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerCommandStore) GetByTrigger(teamID string, trigger string) (*model.Command, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandStore.GetByTrigger")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.CommandStore.GetByTrigger(teamID, trigger)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerCommandStore) PermanentDeleteByTeam(teamID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandStore.PermanentDeleteByTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.CommandStore.PermanentDeleteByTeam(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerCommandStore) PermanentDeleteByUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandStore.PermanentDeleteByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.CommandStore.PermanentDeleteByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerCommandStore) Save(webhook *model.Command) (*model.Command, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.CommandStore.Save(webhook)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerCommandStore) Update(hook *model.Command) (*model.Command, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.CommandStore.Update(hook)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerCommandWebhookStore) Cleanup() {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandWebhookStore.Cleanup")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.CommandWebhookStore.Cleanup()
}
func (s *OpenTracingLayerCommandWebhookStore) Get(id string) (*model.CommandWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandWebhookStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.CommandWebhookStore.Get(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerCommandWebhookStore) Save(webhook *model.CommandWebhook) (*model.CommandWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandWebhookStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.CommandWebhookStore.Save(webhook)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerCommandWebhookStore) TryUse(id string, limit int) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "CommandWebhookStore.TryUse")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.CommandWebhookStore.TryUse(id, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerComplianceStore) ComplianceExport(compliance *model.Compliance, cursor model.ComplianceExportCursor, limit int) ([]*model.CompliancePost, model.ComplianceExportCursor, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ComplianceStore.ComplianceExport")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.ComplianceStore.ComplianceExport(compliance, cursor, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerComplianceStore) Get(id string) (*model.Compliance, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ComplianceStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ComplianceStore.Get(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerComplianceStore) GetAll(offset int, limit int) (model.Compliances, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ComplianceStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ComplianceStore.GetAll(offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerComplianceStore) MessageExport(ctx context.Context, cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ComplianceStore.MessageExport")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.ComplianceStore.MessageExport(ctx, cursor, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerComplianceStore) Save(compliance *model.Compliance) (*model.Compliance, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ComplianceStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ComplianceStore.Save(compliance)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerComplianceStore) Update(compliance *model.Compliance) (*model.Compliance, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ComplianceStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ComplianceStore.Update(compliance)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerDraftStore) Delete(userID string, channelID string, rootID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DraftStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.DraftStore.Delete(userID, channelID, rootID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerDraftStore) Get(userID string, channelID string, rootID string, includeDeleted bool) (*model.Draft, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DraftStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.DraftStore.Get(userID, channelID, rootID, includeDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerDraftStore) GetDraftsForUser(userID string, teamID string) ([]*model.Draft, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DraftStore.GetDraftsForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.DraftStore.GetDraftsForUser(userID, teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerDraftStore) Save(d *model.Draft) (*model.Draft, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DraftStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.DraftStore.Save(d)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerDraftStore) Update(d *model.Draft) (*model.Draft, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DraftStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.DraftStore.Update(d)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerEmojiStore) Delete(emoji *model.Emoji, timestamp int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "EmojiStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.EmojiStore.Delete(emoji, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerEmojiStore) Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "EmojiStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.EmojiStore.Get(ctx, id, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerEmojiStore) GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "EmojiStore.GetByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.EmojiStore.GetByName(ctx, name, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerEmojiStore) GetList(offset int, limit int, sort string) ([]*model.Emoji, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "EmojiStore.GetList")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.EmojiStore.GetList(offset, limit, sort)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerEmojiStore) GetMultipleByName(names []string) ([]*model.Emoji, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "EmojiStore.GetMultipleByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.EmojiStore.GetMultipleByName(names)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerEmojiStore) Save(emoji *model.Emoji) (*model.Emoji, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "EmojiStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.EmojiStore.Save(emoji)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerEmojiStore) Search(name string, prefixOnly bool, limit int) ([]*model.Emoji, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "EmojiStore.Search")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.EmojiStore.Search(name, prefixOnly, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) AttachToPost(fileID string, postID string, channelID string, creatorID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.AttachToPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.FileInfoStore.AttachToPost(fileID, postID, channelID, creatorID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerFileInfoStore) ClearCaches() {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.ClearCaches")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.FileInfoStore.ClearCaches()
}
func (s *OpenTracingLayerFileInfoStore) CountAll() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.CountAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.CountAll()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) DeleteForPost(postID string) (string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.DeleteForPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.DeleteForPost(postID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) Get(id string) (*model.FileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.Get(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) GetByIds(ids []string) ([]*model.FileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.GetByIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.GetByIds(ids)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) GetByPath(path string) (*model.FileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.GetByPath")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.GetByPath(path)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) GetFilesBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.FileForIndexing, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.GetFilesBatchForIndexing")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.GetFilesBatchForIndexing(startTime, startFileID, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) GetForPost(postID string, readFromMaster bool, includeDeleted bool, allowFromCache bool) ([]*model.FileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.GetForPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.GetForPost(postID, readFromMaster, includeDeleted, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) GetForUser(userID string) ([]*model.FileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.GetForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.GetForUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) GetFromMaster(id string) (*model.FileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.GetFromMaster")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.GetFromMaster(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) GetStorageUsage(allowFromCache bool, includeDeleted bool) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.GetStorageUsage")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.GetStorageUsage(allowFromCache, includeDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) GetUptoNSizeFileTime(n int64) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.GetUptoNSizeFileTime")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.GetUptoNSizeFileTime(n)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) GetWithOptions(page int, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.GetWithOptions")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.GetWithOptions(page, perPage, opt)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) InvalidateFileInfosForPostCache(postID string, deleted bool) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.InvalidateFileInfosForPostCache")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.FileInfoStore.InvalidateFileInfosForPostCache(postID, deleted)
}
func (s *OpenTracingLayerFileInfoStore) PermanentDelete(fileID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.PermanentDelete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.FileInfoStore.PermanentDelete(fileID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerFileInfoStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.PermanentDeleteBatch")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.PermanentDeleteBatch(endTime, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) PermanentDeleteByUser(userID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.PermanentDeleteByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.PermanentDeleteByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) Save(info *model.FileInfo) (*model.FileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.Save(info)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) Search(paramsList []*model.SearchParams, userID string, teamID string, page int, perPage int) (*model.FileInfoList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.Search")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.Search(paramsList, userID, teamID, page, perPage)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerFileInfoStore) SetContent(fileID string, content string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.SetContent")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.FileInfoStore.SetContent(fileID, content)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerFileInfoStore) Upsert(info *model.FileInfo) (*model.FileInfo, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "FileInfoStore.Upsert")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.FileInfoStore.Upsert(info)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) AdminRoleGroupsForSyncableMember(userID string, syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.AdminRoleGroupsForSyncableMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.AdminRoleGroupsForSyncableMember(userID, syncableID, syncableType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.ChannelMembersMinusGroupMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.ChannelMembersMinusGroupMembers(channelID, groupIDs, page, perPage)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) ChannelMembersToAdd(since int64, channelID *string, includeRemovedMembers bool) ([]*model.UserChannelIDPair, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.ChannelMembersToAdd")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.ChannelMembersToAdd(since, channelID, includeRemovedMembers)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) ChannelMembersToRemove(channelID *string) ([]*model.ChannelMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.ChannelMembersToRemove")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.ChannelMembersToRemove(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) CountChannelMembersMinusGroupMembers(channelID string, groupIDs []string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.CountChannelMembersMinusGroupMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.CountChannelMembersMinusGroupMembers(channelID, groupIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) CountGroupsByChannel(channelID string, opts model.GroupSearchOpts) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.CountGroupsByChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.CountGroupsByChannel(channelID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) CountGroupsByTeam(teamID string, opts model.GroupSearchOpts) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.CountGroupsByTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.CountGroupsByTeam(teamID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) CountTeamMembersMinusGroupMembers(teamID string, groupIDs []string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.CountTeamMembersMinusGroupMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.CountTeamMembersMinusGroupMembers(teamID, groupIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) Create(group *model.Group) (*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.Create")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.Create(group)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) CreateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.CreateGroupSyncable")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.CreateGroupSyncable(groupSyncable)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) CreateWithUserIds(group *model.GroupWithUserIds) (*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.CreateWithUserIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.CreateWithUserIds(group)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) Delete(groupID string) (*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.Delete(groupID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.DeleteGroupSyncable")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.DeleteGroupSyncable(groupID, syncableID, syncableType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) DeleteMember(groupID string, userID string) (*model.GroupMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.DeleteMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.DeleteMember(groupID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) DeleteMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.DeleteMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.DeleteMembers(groupID, userIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) DistinctGroupMemberCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.DistinctGroupMemberCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.DistinctGroupMemberCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) DistinctGroupMemberCountForSource(source model.GroupSource) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.DistinctGroupMemberCountForSource")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.DistinctGroupMemberCountForSource(source)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) Get(groupID string) (*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.Get(groupID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetAllBySource(groupSource model.GroupSource) ([]*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetAllBySource")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetAllBySource(groupSource)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetAllGroupSyncablesByGroupId(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetAllGroupSyncablesByGroupId")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetAllGroupSyncablesByGroupId(groupID, syncableType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetByIDs(groupIDs []string) ([]*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetByIDs")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetByIDs(groupIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetByName(name string, opts model.GroupSearchOpts) (*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetByName(name, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetByRemoteID")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetByRemoteID(remoteID, groupSource)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetByUser(userID string) ([]*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetGroupSyncable")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetGroupSyncable(groupID, syncableID, syncableType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetGroups(page int, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetGroups")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetGroups(page, perPage, opts, viewRestrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetGroupsAssociatedToChannelsByTeam(teamID string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetGroupsAssociatedToChannelsByTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetGroupsAssociatedToChannelsByTeam(teamID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetGroupsByChannel(channelID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetGroupsByChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetGroupsByChannel(channelID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetGroupsByTeam(teamID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetGroupsByTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetGroupsByTeam(teamID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetMember(groupID string, userID string) (*model.GroupMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetMember(groupID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetMemberCount(groupID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMemberCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetMemberCount(groupID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetMemberCountWithRestrictions(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMemberCountWithRestrictions")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetMemberCountWithRestrictions(groupID, viewRestrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetMemberUsers(groupID string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMemberUsers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetMemberUsers(groupID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetMemberUsersInTeam(groupID string, teamID string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMemberUsersInTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetMemberUsersInTeam(groupID, teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetMemberUsersNotInChannel(groupID string, channelID string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMemberUsersNotInChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetMemberUsersNotInChannel(groupID, channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMemberUsersPage")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetMemberUsersPage(groupID, page, perPage, viewRestrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetMemberUsersSortedPage")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, teammateNameDisplay)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GetNonMemberUsersPage")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GetNonMemberUsersPage(groupID, page, perPage, viewRestrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GroupChannelCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GroupChannelCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GroupChannelCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GroupCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GroupCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GroupCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GroupCountBySource(source model.GroupSource) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GroupCountBySource")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GroupCountBySource(source)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GroupCountWithAllowReference() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GroupCountWithAllowReference")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GroupCountWithAllowReference()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GroupMemberCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GroupMemberCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GroupMemberCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) GroupTeamCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.GroupTeamCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.GroupTeamCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) PermanentDeleteMembersByUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.PermanentDeleteMembersByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.GroupStore.PermanentDeleteMembersByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerGroupStore) PermittedSyncableAdmins(syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.PermittedSyncableAdmins")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.PermittedSyncableAdmins(syncableID, syncableType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) Restore(groupID string) (*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.Restore")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.Restore(groupID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.TeamMembersMinusGroupMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.TeamMembersMinusGroupMembers(teamID, groupIDs, page, perPage)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) TeamMembersToAdd(since int64, teamID *string, includeRemovedMembers bool) ([]*model.UserTeamIDPair, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.TeamMembersToAdd")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.TeamMembersToAdd(since, teamID, includeRemovedMembers)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) TeamMembersToRemove(teamID *string) ([]*model.TeamMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.TeamMembersToRemove")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.TeamMembersToRemove(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) Update(group *model.Group) (*model.Group, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.Update(group)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.UpdateGroupSyncable")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.UpdateGroupSyncable(groupSyncable)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) UpsertMember(groupID string, userID string) (*model.GroupMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.UpsertMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.UpsertMember(groupID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerGroupStore) UpsertMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "GroupStore.UpsertMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.GroupStore.UpsertMembers(groupID, userIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) Cleanup(expiryTime int64, batchSize int) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.Cleanup")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.JobStore.Cleanup(expiryTime, batchSize)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerJobStore) Delete(id string) (string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.Delete(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) Get(id string) (*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.Get(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) GetAllByStatus(status string) ([]*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetAllByStatus")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.GetAllByStatus(status)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) GetAllByType(jobType string) ([]*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetAllByType")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.GetAllByType(jobType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) GetAllByTypeAndStatus(jobType string, status string) ([]*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetAllByTypeAndStatus")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.GetAllByTypeAndStatus(jobType, status)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) GetAllByTypePage(jobType string, offset int, limit int) ([]*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetAllByTypePage")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.GetAllByTypePage(jobType, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) GetAllByTypesPage(jobTypes []string, offset int, limit int) ([]*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetAllByTypesPage")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.GetAllByTypesPage(jobTypes, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) GetAllPage(offset int, limit int) ([]*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetAllPage")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.GetAllPage(offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) GetCountByStatusAndType(status string, jobType string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetCountByStatusAndType")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.GetCountByStatusAndType(status, jobType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) GetNewestJobByStatusAndType(status string, jobType string) (*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetNewestJobByStatusAndType")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.GetNewestJobByStatusAndType(status, jobType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) GetNewestJobByStatusesAndType(statuses []string, jobType string) (*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetNewestJobByStatusesAndType")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.GetNewestJobByStatusesAndType(statuses, jobType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) Save(job *model.Job) (*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.Save(job)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) UpdateOptimistically(job *model.Job, currentStatus string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.UpdateOptimistically")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.UpdateOptimistically(job, currentStatus)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) UpdateStatus(id string, status string) (*model.Job, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.UpdateStatus")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.UpdateStatus(id, status)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerJobStore) UpdateStatusOptimistically(id string, currentStatus string, newStatus string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.UpdateStatusOptimistically")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.JobStore.UpdateStatusOptimistically(id, currentStatus, newStatus)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerLicenseStore) Get(id string) (*model.LicenseRecord, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "LicenseStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.LicenseStore.Get(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerLicenseStore) GetAll() ([]*model.LicenseRecord, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "LicenseStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.LicenseStore.GetAll()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerLicenseStore) Save(license *model.LicenseRecord) (*model.LicenseRecord, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "LicenseStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.LicenseStore.Save(license)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerLinkMetadataStore) Get(url string, timestamp int64) (*model.LinkMetadata, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "LinkMetadataStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.LinkMetadataStore.Get(url, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerLinkMetadataStore) Save(linkMetadata *model.LinkMetadata) (*model.LinkMetadata, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "LinkMetadataStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.LinkMetadataStore.Save(linkMetadata)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerNotifyAdminStore) DeleteBefore(trial bool, now int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "NotifyAdminStore.DeleteBefore")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.NotifyAdminStore.DeleteBefore(trial, now)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerNotifyAdminStore) Get(trial bool) ([]*model.NotifyAdminData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "NotifyAdminStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.NotifyAdminStore.Get(trial)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostFeature) ([]*model.NotifyAdminData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "NotifyAdminStore.GetDataByUserIdAndFeature")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.NotifyAdminStore.GetDataByUserIdAndFeature(userId, feature)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerNotifyAdminStore) Save(data *model.NotifyAdminData) (*model.NotifyAdminData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "NotifyAdminStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.NotifyAdminStore.Save(data)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerNotifyAdminStore) Update(userId string, requiredPlan string, requiredFeature model.MattermostFeature, now int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "NotifyAdminStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.NotifyAdminStore.Update(userId, requiredPlan, requiredFeature, now)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerOAuthStore) DeleteApp(id string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.DeleteApp")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.OAuthStore.DeleteApp(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerOAuthStore) GetAccessData(token string) (*model.AccessData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.GetAccessData")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.GetAccessData(token)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) GetAccessDataByRefreshToken(token string) (*model.AccessData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.GetAccessDataByRefreshToken")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.GetAccessDataByRefreshToken(token)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) GetAccessDataByUserForApp(userID string, clientId string) ([]*model.AccessData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.GetAccessDataByUserForApp")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.GetAccessDataByUserForApp(userID, clientId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) GetApp(id string) (*model.OAuthApp, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.GetApp")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.GetApp(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) GetAppByUser(userID string, offset int, limit int) ([]*model.OAuthApp, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.GetAppByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.GetAppByUser(userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) GetApps(offset int, limit int) ([]*model.OAuthApp, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.GetApps")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.GetApps(offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) GetAuthData(code string) (*model.AuthData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.GetAuthData")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.GetAuthData(code)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) GetAuthorizedApps(userID string, offset int, limit int) ([]*model.OAuthApp, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.GetAuthorizedApps")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.GetAuthorizedApps(userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) GetPreviousAccessData(userID string, clientId string) (*model.AccessData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.GetPreviousAccessData")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.GetPreviousAccessData(userID, clientId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) PermanentDeleteAuthDataByUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.PermanentDeleteAuthDataByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.OAuthStore.PermanentDeleteAuthDataByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerOAuthStore) RemoveAccessData(token string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.RemoveAccessData")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.OAuthStore.RemoveAccessData(token)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerOAuthStore) RemoveAllAccessData() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.RemoveAllAccessData")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.OAuthStore.RemoveAllAccessData()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerOAuthStore) RemoveAuthData(code string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.RemoveAuthData")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.OAuthStore.RemoveAuthData(code)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerOAuthStore) RemoveAuthDataByClientId(clientId string, userId string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.RemoveAuthDataByClientId")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.OAuthStore.RemoveAuthDataByClientId(clientId, userId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerOAuthStore) SaveAccessData(accessData *model.AccessData) (*model.AccessData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.SaveAccessData")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.SaveAccessData(accessData)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) SaveApp(app *model.OAuthApp) (*model.OAuthApp, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.SaveApp")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.SaveApp(app)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) SaveAuthData(authData *model.AuthData) (*model.AuthData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.SaveAuthData")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.SaveAuthData(authData)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) UpdateAccessData(accessData *model.AccessData) (*model.AccessData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.UpdateAccessData")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.UpdateAccessData(accessData)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerOAuthStore) UpdateApp(app *model.OAuthApp) (*model.OAuthApp, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.UpdateApp")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.OAuthStore.UpdateApp(app)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPluginStore) CompareAndDelete(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PluginStore.CompareAndDelete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PluginStore.CompareAndDelete(keyVal, oldValue)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPluginStore) CompareAndSet(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PluginStore.CompareAndSet")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PluginStore.CompareAndSet(keyVal, oldValue)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPluginStore) Delete(pluginID string, key string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PluginStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PluginStore.Delete(pluginID, key)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPluginStore) DeleteAllExpired() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PluginStore.DeleteAllExpired")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PluginStore.DeleteAllExpired()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPluginStore) DeleteAllForPlugin(PluginID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PluginStore.DeleteAllForPlugin")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PluginStore.DeleteAllForPlugin(PluginID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPluginStore) Get(pluginID string, key string) (*model.PluginKeyValue, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PluginStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PluginStore.Get(pluginID, key)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPluginStore) List(pluginID string, page int, perPage int) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PluginStore.List")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PluginStore.List(pluginID, page, perPage)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPluginStore) SaveOrUpdate(keyVal *model.PluginKeyValue) (*model.PluginKeyValue, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PluginStore.SaveOrUpdate")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PluginStore.SaveOrUpdate(keyVal)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPluginStore) SetWithOptions(pluginID string, key string, value []byte, options model.PluginKVSetOptions) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PluginStore.SetWithOptions")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PluginStore.SetWithOptions(pluginID, key, value, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) AnalyticsPostCount(options *model.PostCountOptions) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.AnalyticsPostCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.AnalyticsPostCount(options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) AnalyticsPostCountsByDay(options *model.AnalyticsPostCountsOptions) (model.AnalyticsRows, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.AnalyticsPostCountsByDay")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.AnalyticsPostCountsByDay(options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) AnalyticsUserCountsWithPostsByDay(teamID string) (model.AnalyticsRows, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.AnalyticsUserCountsWithPostsByDay")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.AnalyticsUserCountsWithPostsByDay(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) ClearCaches() {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.ClearCaches")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.PostStore.ClearCaches()
}
func (s *OpenTracingLayerPostStore) Delete(postID string, timestamp int64, deleteByID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PostStore.Delete(postID, timestamp, deleteByID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPostStore) DeleteOrphanedRows(limit int) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.DeleteOrphanedRows")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.DeleteOrphanedRows(limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.Get(ctx, id, opts, userID, sanitizeOptions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetDirectPostParentsForExportAfter(limit int, afterID string) ([]*model.DirectPostForExport, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetDirectPostParentsForExportAfter")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetDirectPostParentsForExportAfter(limit, afterID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetEditHistoryForPost(postId string) ([]*model.Post, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetEditHistoryForPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetEditHistoryForPost(postId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetEtag(channelID string, allowFromCache bool, collapsedThreads bool) string {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetEtag")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result := s.PostStore.GetEtag(channelID, allowFromCache, collapsedThreads)
return result
}
func (s *OpenTracingLayerPostStore) GetFlaggedPosts(userID string, offset int, limit int) (*model.PostList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetFlaggedPosts")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetFlaggedPosts(userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetFlaggedPostsForChannel(userID string, channelID string, offset int, limit int) (*model.PostList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetFlaggedPostsForChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetFlaggedPostsForChannel(userID, channelID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetFlaggedPostsForTeam(userID string, teamID string, offset int, limit int) (*model.PostList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetFlaggedPostsForTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
span.SetTag("userID", userID)
span.SetTag("teamID", teamID)
span.SetTag("offset", offset)
span.SetTag("limit", limit)
defer span.Finish()
result, err := s.PostStore.GetFlaggedPostsForTeam(userID, teamID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetMaxPostSize() int {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetMaxPostSize")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result := s.PostStore.GetMaxPostSize()
return result
}
func (s *OpenTracingLayerPostStore) GetNthRecentPostTime(n int64) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetNthRecentPostTime")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetNthRecentPostTime(n)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetOldest() (*model.Post, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetOldest")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetOldest()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetOldestEntityCreationTime() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetOldestEntityCreationTime")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetOldestEntityCreationTime()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetParentsForExportAfter(limit int, afterID string) ([]*model.PostForExport, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetParentsForExportAfter")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetParentsForExportAfter(limit, afterID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostAfterTime(channelID string, timestamp int64, collapsedThreads bool) (*model.Post, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostAfterTime")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostAfterTime(channelID, timestamp, collapsedThreads)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostIdAfterTime(channelID string, timestamp int64, collapsedThreads bool) (string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostIdAfterTime")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostIdAfterTime(channelID, timestamp, collapsedThreads)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostIdBeforeTime(channelID string, timestamp int64, collapsedThreads bool) (string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostIdBeforeTime")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostIdBeforeTime(channelID, timestamp, collapsedThreads)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostReminderMetadata(postID string) (*store.PostReminderMetadata, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostReminderMetadata")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostReminderMetadata(postID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostReminders(now int64) ([]*model.PostReminder, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostReminders")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostReminders(now)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPosts(options model.GetPostsOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPosts")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPosts(options, allowFromCache, sanitizeOptions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostsAfter(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostsAfter")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostsAfter(options, sanitizeOptions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostsBatchForIndexing(startTime int64, startPostID string, limit int) ([]*model.PostForIndexing, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostsBatchForIndexing")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostsBatchForIndexing(startTime, startPostID, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostsBefore(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostsBefore")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostsBefore(options, sanitizeOptions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostsByIds(postIds []string) ([]*model.Post, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostsByIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostsByIds(postIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostsByThread(threadID string, since int64) ([]*model.Post, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostsByThread")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostsByThread(threadID, since)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostsCreatedAt(channelID string, timestamp int64) ([]*model.Post, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostsCreatedAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostsCreatedAt(channelID, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostsSince(options model.GetPostsSinceOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostsSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetPostsSince(options, allowFromCache, sanitizeOptions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetPostsSinceForSync(options model.GetPostsSinceForSyncOptions, cursor model.GetPostsSinceForSyncCursor, limit int) ([]*model.Post, model.GetPostsSinceForSyncCursor, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetPostsSinceForSync")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.PostStore.GetPostsSinceForSync(options, cursor, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerPostStore) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetRecentSearchesForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetRecentSearchesForUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetRepliesForExport(parentID string) ([]*model.ReplyForExport, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetRepliesForExport")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetRepliesForExport(parentID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetSingle(id string, inclDeleted bool) (*model.Post, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetSingle")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetSingle(id, inclDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetTopDMsForUserSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetTopDMsForUserSince(userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.HasAutoResponsePostByUserSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.HasAutoResponsePostByUserSince(options, userId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) InvalidateLastPostTimeCache(channelID string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.InvalidateLastPostTimeCache")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.PostStore.InvalidateLastPostTimeCache(channelID)
}
func (s *OpenTracingLayerPostStore) LogRecentSearch(userID string, searchQuery []byte, createAt int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.LogRecentSearch")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PostStore.LogRecentSearch(userID, searchQuery, createAt)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPostStore) Overwrite(post *model.Post) (*model.Post, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.Overwrite")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.Overwrite(post)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) OverwriteMultiple(posts []*model.Post) ([]*model.Post, int, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.OverwriteMultiple")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.PostStore.OverwriteMultiple(posts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerPostStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.PermanentDeleteBatch")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.PermanentDeleteBatch(endTime, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.PermanentDeleteBatchForRetentionPolicies")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.PostStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerPostStore) PermanentDeleteByChannel(channelID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.PermanentDeleteByChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PostStore.PermanentDeleteByChannel(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPostStore) PermanentDeleteByUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.PermanentDeleteByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PostStore.PermanentDeleteByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPostStore) Save(post *model.Post) (*model.Post, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.Save(post)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, int, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.SaveMultiple")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.PostStore.SaveMultiple(posts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerPostStore) Search(teamID string, userID string, params *model.SearchParams) (*model.PostList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.Search")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.Search(teamID, userID, params)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) SearchPostsForUser(paramsList []*model.SearchParams, userID string, teamID string, page int, perPage int) (*model.PostSearchResults, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.SearchPostsForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.SearchPostsForUser(paramsList, userID, teamID, page, perPage)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) SetPostReminder(reminder *model.PostReminder) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.SetPostReminder")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PostStore.SetPostReminder(reminder)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPostStore) Update(newPost *model.Post, oldPost *model.Post) (*model.Post, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.Update(newPost, oldPost)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostAcknowledgementStore) Delete(acknowledgement *model.PostAcknowledgement) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostAcknowledgementStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PostAcknowledgementStore.Delete(acknowledgement)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPostAcknowledgementStore) Get(postID string, userID string) (*model.PostAcknowledgement, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostAcknowledgementStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostAcknowledgementStore.Get(postID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostAcknowledgementStore) GetForPost(postID string) ([]*model.PostAcknowledgement, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostAcknowledgementStore.GetForPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostAcknowledgementStore.GetForPost(postID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostAcknowledgementStore) GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostAcknowledgementStore.GetForPosts")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostAcknowledgementStore.GetForPosts(postIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostAcknowledgementStore) Save(postID string, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostAcknowledgementStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostAcknowledgementStore.Save(postID, userID, acknowledgedAt)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostPriorityStore.GetForPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostPriorityStore.GetForPost(postId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostPriorityStore) GetForPosts(ids []string) ([]*model.PostPriority, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostPriorityStore.GetForPosts")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostPriorityStore.GetForPosts(ids)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPreferenceStore) CleanupFlagsBatch(limit int64) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.CleanupFlagsBatch")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PreferenceStore.CleanupFlagsBatch(limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPreferenceStore) Delete(userID string, category string, name string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PreferenceStore.Delete(userID, category, name)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPreferenceStore) DeleteCategory(userID string, category string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.DeleteCategory")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PreferenceStore.DeleteCategory(userID, category)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPreferenceStore) DeleteCategoryAndName(category string, name string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.DeleteCategoryAndName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PreferenceStore.DeleteCategoryAndName(category, name)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPreferenceStore) DeleteOrphanedRows(limit int) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.DeleteOrphanedRows")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PreferenceStore.DeleteOrphanedRows(limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPreferenceStore) Get(userID string, category string, name string) (*model.Preference, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PreferenceStore.Get(userID, category, name)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPreferenceStore) GetAll(userID string) (model.Preferences, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PreferenceStore.GetAll(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPreferenceStore) GetCategory(userID string, category string) (model.Preferences, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.GetCategory")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PreferenceStore.GetCategory(userID, category)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPreferenceStore) GetCategoryAndName(category string, nane string) (model.Preferences, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.GetCategoryAndName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PreferenceStore.GetCategoryAndName(category, nane)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPreferenceStore) PermanentDeleteByUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.PermanentDeleteByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PreferenceStore.PermanentDeleteByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPreferenceStore) Save(preferences model.Preferences) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PreferenceStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PreferenceStore.Save(preferences)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerProductNoticesStore) Clear(notices []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ProductNoticesStore.Clear")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ProductNoticesStore.Clear(notices)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerProductNoticesStore) ClearOldNotices(currentNotices model.ProductNotices) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ProductNoticesStore.ClearOldNotices")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ProductNoticesStore.ClearOldNotices(currentNotices)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerProductNoticesStore) GetViews(userID string) ([]model.ProductNoticeViewState, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ProductNoticesStore.GetViews")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ProductNoticesStore.GetViews(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerProductNoticesStore) View(userID string, notices []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ProductNoticesStore.View")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ProductNoticesStore.View(userID, notices)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerReactionStore) BulkGetForPosts(postIds []string) ([]*model.Reaction, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.BulkGetForPosts")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ReactionStore.BulkGetForPosts(postIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerReactionStore) Delete(reaction *model.Reaction) (*model.Reaction, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ReactionStore.Delete(reaction)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerReactionStore) DeleteAllWithEmojiName(emojiName string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.DeleteAllWithEmojiName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ReactionStore.DeleteAllWithEmojiName(emojiName)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerReactionStore) DeleteOrphanedRows(limit int) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.DeleteOrphanedRows")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ReactionStore.DeleteOrphanedRows(limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerReactionStore) GetForPost(postID string, allowFromCache bool) ([]*model.Reaction, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.GetForPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ReactionStore.GetForPost(postID, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerReactionStore) GetForPostSince(postId string, since int64, excludeRemoteId string, inclDeleted bool) ([]*model.Reaction, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.GetForPostSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ReactionStore.GetForPostSince(postId, since, excludeRemoteId, inclDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerReactionStore) GetTopForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.GetTopForTeamSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ReactionStore.GetTopForTeamSince(teamID, userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerReactionStore) GetTopForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.GetTopForUserSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ReactionStore.GetTopForUserSince(userID, teamID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerReactionStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.PermanentDeleteBatch")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ReactionStore.PermanentDeleteBatch(endTime, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ReactionStore.Save(reaction)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRemoteClusterStore) Delete(remoteClusterId string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RemoteClusterStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RemoteClusterStore.Delete(remoteClusterId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRemoteClusterStore) Get(remoteClusterId string) (*model.RemoteCluster, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RemoteClusterStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RemoteClusterStore.Get(remoteClusterId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRemoteClusterStore) GetAll(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RemoteClusterStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RemoteClusterStore.GetAll(filter)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRemoteClusterStore) Save(rc *model.RemoteCluster) (*model.RemoteCluster, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RemoteClusterStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RemoteClusterStore.Save(rc)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRemoteClusterStore) SetLastPingAt(remoteClusterId string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RemoteClusterStore.SetLastPingAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.RemoteClusterStore.SetLastPingAt(remoteClusterId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerRemoteClusterStore) Update(rc *model.RemoteCluster) (*model.RemoteCluster, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RemoteClusterStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RemoteClusterStore.Update(rc)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRemoteClusterStore) UpdateTopics(remoteClusterId string, topics string) (*model.RemoteCluster, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RemoteClusterStore.UpdateTopics")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RemoteClusterStore.UpdateTopics(remoteClusterId, topics)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) AddChannels(policyId string, channelIds []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.AddChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.RetentionPolicyStore.AddChannels(policyId, channelIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerRetentionPolicyStore) AddTeams(policyId string, teamIds []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.AddTeams")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.RetentionPolicyStore.AddTeams(policyId, teamIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerRetentionPolicyStore) Delete(id string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.RetentionPolicyStore.Delete(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerRetentionPolicyStore) DeleteOrphanedRows(limit int) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.DeleteOrphanedRows")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.DeleteOrphanedRows(limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) Get(id string) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.Get(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) GetAll(offset int, limit int) ([]*model.RetentionPolicyWithTeamAndChannelCounts, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.GetAll(offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) GetChannelPoliciesCountForUser(userID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetChannelPoliciesCountForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.GetChannelPoliciesCountForUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) GetChannelPoliciesForUser(userID string, offset int, limit int) ([]*model.RetentionPolicyForChannel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetChannelPoliciesForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.GetChannelPoliciesForUser(userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) GetChannels(policyId string, offset int, limit int) (model.ChannelListWithTeamData, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.GetChannels(policyId, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) GetChannelsCount(policyId string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetChannelsCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.GetChannelsCount(policyId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) GetCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.GetCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetTeamPoliciesCountForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.GetTeamPoliciesCountForUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) GetTeamPoliciesForUser(userID string, offset int, limit int) ([]*model.RetentionPolicyForTeam, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetTeamPoliciesForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.GetTeamPoliciesForUser(userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) GetTeams(policyId string, offset int, limit int) ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetTeams")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.GetTeams(policyId, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) GetTeamsCount(policyId string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetTeamsCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.GetTeamsCount(policyId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) Patch(patch *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.Patch")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.Patch(patch)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRetentionPolicyStore) RemoveChannels(policyId string, channelIds []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.RemoveChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.RetentionPolicyStore.RemoveChannels(policyId, channelIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerRetentionPolicyStore) RemoveTeams(policyId string, teamIds []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.RemoveTeams")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.RetentionPolicyStore.RemoveTeams(policyId, teamIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerRetentionPolicyStore) Save(policy *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RetentionPolicyStore.Save(policy)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRoleStore) AllChannelSchemeRoles() ([]*model.Role, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.AllChannelSchemeRoles")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RoleStore.AllChannelSchemeRoles()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRoleStore) ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.ChannelHigherScopedPermissions")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RoleStore.ChannelHigherScopedPermissions(roleNames)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRoleStore) ChannelRolesUnderTeamRole(roleName string) ([]*model.Role, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.ChannelRolesUnderTeamRole")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RoleStore.ChannelRolesUnderTeamRole(roleName)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRoleStore) Delete(roleID string) (*model.Role, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RoleStore.Delete(roleID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRoleStore) Get(roleID string) (*model.Role, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RoleStore.Get(roleID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRoleStore) GetAll() ([]*model.Role, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RoleStore.GetAll()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRoleStore) GetByName(ctx context.Context, name string) (*model.Role, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.GetByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RoleStore.GetByName(ctx, name)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRoleStore) GetByNames(names []string) ([]*model.Role, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.GetByNames")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RoleStore.GetByNames(names)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerRoleStore) PermanentDeleteAll() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.PermanentDeleteAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.RoleStore.PermanentDeleteAll()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerRoleStore) Save(role *model.Role) (*model.Role, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RoleStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.RoleStore.Save(role)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSchemeStore) CountByScope(scope string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SchemeStore.CountByScope")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SchemeStore.CountByScope(scope)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSchemeStore) CountWithoutPermission(scope string, permissionID string, roleScope model.RoleScope, roleType model.RoleType) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SchemeStore.CountWithoutPermission")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SchemeStore.CountWithoutPermission(scope, permissionID, roleScope, roleType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSchemeStore) Delete(schemeID string) (*model.Scheme, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SchemeStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SchemeStore.Delete(schemeID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSchemeStore) Get(schemeID string) (*model.Scheme, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SchemeStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SchemeStore.Get(schemeID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSchemeStore) GetAllPage(scope string, offset int, limit int) ([]*model.Scheme, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SchemeStore.GetAllPage")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SchemeStore.GetAllPage(scope, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSchemeStore) GetByName(schemeName string) (*model.Scheme, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SchemeStore.GetByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SchemeStore.GetByName(schemeName)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSchemeStore) PermanentDeleteAll() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SchemeStore.PermanentDeleteAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SchemeStore.PermanentDeleteAll()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSchemeStore) Save(scheme *model.Scheme) (*model.Scheme, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SchemeStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SchemeStore.Save(scheme)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSessionStore) AnalyticsSessionCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.AnalyticsSessionCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SessionStore.AnalyticsSessionCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSessionStore) Cleanup(expiryTime int64, batchSize int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.Cleanup")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SessionStore.Cleanup(expiryTime, batchSize)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSessionStore) Get(ctx context.Context, sessionIDOrToken string) (*model.Session, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SessionStore.Get(ctx, sessionIDOrToken)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSessionStore) GetSessions(userID string) ([]*model.Session, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.GetSessions")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SessionStore.GetSessions(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSessionStore) GetSessionsExpired(thresholdMillis int64, mobileOnly bool, unnotifiedOnly bool) ([]*model.Session, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.GetSessionsExpired")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SessionStore.GetSessionsExpired(thresholdMillis, mobileOnly, unnotifiedOnly)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSessionStore) GetSessionsWithActiveDeviceIds(userID string) ([]*model.Session, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.GetSessionsWithActiveDeviceIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SessionStore.GetSessionsWithActiveDeviceIds(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSessionStore) PermanentDeleteSessionsByUser(teamID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.PermanentDeleteSessionsByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SessionStore.PermanentDeleteSessionsByUser(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSessionStore) Remove(sessionIDOrToken string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.Remove")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SessionStore.Remove(sessionIDOrToken)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSessionStore) RemoveAllSessions() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.RemoveAllSessions")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SessionStore.RemoveAllSessions()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSessionStore) Save(session *model.Session) (*model.Session, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SessionStore.Save(session)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSessionStore) UpdateDeviceId(id string, deviceID string, expiresAt int64) (string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.UpdateDeviceId")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SessionStore.UpdateDeviceId(id, deviceID, expiresAt)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSessionStore) UpdateExpiredNotify(sessionid string, notified bool) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.UpdateExpiredNotify")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SessionStore.UpdateExpiredNotify(sessionid, notified)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSessionStore) UpdateExpiresAt(sessionID string, timestamp int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.UpdateExpiresAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SessionStore.UpdateExpiresAt(sessionID, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSessionStore) UpdateLastActivityAt(sessionID string, timestamp int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.UpdateLastActivityAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SessionStore.UpdateLastActivityAt(sessionID, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSessionStore) UpdateProps(session *model.Session) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.UpdateProps")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SessionStore.UpdateProps(session)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSessionStore) UpdateRoles(userID string, roles string) (string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.UpdateRoles")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SessionStore.UpdateRoles(userID, roles)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) Delete(channelId string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.Delete(channelId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) DeleteRemote(remoteId string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.DeleteRemote")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.DeleteRemote(remoteId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) Get(channelId string) (*model.SharedChannel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.Get(channelId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetAll(offset int, limit int, opts model.SharedChannelFilterOpts) ([]*model.SharedChannel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetAll(offset, limit, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetAllCount(opts model.SharedChannelFilterOpts) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetAllCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetAllCount(opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetAttachment(fileId string, remoteId string) (*model.SharedChannelAttachment, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetAttachment")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetAttachment(fileId, remoteId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetRemote(id string) (*model.SharedChannelRemote, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetRemote")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetRemote(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetRemoteByIds(channelId string, remoteId string) (*model.SharedChannelRemote, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetRemoteByIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetRemoteByIds(channelId, remoteId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetRemoteForUser(remoteId string, userId string) (*model.RemoteCluster, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetRemoteForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetRemoteForUser(remoteId, userId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetRemotes")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetRemotes(opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetRemotesStatus(channelId string) ([]*model.SharedChannelRemoteStatus, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetRemotesStatus")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetRemotesStatus(channelId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetSingleUser(userID string, channelID string, remoteID string) (*model.SharedChannelUser, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetSingleUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetSingleUser(userID, channelID, remoteID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetUsersForSync(filter model.GetUsersForSyncFilter) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetUsersForSync")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetUsersForSync(filter)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) GetUsersForUser(userID string) ([]*model.SharedChannelUser, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.GetUsersForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.GetUsersForUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) HasChannel(channelID string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.HasChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.HasChannel(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) HasRemote(channelID string, remoteId string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.HasRemote")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.HasRemote(channelID, remoteId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) Save(sc *model.SharedChannel) (*model.SharedChannel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.Save(sc)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) SaveAttachment(remote *model.SharedChannelAttachment) (*model.SharedChannelAttachment, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.SaveAttachment")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.SaveAttachment(remote)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) SaveRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.SaveRemote")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.SaveRemote(remote)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) SaveUser(remote *model.SharedChannelUser) (*model.SharedChannelUser, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.SaveUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.SaveUser(remote)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) Update(sc *model.SharedChannel) (*model.SharedChannel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.Update(sc)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) UpdateAttachmentLastSyncAt(id string, syncTime int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.UpdateAttachmentLastSyncAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SharedChannelStore.UpdateAttachmentLastSyncAt(id, syncTime)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSharedChannelStore) UpdateRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.UpdateRemote")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.UpdateRemote(remote)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSharedChannelStore) UpdateRemoteCursor(id string, cursor model.GetPostsSinceForSyncCursor) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.UpdateRemoteCursor")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SharedChannelStore.UpdateRemoteCursor(id, cursor)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSharedChannelStore) UpdateUserLastSyncAt(userID string, channelID string, remoteID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.UpdateUserLastSyncAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SharedChannelStore.UpdateUserLastSyncAt(userID, channelID, remoteID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSharedChannelStore) UpsertAttachment(remote *model.SharedChannelAttachment) (string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SharedChannelStore.UpsertAttachment")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SharedChannelStore.UpsertAttachment(remote)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerStatusStore) Get(userID string) (*model.Status, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "StatusStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.StatusStore.Get(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerStatusStore) GetByIds(userIds []string) ([]*model.Status, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "StatusStore.GetByIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.StatusStore.GetByIds(userIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerStatusStore) GetTotalActiveUsersCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "StatusStore.GetTotalActiveUsersCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.StatusStore.GetTotalActiveUsersCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerStatusStore) ResetAll() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "StatusStore.ResetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.StatusStore.ResetAll()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerStatusStore) SaveOrUpdate(status *model.Status) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "StatusStore.SaveOrUpdate")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.StatusStore.SaveOrUpdate(status)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerStatusStore) UpdateExpiredDNDStatuses() ([]*model.Status, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "StatusStore.UpdateExpiredDNDStatuses")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.StatusStore.UpdateExpiredDNDStatuses()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerStatusStore) UpdateLastActivityAt(userID string, lastActivityAt int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "StatusStore.UpdateLastActivityAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.StatusStore.UpdateLastActivityAt(userID, lastActivityAt)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSystemStore) Get() (model.StringMap, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SystemStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SystemStore.Get()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSystemStore) GetByName(name string) (*model.System, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SystemStore.GetByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SystemStore.GetByName(name)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSystemStore) InsertIfExists(system *model.System) (*model.System, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SystemStore.InsertIfExists")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SystemStore.InsertIfExists(system)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSystemStore) PermanentDeleteByName(name string) (*model.System, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SystemStore.PermanentDeleteByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SystemStore.PermanentDeleteByName(name)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSystemStore) Save(system *model.System) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SystemStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SystemStore.Save(system)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSystemStore) SaveOrUpdate(system *model.System) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SystemStore.SaveOrUpdate")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SystemStore.SaveOrUpdate(system)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSystemStore) SaveOrUpdateWithWarnMetricHandling(system *model.System) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SystemStore.SaveOrUpdateWithWarnMetricHandling")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SystemStore.SaveOrUpdateWithWarnMetricHandling(system)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSystemStore) Update(system *model.System) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SystemStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.SystemStore.Update(system)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTeamStore) AnalyticsGetTeamCountForScheme(schemeID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.AnalyticsGetTeamCountForScheme")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.AnalyticsGetTeamCountForScheme(schemeID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) AnalyticsTeamCount(opts *model.TeamSearch) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.AnalyticsTeamCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.AnalyticsTeamCount(opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) ClearAllCustomRoleAssignments() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.ClearAllCustomRoleAssignments")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TeamStore.ClearAllCustomRoleAssignments()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTeamStore) ClearCaches() {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.ClearCaches")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.TeamStore.ClearCaches()
}
func (s *OpenTracingLayerTeamStore) Get(id string) (*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.Get(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetActiveMemberCount(teamID string, restrictions *model.ViewUsersRestrictions) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetActiveMemberCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetActiveMemberCount(teamID, restrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetAll() ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetAll()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetAllForExportAfter(limit int, afterID string) ([]*model.TeamForExport, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetAllForExportAfter")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetAllForExportAfter(limit, afterID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetAllPage(offset int, limit int, opts *model.TeamSearch) ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetAllPage")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetAllPage(offset, limit, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetAllPrivateTeamListing() ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetAllPrivateTeamListing")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetAllPrivateTeamListing()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetAllTeamListing() ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetAllTeamListing")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetAllTeamListing()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetByEmptyInviteID() ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetByEmptyInviteID")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetByEmptyInviteID()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetByInviteId(inviteID string) (*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetByInviteId")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetByInviteId(inviteID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetByName(name string) (*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetByName")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetByName(name)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetByNames(name []string) ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetByNames")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetByNames(name)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetChannelUnreadsForAllTeams(excludeTeamID string, userID string) ([]*model.ChannelUnread, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetChannelUnreadsForAllTeams")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetChannelUnreadsForAllTeams(excludeTeamID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetChannelUnreadsForTeam(teamID string, userID string) ([]*model.ChannelUnread, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetChannelUnreadsForTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetChannelUnreadsForTeam(teamID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetCommonTeamIDsForTwoUsers(userID string, otherUserID string) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetCommonTeamIDsForTwoUsers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetCommonTeamIDsForTwoUsers(userID, otherUserID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetMany(ids []string) ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetMany")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetMany(ids)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetMember(ctx context.Context, teamID string, userID string) (*model.TeamMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetMember(ctx, teamID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetMembers(teamID string, offset int, limit int, teamMembersGetOptions *model.TeamMembersGetOptions) ([]*model.TeamMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetMembers(teamID, offset, limit, teamMembersGetOptions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetMembersByIds(teamID string, userIds []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetMembersByIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetMembersByIds(teamID, userIds, restrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetNewTeamMembersSince(teamID string, since int64, offset int, limit int) (*model.NewTeamMembersList, int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetNewTeamMembersSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.TeamStore.GetNewTeamMembersSince(teamID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerTeamStore) GetTeamMembersForExport(userID string) ([]*model.TeamMemberForExport, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetTeamMembersForExport")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetTeamMembersForExport(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetTeamsByScheme(schemeID string, offset int, limit int) ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetTeamsByScheme")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetTeamsByScheme(schemeID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetTeamsByUserId(userID string) ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetTeamsByUserId")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetTeamsByUserId(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetTeamsForUser(ctx context.Context, userID string, excludeTeamID string, includeDeleted bool) ([]*model.TeamMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetTeamsForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetTeamsForUser(ctx, userID, excludeTeamID, includeDeleted)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetTeamsForUserWithPagination(userID string, page int, perPage int) ([]*model.TeamMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetTeamsForUserWithPagination")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetTeamsForUserWithPagination(userID, page, perPage)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetTotalMemberCount(teamID string, restrictions *model.ViewUsersRestrictions) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetTotalMemberCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetTotalMemberCount(teamID, restrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetUserTeamIds(userID string, allowFromCache bool) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetUserTeamIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetUserTeamIds(userID, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GroupSyncedTeamCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GroupSyncedTeamCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GroupSyncedTeamCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) InvalidateAllTeamIdsForUser(userID string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.InvalidateAllTeamIdsForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.TeamStore.InvalidateAllTeamIdsForUser(userID)
}
func (s *OpenTracingLayerTeamStore) MigrateTeamMembers(fromTeamID string, fromUserID string) (map[string]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.MigrateTeamMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.MigrateTeamMembers(fromTeamID, fromUserID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) PermanentDelete(teamID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.PermanentDelete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TeamStore.PermanentDelete(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTeamStore) RemoveAllMembersByTeam(teamID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.RemoveAllMembersByTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TeamStore.RemoveAllMembersByTeam(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTeamStore) RemoveAllMembersByUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.RemoveAllMembersByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TeamStore.RemoveAllMembersByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTeamStore) RemoveMember(teamID string, userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.RemoveMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TeamStore.RemoveMember(teamID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTeamStore) RemoveMembers(teamID string, userIds []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.RemoveMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TeamStore.RemoveMembers(teamID, userIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTeamStore) ResetAllTeamSchemes() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.ResetAllTeamSchemes")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TeamStore.ResetAllTeamSchemes()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTeamStore) Save(team *model.Team) (*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.Save(team)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) SaveMember(member *model.TeamMember, maxUsersPerTeam int) (*model.TeamMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.SaveMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.SaveMember(member, maxUsersPerTeam)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) SaveMultipleMembers(members []*model.TeamMember, maxUsersPerTeam int) ([]*model.TeamMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.SaveMultipleMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.SaveMultipleMembers(members, maxUsersPerTeam)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) SearchAll(opts *model.TeamSearch) ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.SearchAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.SearchAll(opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) SearchAllPaged(opts *model.TeamSearch) ([]*model.Team, int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.SearchAllPaged")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.TeamStore.SearchAllPaged(opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerTeamStore) SearchOpen(opts *model.TeamSearch) ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.SearchOpen")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.SearchOpen(opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) SearchPrivate(opts *model.TeamSearch) ([]*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.SearchPrivate")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.SearchPrivate(opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) Update(team *model.Team) (*model.Team, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.Update(team)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) UpdateLastTeamIconUpdate(teamID string, curTime int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.UpdateLastTeamIconUpdate")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TeamStore.UpdateLastTeamIconUpdate(teamID, curTime)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTeamStore) UpdateMember(member *model.TeamMember) (*model.TeamMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.UpdateMember")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.UpdateMember(member)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) UpdateMembersRole(teamID string, userIDs []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.UpdateMembersRole")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TeamStore.UpdateMembersRole(teamID, userIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTeamStore) UpdateMultipleMembers(members []*model.TeamMember) ([]*model.TeamMember, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.UpdateMultipleMembers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.UpdateMultipleMembers(members)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) UserBelongsToTeams(userID string, teamIds []string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.UserBelongsToTeams")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.UserBelongsToTeams(userID, teamIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTermsOfServiceStore) Get(id string, allowFromCache bool) (*model.TermsOfService, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TermsOfServiceStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TermsOfServiceStore.Get(id, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTermsOfServiceStore) GetLatest(allowFromCache bool) (*model.TermsOfService, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TermsOfServiceStore.GetLatest")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TermsOfServiceStore.GetLatest(allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTermsOfServiceStore) Save(termsOfService *model.TermsOfService) (*model.TermsOfService, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TermsOfServiceStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TermsOfServiceStore.Save(termsOfService)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) DeleteMembershipForUser(userId string, postID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.DeleteMembershipForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ThreadStore.DeleteMembershipForUser(userId, postID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.DeleteOrphanedRows")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.DeleteOrphanedRows(limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) Get(id string) (*model.Thread, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.Get(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetMembershipForUser(userId string, postID string) (*model.ThreadMembership, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetMembershipForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetMembershipForUser(userId, postID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetMembershipsForUser(userId string, teamID string) ([]*model.ThreadMembership, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetMembershipsForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetMembershipsForUser(userId, teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string, includeUrgentMentionCount bool) (map[string]*model.TeamUnread, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTeamsUnreadForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetTeamsUnreadForUser(userID, teamIDs, includeUrgentMentionCount)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetThreadFollowers(threadID string, fetchOnlyActive bool) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetThreadFollowers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetThreadFollowers(threadID, fetchOnlyActive)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool, postPriorityIsEnabled bool) (*model.ThreadResponse, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetThreadForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetThreadForUser(threadMembership, extended, postPriorityIsEnabled)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetThreadUnreadReplyCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetThreadUnreadReplyCount(threadMembership)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetThreadsForUser(userId string, teamID string, opts model.GetUserThreadsOpts) ([]*model.ThreadResponse, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetThreadsForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetThreadsForUser(userId, teamID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTopThreadsForTeamSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetTopThreadsForTeamSince(teamID, userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTopThreadsForUserSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetTopThreadsForUserSince(teamID, userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetTotalThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTotalThreads")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetTotalThreads(userId, teamID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetTotalUnreadMentions(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTotalUnreadMentions")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetTotalUnreadMentions(userId, teamID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetTotalUnreadThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTotalUnreadThreads")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetTotalUnreadThreads(userId, teamID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetTotalUnreadUrgentMentions(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTotalUnreadUrgentMentions")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetTotalUnreadUrgentMentions(userId, teamID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) MaintainMembership(userID string, postID string, opts store.ThreadMembershipOpts) (*model.ThreadMembership, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.MaintainMembership")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.MaintainMembership(userID, postID, opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) MarkAllAsRead(userID string, threadIds []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.MarkAllAsRead")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ThreadStore.MarkAllAsRead(userID, threadIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerThreadStore) MarkAllAsReadByChannels(userID string, channelIDs []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.MarkAllAsReadByChannels")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ThreadStore.MarkAllAsReadByChannels(userID, channelIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerThreadStore) MarkAllAsReadByTeam(userID string, teamID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.MarkAllAsReadByTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ThreadStore.MarkAllAsReadByTeam(userID, teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerThreadStore) MarkAsRead(userID string, threadID string, timestamp int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.MarkAsRead")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ThreadStore.MarkAsRead(userID, threadID, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerThreadStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.PermanentDeleteBatchForRetentionPolicies")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.ThreadStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.PermanentDeleteBatchThreadMembershipsForRetentionPolicies")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.ThreadStore.PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerThreadStore) UpdateMembership(membership *model.ThreadMembership) (*model.ThreadMembership, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.UpdateMembership")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.UpdateMembership(membership)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTokenStore) Cleanup(expiryTime int64) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TokenStore.Cleanup")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.TokenStore.Cleanup(expiryTime)
}
func (s *OpenTracingLayerTokenStore) Delete(token string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TokenStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TokenStore.Delete(token)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTokenStore) GetAllTokensByType(tokenType string) ([]*model.Token, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TokenStore.GetAllTokensByType")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TokenStore.GetAllTokensByType(tokenType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTokenStore) GetByToken(token string) (*model.Token, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TokenStore.GetByToken")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TokenStore.GetByToken(token)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTokenStore) RemoveAllTokensByType(tokenType string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TokenStore.RemoveAllTokensByType")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TokenStore.RemoveAllTokensByType(tokenType)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTokenStore) Save(recovery *model.Token) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TokenStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.TokenStore.Save(recovery)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerTrueUpReviewStore) CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TrueUpReviewStore.CreateTrueUpReviewStatusRecord")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TrueUpReviewStore.CreateTrueUpReviewStatusRecord(reviewStatus)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTrueUpReviewStore) GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TrueUpReviewStore.GetTrueUpReviewStatus")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TrueUpReviewStore.GetTrueUpReviewStatus(dueDate)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTrueUpReviewStore) Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TrueUpReviewStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TrueUpReviewStore.Update(reviewStatus)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUploadSessionStore) Delete(id string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UploadSessionStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UploadSessionStore.Delete(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUploadSessionStore) Get(ctx context.Context, id string) (*model.UploadSession, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UploadSessionStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UploadSessionStore.Get(ctx, id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUploadSessionStore) GetForUser(userID string) ([]*model.UploadSession, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UploadSessionStore.GetForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UploadSessionStore.GetForUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUploadSessionStore) Save(session *model.UploadSession) (*model.UploadSession, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UploadSessionStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UploadSessionStore.Save(session)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUploadSessionStore) Update(session *model.UploadSession) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UploadSessionStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UploadSessionStore.Update(session)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) AnalyticsActiveCount(timestamp int64, options model.UserCountOptions) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.AnalyticsActiveCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.AnalyticsActiveCount(timestamp, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) AnalyticsActiveCountForPeriod(startTime int64, endTime int64, options model.UserCountOptions) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.AnalyticsActiveCountForPeriod")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.AnalyticsActiveCountForPeriod(startTime, endTime, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) AnalyticsGetExternalUsers(hostDomain string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.AnalyticsGetExternalUsers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.AnalyticsGetExternalUsers(hostDomain)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) AnalyticsGetGuestCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.AnalyticsGetGuestCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.AnalyticsGetGuestCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) AnalyticsGetInactiveUsersCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.AnalyticsGetInactiveUsersCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.AnalyticsGetInactiveUsersCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) AnalyticsGetSystemAdminCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.AnalyticsGetSystemAdminCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.AnalyticsGetSystemAdminCount()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) AutocompleteUsersInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.AutocompleteUsersInChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.AutocompleteUsersInChannel(teamID, channelID, term, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) ClearAllCustomRoleAssignments() error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.ClearAllCustomRoleAssignments")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.ClearAllCustomRoleAssignments()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) ClearCaches() {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.ClearCaches")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.UserStore.ClearCaches()
}
func (s *OpenTracingLayerUserStore) Count(options model.UserCountOptions) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.Count")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.Count(options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) DeactivateGuests() ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.DeactivateGuests")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.DeactivateGuests()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) DemoteUserToGuest(userID string) (*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.DemoteUserToGuest")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.DemoteUserToGuest(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) Get(ctx context.Context, id string) (*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.Get(ctx, id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetAll() ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetAll()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetAllAfter(limit int, afterID string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetAllAfter")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetAllAfter(limit, afterID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetAllNotInAuthService(authServices []string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetAllNotInAuthService")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetAllNotInAuthService(authServices)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetAllProfiles(options *model.UserGetOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetAllProfiles")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetAllProfiles(options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetAllProfilesInChannel(ctx context.Context, channelID string, allowFromCache bool) (map[string]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetAllProfilesInChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetAllProfilesInChannel(ctx, channelID, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetAllUsingAuthService(authService string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetAllUsingAuthService")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetAllUsingAuthService(authService)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetAnyUnreadPostCountForChannel(userID string, channelID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetAnyUnreadPostCountForChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetAnyUnreadPostCountForChannel(userID, channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetByAuth(authData *string, authService string) (*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetByAuth")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetByAuth(authData, authService)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetByEmail(email string) (*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetByEmail")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetByEmail(email)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetByUsername(username string) (*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetByUsername")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetByUsername(username)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetChannelGroupUsers(channelID string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetChannelGroupUsers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetChannelGroupUsers(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetEtagForAllProfiles() string {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetEtagForAllProfiles")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result := s.UserStore.GetEtagForAllProfiles()
return result
}
func (s *OpenTracingLayerUserStore) GetEtagForProfiles(teamID string) string {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetEtagForProfiles")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result := s.UserStore.GetEtagForProfiles(teamID)
return result
}
func (s *OpenTracingLayerUserStore) GetEtagForProfilesNotInTeam(teamID string) string {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetEtagForProfilesNotInTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result := s.UserStore.GetEtagForProfilesNotInTeam(teamID)
return result
}
func (s *OpenTracingLayerUserStore) GetFirstSystemAdminID() (string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetFirstSystemAdminID")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetFirstSystemAdminID()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetForLogin(loginID string, allowSignInWithUsername bool, allowSignInWithEmail bool) (*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetForLogin")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetForLogin(loginID, allowSignInWithUsername, allowSignInWithEmail)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetKnownUsers(userID string) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetKnownUsers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetKnownUsers(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetMany(ctx context.Context, ids []string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetMany")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetMany(ctx, ids)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetNewUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetNewUsersForTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetNewUsersForTeam(teamID, offset, limit, viewRestrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetProfileByGroupChannelIdsForUser(userID string, channelIds []string) (map[string][]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetProfileByGroupChannelIdsForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetProfileByGroupChannelIdsForUser(userID, channelIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetProfileByIds(ctx context.Context, userIds []string, options *store.UserGetByIdsOpts, allowFromCache bool) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetProfileByIds")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetProfileByIds(ctx, userIds, options, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetProfiles(options *model.UserGetOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetProfiles")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetProfiles(options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetProfilesByUsernames(usernames []string, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetProfilesByUsernames")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetProfilesByUsernames(usernames, viewRestrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetProfilesInChannel(options *model.UserGetOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetProfilesInChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetProfilesInChannel(options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetProfilesInChannelByAdmin(options *model.UserGetOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetProfilesInChannelByAdmin")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetProfilesInChannelByAdmin(options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetProfilesInChannelByStatus(options *model.UserGetOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetProfilesInChannelByStatus")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetProfilesInChannelByStatus(options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetProfilesNotInChannel(teamID string, channelId string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetProfilesNotInChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetProfilesNotInChannel(teamID, channelId, groupConstrained, offset, limit, viewRestrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetProfilesNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetProfilesNotInTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetProfilesNotInTeam(teamID, groupConstrained, offset, limit, viewRestrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetProfilesWithoutTeam(options *model.UserGetOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetProfilesWithoutTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetProfilesWithoutTeam(options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetRecentlyActiveUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetRecentlyActiveUsersForTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetRecentlyActiveUsersForTeam(teamID, offset, limit, viewRestrictions)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetSystemAdminProfiles() (map[string]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetSystemAdminProfiles")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetSystemAdminProfiles()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetTeamGroupUsers(teamID string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetTeamGroupUsers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetTeamGroupUsers(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetUnreadCount(userID string, isCRTEnabled bool) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetUnreadCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetUnreadCount(userID, isCRTEnabled)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetUnreadCountForChannel(userID string, channelID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetUnreadCountForChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetUnreadCountForChannel(userID, channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetUsersBatchForIndexing")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetUsersBatchForIndexing(startTime, startFileID, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetUsersWithInvalidEmails(page int, perPage int, restrictedDomains string) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetUsersWithInvalidEmails")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetUsersWithInvalidEmails(page, perPage, restrictedDomains)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) InferSystemInstallDate() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.InferSystemInstallDate")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.InferSystemInstallDate()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) InsertUsers(users []*model.User) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.InsertUsers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.InsertUsers(users)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) InvalidateProfileCacheForUser(userID string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.InvalidateProfileCacheForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.UserStore.InvalidateProfileCacheForUser(userID)
}
func (s *OpenTracingLayerUserStore) InvalidateProfilesInChannelCache(channelID string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.InvalidateProfilesInChannelCache")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.UserStore.InvalidateProfilesInChannelCache(channelID)
}
func (s *OpenTracingLayerUserStore) InvalidateProfilesInChannelCacheByUser(userID string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.InvalidateProfilesInChannelCacheByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.UserStore.InvalidateProfilesInChannelCacheByUser(userID)
}
func (s *OpenTracingLayerUserStore) IsEmpty(excludeBots bool) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.IsEmpty")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.IsEmpty(excludeBots)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) PermanentDelete(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.PermanentDelete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.PermanentDelete(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) PromoteGuestToUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.PromoteGuestToUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.PromoteGuestToUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) ResetAuthDataToEmailForUsers(service string, userIDs []string, includeDeleted bool, dryRun bool) (int, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.ResetAuthDataToEmailForUsers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.ResetAuthDataToEmailForUsers(service, userIDs, includeDeleted, dryRun)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) ResetLastPictureUpdate(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.ResetLastPictureUpdate")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.ResetLastPictureUpdate(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) Save(user *model.User) (*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.Save(user)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) Search(teamID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.Search")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.Search(teamID, term, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) SearchInChannel(channelID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.SearchInChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.SearchInChannel(channelID, term, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) SearchInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.SearchInGroup")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.SearchInGroup(groupID, term, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) SearchNotInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.SearchNotInChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.SearchNotInChannel(teamID, channelID, term, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) SearchNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.SearchNotInGroup")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.SearchNotInGroup(groupID, term, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) SearchNotInTeam(notInTeamID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.SearchNotInTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.SearchNotInTeam(notInTeamID, term, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) SearchWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.SearchWithoutTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.SearchWithoutTeam(term, options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) Update(user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.Update(user, allowRoleUpdate)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) UpdateAuthData(userID string, service string, authData *string, email string, resetMfa bool) (string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.UpdateAuthData")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.UpdateAuthData(userID, service, authData, email, resetMfa)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) UpdateFailedPasswordAttempts(userID string, attempts int) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.UpdateFailedPasswordAttempts")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.UpdateFailedPasswordAttempts(userID, attempts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) UpdateLastPictureUpdate(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.UpdateLastPictureUpdate")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.UpdateLastPictureUpdate(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) UpdateMfaActive(userID string, active bool) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.UpdateMfaActive")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.UpdateMfaActive(userID, active)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) UpdateMfaSecret(userID string, secret string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.UpdateMfaSecret")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.UpdateMfaSecret(userID, secret)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) UpdateNotifyProps(userID string, props map[string]string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.UpdateNotifyProps")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.UpdateNotifyProps(userID, props)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) UpdatePassword(userID string, newPassword string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.UpdatePassword")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.UpdatePassword(userID, newPassword)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) UpdateUpdateAt(userID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.UpdateUpdateAt")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.UpdateUpdateAt(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) VerifyEmail(userID string, email string) (string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.VerifyEmail")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.VerifyEmail(userID, email)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserAccessTokenStore) Delete(tokenID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserAccessTokenStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserAccessTokenStore.Delete(tokenID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserAccessTokenStore) DeleteAllForUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserAccessTokenStore.DeleteAllForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserAccessTokenStore.DeleteAllForUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserAccessTokenStore) Get(tokenID string) (*model.UserAccessToken, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserAccessTokenStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserAccessTokenStore.Get(tokenID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserAccessTokenStore) GetAll(offset int, limit int) ([]*model.UserAccessToken, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserAccessTokenStore.GetAll")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserAccessTokenStore.GetAll(offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserAccessTokenStore) GetByToken(tokenString string) (*model.UserAccessToken, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserAccessTokenStore.GetByToken")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserAccessTokenStore.GetByToken(tokenString)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserAccessTokenStore) GetByUser(userID string, page int, perPage int) ([]*model.UserAccessToken, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserAccessTokenStore.GetByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserAccessTokenStore.GetByUser(userID, page, perPage)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserAccessTokenStore) Save(token *model.UserAccessToken) (*model.UserAccessToken, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserAccessTokenStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserAccessTokenStore.Save(token)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserAccessTokenStore) Search(term string) ([]*model.UserAccessToken, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserAccessTokenStore.Search")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserAccessTokenStore.Search(term)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserAccessTokenStore) UpdateTokenDisable(tokenID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserAccessTokenStore.UpdateTokenDisable")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserAccessTokenStore.UpdateTokenDisable(tokenID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserAccessTokenStore) UpdateTokenEnable(tokenID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserAccessTokenStore.UpdateTokenEnable")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserAccessTokenStore.UpdateTokenEnable(tokenID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserTermsOfServiceStore) Delete(userID string, termsOfServiceId string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserTermsOfServiceStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserTermsOfServiceStore.Delete(userID, termsOfServiceId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserTermsOfServiceStore) GetByUser(userID string) (*model.UserTermsOfService, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserTermsOfServiceStore.GetByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserTermsOfServiceStore.GetByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserTermsOfServiceStore) Save(userTermsOfService *model.UserTermsOfService) (*model.UserTermsOfService, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserTermsOfServiceStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserTermsOfServiceStore.Save(userTermsOfService)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) AnalyticsIncomingCount(teamID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.AnalyticsIncomingCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.AnalyticsIncomingCount(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) AnalyticsOutgoingCount(teamID string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.AnalyticsOutgoingCount")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.AnalyticsOutgoingCount(teamID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) ClearCaches() {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.ClearCaches")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.WebhookStore.ClearCaches()
}
func (s *OpenTracingLayerWebhookStore) DeleteIncoming(webhookID string, timestamp int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.DeleteIncoming")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.WebhookStore.DeleteIncoming(webhookID, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerWebhookStore) DeleteOutgoing(webhookID string, timestamp int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.DeleteOutgoing")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.WebhookStore.DeleteOutgoing(webhookID, timestamp)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerWebhookStore) GetIncoming(id string, allowFromCache bool) (*model.IncomingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetIncoming")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetIncoming(id, allowFromCache)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetIncomingByChannel(channelID string) ([]*model.IncomingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetIncomingByChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetIncomingByChannel(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetIncomingByTeam(teamID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetIncomingByTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetIncomingByTeam(teamID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetIncomingByTeamByUser(teamID string, userID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetIncomingByTeamByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetIncomingByTeamByUser(teamID, userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetIncomingList(offset int, limit int) ([]*model.IncomingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetIncomingList")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetIncomingList(offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetIncomingListByUser(userID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetIncomingListByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetIncomingListByUser(userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetOutgoing(id string) (*model.OutgoingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetOutgoing")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetOutgoing(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetOutgoingByChannel(channelID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetOutgoingByChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetOutgoingByChannel(channelID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetOutgoingByChannelByUser(channelID string, userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetOutgoingByChannelByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetOutgoingByChannelByUser(channelID, userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetOutgoingByTeam(teamID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetOutgoingByTeam")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetOutgoingByTeam(teamID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetOutgoingByTeamByUser(teamID string, userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetOutgoingByTeamByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetOutgoingByTeamByUser(teamID, userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetOutgoingList(offset int, limit int) ([]*model.OutgoingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetOutgoingList")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetOutgoingList(offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) GetOutgoingListByUser(userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.GetOutgoingListByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.GetOutgoingListByUser(userID, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) InvalidateWebhookCache(webhook string) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.InvalidateWebhookCache")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
s.WebhookStore.InvalidateWebhookCache(webhook)
}
func (s *OpenTracingLayerWebhookStore) PermanentDeleteIncomingByChannel(channelID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.PermanentDeleteIncomingByChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.WebhookStore.PermanentDeleteIncomingByChannel(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerWebhookStore) PermanentDeleteIncomingByUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.PermanentDeleteIncomingByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.WebhookStore.PermanentDeleteIncomingByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerWebhookStore) PermanentDeleteOutgoingByChannel(channelID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.PermanentDeleteOutgoingByChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.WebhookStore.PermanentDeleteOutgoingByChannel(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerWebhookStore) PermanentDeleteOutgoingByUser(userID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.PermanentDeleteOutgoingByUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.WebhookStore.PermanentDeleteOutgoingByUser(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerWebhookStore) SaveIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.SaveIncoming")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.SaveIncoming(webhook)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) SaveOutgoing(webhook *model.OutgoingWebhook) (*model.OutgoingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.SaveOutgoing")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.SaveOutgoing(webhook)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) UpdateIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.UpdateIncoming")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.UpdateIncoming(webhook)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerWebhookStore) UpdateOutgoing(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.UpdateOutgoing")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.WebhookStore.UpdateOutgoing(hook)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayer) Close() {
s.Store.Close()
}
func (s *OpenTracingLayer) DropAllTables() {
s.Store.DropAllTables()
}
func (s *OpenTracingLayer) LockToMaster() {
s.Store.LockToMaster()
}
func (s *OpenTracingLayer) MarkSystemRanUnitTests() {
s.Store.MarkSystemRanUnitTests()
}
func (s *OpenTracingLayer) SetContext(context context.Context) {
s.Store.SetContext(context)
}
func (s *OpenTracingLayer) TotalMasterDbConnections() int {
return s.Store.TotalMasterDbConnections()
}
func (s *OpenTracingLayer) TotalReadDbConnections() int {
return s.Store.TotalReadDbConnections()
}
func (s *OpenTracingLayer) TotalSearchDbConnections() int {
return s.Store.TotalSearchDbConnections()
}
func (s *OpenTracingLayer) UnlockFromMaster() {
s.Store.UnlockFromMaster()
}
func New(childStore store.Store, ctx context.Context) *OpenTracingLayer {
newStore := OpenTracingLayer{
Store: childStore,
}
newStore.AuditStore = &OpenTracingLayerAuditStore{AuditStore: childStore.Audit(), Root: &newStore}
newStore.BotStore = &OpenTracingLayerBotStore{BotStore: childStore.Bot(), Root: &newStore}
newStore.ChannelStore = &OpenTracingLayerChannelStore{ChannelStore: childStore.Channel(), Root: &newStore}
newStore.ChannelMemberHistoryStore = &OpenTracingLayerChannelMemberHistoryStore{ChannelMemberHistoryStore: childStore.ChannelMemberHistory(), Root: &newStore}
newStore.ClusterDiscoveryStore = &OpenTracingLayerClusterDiscoveryStore{ClusterDiscoveryStore: childStore.ClusterDiscovery(), Root: &newStore}
newStore.CommandStore = &OpenTracingLayerCommandStore{CommandStore: childStore.Command(), Root: &newStore}
newStore.CommandWebhookStore = &OpenTracingLayerCommandWebhookStore{CommandWebhookStore: childStore.CommandWebhook(), Root: &newStore}
newStore.ComplianceStore = &OpenTracingLayerComplianceStore{ComplianceStore: childStore.Compliance(), Root: &newStore}
newStore.DraftStore = &OpenTracingLayerDraftStore{DraftStore: childStore.Draft(), Root: &newStore}
newStore.EmojiStore = &OpenTracingLayerEmojiStore{EmojiStore: childStore.Emoji(), Root: &newStore}
newStore.FileInfoStore = &OpenTracingLayerFileInfoStore{FileInfoStore: childStore.FileInfo(), Root: &newStore}
newStore.GroupStore = &OpenTracingLayerGroupStore{GroupStore: childStore.Group(), Root: &newStore}
newStore.JobStore = &OpenTracingLayerJobStore{JobStore: childStore.Job(), Root: &newStore}
newStore.LicenseStore = &OpenTracingLayerLicenseStore{LicenseStore: childStore.License(), Root: &newStore}
newStore.LinkMetadataStore = &OpenTracingLayerLinkMetadataStore{LinkMetadataStore: childStore.LinkMetadata(), Root: &newStore}
newStore.NotifyAdminStore = &OpenTracingLayerNotifyAdminStore{NotifyAdminStore: childStore.NotifyAdmin(), Root: &newStore}
newStore.OAuthStore = &OpenTracingLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore}
newStore.PluginStore = &OpenTracingLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
newStore.PostStore = &OpenTracingLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
newStore.PostAcknowledgementStore = &OpenTracingLayerPostAcknowledgementStore{PostAcknowledgementStore: childStore.PostAcknowledgement(), Root: &newStore}
newStore.PostPriorityStore = &OpenTracingLayerPostPriorityStore{PostPriorityStore: childStore.PostPriority(), Root: &newStore}
newStore.PreferenceStore = &OpenTracingLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore}
newStore.ProductNoticesStore = &OpenTracingLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore}
newStore.ReactionStore = &OpenTracingLayerReactionStore{ReactionStore: childStore.Reaction(), Root: &newStore}
newStore.RemoteClusterStore = &OpenTracingLayerRemoteClusterStore{RemoteClusterStore: childStore.RemoteCluster(), Root: &newStore}
newStore.RetentionPolicyStore = &OpenTracingLayerRetentionPolicyStore{RetentionPolicyStore: childStore.RetentionPolicy(), Root: &newStore}
newStore.RoleStore = &OpenTracingLayerRoleStore{RoleStore: childStore.Role(), Root: &newStore}
newStore.SchemeStore = &OpenTracingLayerSchemeStore{SchemeStore: childStore.Scheme(), Root: &newStore}
newStore.SessionStore = &OpenTracingLayerSessionStore{SessionStore: childStore.Session(), Root: &newStore}
newStore.SharedChannelStore = &OpenTracingLayerSharedChannelStore{SharedChannelStore: childStore.SharedChannel(), Root: &newStore}
newStore.StatusStore = &OpenTracingLayerStatusStore{StatusStore: childStore.Status(), Root: &newStore}
newStore.SystemStore = &OpenTracingLayerSystemStore{SystemStore: childStore.System(), Root: &newStore}
newStore.TeamStore = &OpenTracingLayerTeamStore{TeamStore: childStore.Team(), Root: &newStore}
newStore.TermsOfServiceStore = &OpenTracingLayerTermsOfServiceStore{TermsOfServiceStore: childStore.TermsOfService(), Root: &newStore}
newStore.ThreadStore = &OpenTracingLayerThreadStore{ThreadStore: childStore.Thread(), Root: &newStore}
newStore.TokenStore = &OpenTracingLayerTokenStore{TokenStore: childStore.Token(), Root: &newStore}
newStore.TrueUpReviewStore = &OpenTracingLayerTrueUpReviewStore{TrueUpReviewStore: childStore.TrueUpReview(), Root: &newStore}
newStore.UploadSessionStore = &OpenTracingLayerUploadSessionStore{UploadSessionStore: childStore.UploadSession(), Root: &newStore}
newStore.UserStore = &OpenTracingLayerUserStore{UserStore: childStore.User(), Root: &newStore}
newStore.UserAccessTokenStore = &OpenTracingLayerUserAccessTokenStore{UserAccessTokenStore: childStore.UserAccessToken(), Root: &newStore}
newStore.UserTermsOfServiceStore = &OpenTracingLayerUserTermsOfServiceStore{UserTermsOfServiceStore: childStore.UserTermsOfService(), Root: &newStore}
newStore.WebhookStore = &OpenTracingLayerWebhookStore{WebhookStore: childStore.Webhook(), Root: &newStore}
return &newStore
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Code generated by "make store-layers"
// DO NOT EDIT
package retrylayer
import (
"context"
"time"
timepkg "time"
"github.com/go-sql-driver/mysql"
"github.com/lib/pq"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/pkg/errors"
)
const mySQLDeadlockCode = uint16(1213)
type RetryLayer struct {
store.Store
AuditStore store.AuditStore
BotStore store.BotStore
ChannelStore store.ChannelStore
ChannelMemberHistoryStore store.ChannelMemberHistoryStore
ClusterDiscoveryStore store.ClusterDiscoveryStore
CommandStore store.CommandStore
CommandWebhookStore store.CommandWebhookStore
ComplianceStore store.ComplianceStore
DraftStore store.DraftStore
EmojiStore store.EmojiStore
FileInfoStore store.FileInfoStore
GroupStore store.GroupStore
JobStore store.JobStore
LicenseStore store.LicenseStore
LinkMetadataStore store.LinkMetadataStore
NotifyAdminStore store.NotifyAdminStore
OAuthStore store.OAuthStore
PluginStore store.PluginStore
PostStore store.PostStore
PostAcknowledgementStore store.PostAcknowledgementStore
PostPriorityStore store.PostPriorityStore
PreferenceStore store.PreferenceStore
ProductNoticesStore store.ProductNoticesStore
ReactionStore store.ReactionStore
RemoteClusterStore store.RemoteClusterStore
RetentionPolicyStore store.RetentionPolicyStore
RoleStore store.RoleStore
SchemeStore store.SchemeStore
SessionStore store.SessionStore
SharedChannelStore store.SharedChannelStore
StatusStore store.StatusStore
SystemStore store.SystemStore
TeamStore store.TeamStore
TermsOfServiceStore store.TermsOfServiceStore
ThreadStore store.ThreadStore
TokenStore store.TokenStore
TrueUpReviewStore store.TrueUpReviewStore
UploadSessionStore store.UploadSessionStore
UserStore store.UserStore
UserAccessTokenStore store.UserAccessTokenStore
UserTermsOfServiceStore store.UserTermsOfServiceStore
WebhookStore store.WebhookStore
}
func (s *RetryLayer) Audit() store.AuditStore {
return s.AuditStore
}
func (s *RetryLayer) Bot() store.BotStore {
return s.BotStore
}
func (s *RetryLayer) Channel() store.ChannelStore {
return s.ChannelStore
}
func (s *RetryLayer) ChannelMemberHistory() store.ChannelMemberHistoryStore {
return s.ChannelMemberHistoryStore
}
func (s *RetryLayer) ClusterDiscovery() store.ClusterDiscoveryStore {
return s.ClusterDiscoveryStore
}
func (s *RetryLayer) Command() store.CommandStore {
return s.CommandStore
}
func (s *RetryLayer) CommandWebhook() store.CommandWebhookStore {
return s.CommandWebhookStore
}
func (s *RetryLayer) Compliance() store.ComplianceStore {
return s.ComplianceStore
}
func (s *RetryLayer) Draft() store.DraftStore {
return s.DraftStore
}
func (s *RetryLayer) Emoji() store.EmojiStore {
return s.EmojiStore
}
func (s *RetryLayer) FileInfo() store.FileInfoStore {
return s.FileInfoStore
}
func (s *RetryLayer) Group() store.GroupStore {
return s.GroupStore
}
func (s *RetryLayer) Job() store.JobStore {
return s.JobStore
}
func (s *RetryLayer) License() store.LicenseStore {
return s.LicenseStore
}
func (s *RetryLayer) LinkMetadata() store.LinkMetadataStore {
return s.LinkMetadataStore
}
func (s *RetryLayer) NotifyAdmin() store.NotifyAdminStore {
return s.NotifyAdminStore
}
func (s *RetryLayer) OAuth() store.OAuthStore {
return s.OAuthStore
}
func (s *RetryLayer) Plugin() store.PluginStore {
return s.PluginStore
}
func (s *RetryLayer) Post() store.PostStore {
return s.PostStore
}
func (s *RetryLayer) PostAcknowledgement() store.PostAcknowledgementStore {
return s.PostAcknowledgementStore
}
func (s *RetryLayer) PostPriority() store.PostPriorityStore {
return s.PostPriorityStore
}
func (s *RetryLayer) Preference() store.PreferenceStore {
return s.PreferenceStore
}
func (s *RetryLayer) ProductNotices() store.ProductNoticesStore {
return s.ProductNoticesStore
}
func (s *RetryLayer) Reaction() store.ReactionStore {
return s.ReactionStore
}
func (s *RetryLayer) RemoteCluster() store.RemoteClusterStore {
return s.RemoteClusterStore
}
func (s *RetryLayer) RetentionPolicy() store.RetentionPolicyStore {
return s.RetentionPolicyStore
}
func (s *RetryLayer) Role() store.RoleStore {
return s.RoleStore
}
func (s *RetryLayer) Scheme() store.SchemeStore {
return s.SchemeStore
}
func (s *RetryLayer) Session() store.SessionStore {
return s.SessionStore
}
func (s *RetryLayer) SharedChannel() store.SharedChannelStore {
return s.SharedChannelStore
}
func (s *RetryLayer) Status() store.StatusStore {
return s.StatusStore
}
func (s *RetryLayer) System() store.SystemStore {
return s.SystemStore
}
func (s *RetryLayer) Team() store.TeamStore {
return s.TeamStore
}
func (s *RetryLayer) TermsOfService() store.TermsOfServiceStore {
return s.TermsOfServiceStore
}
func (s *RetryLayer) Thread() store.ThreadStore {
return s.ThreadStore
}
func (s *RetryLayer) Token() store.TokenStore {
return s.TokenStore
}
func (s *RetryLayer) TrueUpReview() store.TrueUpReviewStore {
return s.TrueUpReviewStore
}
func (s *RetryLayer) UploadSession() store.UploadSessionStore {
return s.UploadSessionStore
}
func (s *RetryLayer) User() store.UserStore {
return s.UserStore
}
func (s *RetryLayer) UserAccessToken() store.UserAccessTokenStore {
return s.UserAccessTokenStore
}
func (s *RetryLayer) UserTermsOfService() store.UserTermsOfServiceStore {
return s.UserTermsOfServiceStore
}
func (s *RetryLayer) Webhook() store.WebhookStore {
return s.WebhookStore
}
type RetryLayerAuditStore struct {
store.AuditStore
Root *RetryLayer
}
type RetryLayerBotStore struct {
store.BotStore
Root *RetryLayer
}
type RetryLayerChannelStore struct {
store.ChannelStore
Root *RetryLayer
}
type RetryLayerChannelMemberHistoryStore struct {
store.ChannelMemberHistoryStore
Root *RetryLayer
}
type RetryLayerClusterDiscoveryStore struct {
store.ClusterDiscoveryStore
Root *RetryLayer
}
type RetryLayerCommandStore struct {
store.CommandStore
Root *RetryLayer
}
type RetryLayerCommandWebhookStore struct {
store.CommandWebhookStore
Root *RetryLayer
}
type RetryLayerComplianceStore struct {
store.ComplianceStore
Root *RetryLayer
}
type RetryLayerDraftStore struct {
store.DraftStore
Root *RetryLayer
}
type RetryLayerEmojiStore struct {
store.EmojiStore
Root *RetryLayer
}
type RetryLayerFileInfoStore struct {
store.FileInfoStore
Root *RetryLayer
}
type RetryLayerGroupStore struct {
store.GroupStore
Root *RetryLayer
}
type RetryLayerJobStore struct {
store.JobStore
Root *RetryLayer
}
type RetryLayerLicenseStore struct {
store.LicenseStore
Root *RetryLayer
}
type RetryLayerLinkMetadataStore struct {
store.LinkMetadataStore
Root *RetryLayer
}
type RetryLayerNotifyAdminStore struct {
store.NotifyAdminStore
Root *RetryLayer
}
type RetryLayerOAuthStore struct {
store.OAuthStore
Root *RetryLayer
}
type RetryLayerPluginStore struct {
store.PluginStore
Root *RetryLayer
}
type RetryLayerPostStore struct {
store.PostStore
Root *RetryLayer
}
type RetryLayerPostAcknowledgementStore struct {
store.PostAcknowledgementStore
Root *RetryLayer
}
type RetryLayerPostPriorityStore struct {
store.PostPriorityStore
Root *RetryLayer
}
type RetryLayerPreferenceStore struct {
store.PreferenceStore
Root *RetryLayer
}
type RetryLayerProductNoticesStore struct {
store.ProductNoticesStore
Root *RetryLayer
}
type RetryLayerReactionStore struct {
store.ReactionStore
Root *RetryLayer
}
type RetryLayerRemoteClusterStore struct {
store.RemoteClusterStore
Root *RetryLayer
}
type RetryLayerRetentionPolicyStore struct {
store.RetentionPolicyStore
Root *RetryLayer
}
type RetryLayerRoleStore struct {
store.RoleStore
Root *RetryLayer
}
type RetryLayerSchemeStore struct {
store.SchemeStore
Root *RetryLayer
}
type RetryLayerSessionStore struct {
store.SessionStore
Root *RetryLayer
}
type RetryLayerSharedChannelStore struct {
store.SharedChannelStore
Root *RetryLayer
}
type RetryLayerStatusStore struct {
store.StatusStore
Root *RetryLayer
}
type RetryLayerSystemStore struct {
store.SystemStore
Root *RetryLayer
}
type RetryLayerTeamStore struct {
store.TeamStore
Root *RetryLayer
}
type RetryLayerTermsOfServiceStore struct {
store.TermsOfServiceStore
Root *RetryLayer
}
type RetryLayerThreadStore struct {
store.ThreadStore
Root *RetryLayer
}
type RetryLayerTokenStore struct {
store.TokenStore
Root *RetryLayer
}
type RetryLayerTrueUpReviewStore struct {
store.TrueUpReviewStore
Root *RetryLayer
}
type RetryLayerUploadSessionStore struct {
store.UploadSessionStore
Root *RetryLayer
}
type RetryLayerUserStore struct {
store.UserStore
Root *RetryLayer
}
type RetryLayerUserAccessTokenStore struct {
store.UserAccessTokenStore
Root *RetryLayer
}
type RetryLayerUserTermsOfServiceStore struct {
store.UserTermsOfServiceStore
Root *RetryLayer
}
type RetryLayerWebhookStore struct {
store.WebhookStore
Root *RetryLayer
}
func isRepeatableError(err error) bool {
var pqErr *pq.Error
var mysqlErr *mysql.MySQLError
switch {
case errors.As(errors.Cause(err), &pqErr):
if pqErr.Code == "40001" || pqErr.Code == "40P01" {
return true
}
case errors.As(errors.Cause(err), &mysqlErr):
if mysqlErr.Number == mySQLDeadlockCode {
return true
}
}
return false
}
func (s *RetryLayerAuditStore) Get(user_id string, offset int, limit int) (model.Audits, error) {
tries := 0
for {
result, err := s.AuditStore.Get(user_id, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerAuditStore) PermanentDeleteByUser(userID string) error {
tries := 0
for {
err := s.AuditStore.PermanentDeleteByUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerAuditStore) Save(audit *model.Audit) error {
tries := 0
for {
err := s.AuditStore.Save(audit)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerBotStore) Get(userID string, includeDeleted bool) (*model.Bot, error) {
tries := 0
for {
result, err := s.BotStore.Get(userID, includeDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerBotStore) GetAll(options *model.BotGetOptions) ([]*model.Bot, error) {
tries := 0
for {
result, err := s.BotStore.GetAll(options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerBotStore) PermanentDelete(userID string) error {
tries := 0
for {
err := s.BotStore.PermanentDelete(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerBotStore) Save(bot *model.Bot) (*model.Bot, error) {
tries := 0
for {
result, err := s.BotStore.Save(bot)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerBotStore) Update(bot *model.Bot) (*model.Bot, error) {
tries := 0
for {
result, err := s.BotStore.Update(bot)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) AnalyticsDeletedTypeCount(teamID string, channelType model.ChannelType) (int64, error) {
tries := 0
for {
result, err := s.ChannelStore.AnalyticsDeletedTypeCount(teamID, channelType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) AnalyticsTypeCount(teamID string, channelType model.ChannelType) (int64, error) {
tries := 0
for {
result, err := s.ChannelStore.AnalyticsTypeCount(teamID, channelType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) Autocomplete(userID string, term string, includeDeleted bool, isGuest bool) (model.ChannelListWithTeamData, error) {
tries := 0
for {
result, err := s.ChannelStore.Autocomplete(userID, term, includeDeleted, isGuest)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) AutocompleteInTeam(teamID string, userID string, term string, includeDeleted bool, isGuest bool) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.AutocompleteInTeam(teamID, userID, term, includeDeleted, isGuest)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) AutocompleteInTeamForSearch(teamID string, userID string, term string, includeDeleted bool) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.AutocompleteInTeamForSearch(teamID, userID, term, includeDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) ClearAllCustomRoleAssignments() error {
tries := 0
for {
err := s.ChannelStore.ClearAllCustomRoleAssignments()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) ClearCaches() {
s.ChannelStore.ClearCaches()
}
func (s *RetryLayerChannelStore) ClearMembersForUserCache() {
s.ChannelStore.ClearMembersForUserCache()
}
func (s *RetryLayerChannelStore) ClearSidebarOnTeamLeave(userID string, teamID string) error {
tries := 0
for {
err := s.ChannelStore.ClearSidebarOnTeamLeave(userID, teamID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) CountPostsAfter(channelID string, timestamp int64, userID string) (int, int, error) {
tries := 0
for {
result, resultVar1, err := s.ChannelStore.CountPostsAfter(channelID, timestamp, userID)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) CountUrgentPostsAfter(channelID string, timestamp int64, userID string) (int, error) {
tries := 0
for {
result, err := s.ChannelStore.CountUrgentPostsAfter(channelID, timestamp, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) CreateDirectChannel(userID *model.User, otherUserID *model.User, channelOptions ...model.ChannelOption) (*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.CreateDirectChannel(userID, otherUserID, channelOptions...)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) CreateInitialSidebarCategories(userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
tries := 0
for {
result, err := s.ChannelStore.CreateInitialSidebarCategories(userID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) CreateSidebarCategory(userID string, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) {
tries := 0
for {
result, err := s.ChannelStore.CreateSidebarCategory(userID, teamID, newCategory)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) Delete(channelID string, timestamp int64) error {
tries := 0
for {
err := s.ChannelStore.Delete(channelID, timestamp)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) DeleteSidebarCategory(categoryID string) error {
tries := 0
for {
err := s.ChannelStore.DeleteSidebarCategory(categoryID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) DeleteSidebarChannelsByPreferences(preferences model.Preferences) error {
tries := 0
for {
err := s.ChannelStore.DeleteSidebarChannelsByPreferences(preferences)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) Get(id string, allowFromCache bool) (*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.Get(id, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetAll(teamID string) ([]*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.GetAll(teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetAllChannelMembersById(id string) ([]string, error) {
tries := 0
for {
result, err := s.ChannelStore.GetAllChannelMembersById(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetAllChannelMembersForUser(userID string, allowFromCache bool, includeDeleted bool) (map[string]string, error) {
tries := 0
for {
result, err := s.ChannelStore.GetAllChannelMembersForUser(userID, allowFromCache, includeDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetAllChannelMembersNotifyPropsForChannel(channelID string, allowFromCache bool) (map[string]model.StringMap, error) {
tries := 0
for {
result, err := s.ChannelStore.GetAllChannelMembersNotifyPropsForChannel(channelID, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetAllChannels(page int, perPage int, opts store.ChannelSearchOpts) (model.ChannelListWithTeamData, error) {
tries := 0
for {
result, err := s.ChannelStore.GetAllChannels(page, perPage, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetAllChannelsCount(opts store.ChannelSearchOpts) (int64, error) {
tries := 0
for {
result, err := s.ChannelStore.GetAllChannelsCount(opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetAllChannelsForExportAfter(limit int, afterID string) ([]*model.ChannelForExport, error) {
tries := 0
for {
result, err := s.ChannelStore.GetAllChannelsForExportAfter(limit, afterID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetAllDirectChannelsForExportAfter(limit int, afterID string) ([]*model.DirectChannelForExport, error) {
tries := 0
for {
result, err := s.ChannelStore.GetAllDirectChannelsForExportAfter(limit, afterID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetByName(team_id string, name string, allowFromCache bool) (*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.GetByName(team_id, name, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetByNameIncludeDeleted(team_id string, name string, allowFromCache bool) (*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.GetByNameIncludeDeleted(team_id, name, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetByNames(team_id string, names []string, allowFromCache bool) ([]*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.GetByNames(team_id, names, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannelCounts(teamID string, userID string) (*model.ChannelCounts, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannelCounts(teamID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannelMembersForExport(userID string, teamID string) ([]*model.ChannelMemberForExport, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannelMembersForExport(userID, teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannelMembersTimezones(channelID string) ([]model.StringMap, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannelMembersTimezones(channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannelUnread(channelID string, userID string) (*model.ChannelUnread, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannelUnread(channelID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannels(teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannels(teamID, userID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannelsBatchForIndexing(startTime int64, startChannelID string, limit int) ([]*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannelsBatchForIndexing(startTime, startChannelID, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannelsByIds(channelIds []string, includeDeleted bool) ([]*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannelsByIds(channelIds, includeDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannelsByScheme(schemeID string, offset int, limit int) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannelsByScheme(schemeID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannelsByUser(userID string, includeDeleted bool, lastDeleteAt int, pageSize int, fromChannelID string) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannelsByUser(userID, includeDeleted, lastDeleteAt, pageSize, fromChannelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannelsWithCursor(teamId, userId, opts, afterChannelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetChannelsWithTeamDataByIds(channelIds []string, includeDeleted bool) ([]*model.ChannelWithTeamData, error) {
tries := 0
for {
result, err := s.ChannelStore.GetChannelsWithTeamDataByIds(channelIds, includeDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetDeleted(team_id, offset, limit, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetDeletedByName(team_id string, name string) (*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.GetDeletedByName(team_id, name)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetFileCount(channelID string) (int64, error) {
tries := 0
for {
result, err := s.ChannelStore.GetFileCount(channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetForPost(postID string) (*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.GetForPost(postID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetGuestCount(channelID string, allowFromCache bool) (int64, error) {
tries := 0
for {
result, err := s.ChannelStore.GetGuestCount(channelID, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMany(ids []string, allowFromCache bool) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMany(ids, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMember(ctx context.Context, channelID string, userID string) (*model.ChannelMember, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMember(ctx, channelID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMemberCount(channelID string, allowFromCache bool) (int64, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMemberCount(channelID, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMemberCountFromCache(channelID string) int64 {
return s.ChannelStore.GetMemberCountFromCache(channelID)
}
func (s *RetryLayerChannelStore) GetMemberCountsByGroup(ctx context.Context, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMemberCountsByGroup(ctx, channelID, includeTimezones)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMemberForPost(postID string, userID string) (*model.ChannelMember, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMemberForPost(postID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMembers(channelID string, offset int, limit int) (model.ChannelMembers, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMembers(channelID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMembersByChannelIds(channelIds []string, userID string) (model.ChannelMembers, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMembersByChannelIds(channelIds, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMembersByIds(channelID string, userIds []string) (model.ChannelMembers, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMembersByIds(channelID, userIds)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMembersForUser(teamID string, userID string) (model.ChannelMembers, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMembersForUser(teamID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMembersForUserWithCursor(userID string, teamID string, opts *store.ChannelMemberGraphQLSearchOpts) (model.ChannelMembers, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMembersForUserWithCursor(userID, teamID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMembersForUserWithPagination(userID string, page int, perPage int) (model.ChannelMembersWithTeamData, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMembersForUserWithPagination(userID, page, perPage)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMembersInfoByChannelIds(channelIDs)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetMoreChannels(teamID string, userID string, offset int, limit int) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetMoreChannels(teamID, userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetPinnedPostCount(channelID string, allowFromCache bool) (int64, error) {
tries := 0
for {
result, err := s.ChannelStore.GetPinnedPostCount(channelID, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetPinnedPosts(channelID string) (*model.PostList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetPinnedPosts(channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetPrivateChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetPrivateChannelsForTeam(teamID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetPublicChannelsByIdsForTeam(teamID string, channelIds []string) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetPublicChannelsByIdsForTeam(teamID, channelIds)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetPublicChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetPublicChannelsForTeam(teamID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetSidebarCategories(userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
tries := 0
for {
result, err := s.ChannelStore.GetSidebarCategories(userID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetSidebarCategoriesForTeamForUser(userID string, teamID string) (*model.OrderedSidebarCategories, error) {
tries := 0
for {
result, err := s.ChannelStore.GetSidebarCategoriesForTeamForUser(userID, teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetSidebarCategory(categoryID string) (*model.SidebarCategoryWithChannels, error) {
tries := 0
for {
result, err := s.ChannelStore.GetSidebarCategory(categoryID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetSidebarCategoryOrder(userID string, teamID string) ([]string, error) {
tries := 0
for {
result, err := s.ChannelStore.GetSidebarCategoryOrder(userID, teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetTeamChannels(teamID string) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetTeamChannels(teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetTeamForChannel(channelID string) (*model.Team, error) {
tries := 0
for {
result, err := s.ChannelStore.GetTeamForChannel(channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetTeamMembersForChannel(channelID string) ([]string, error) {
tries := 0
for {
result, err := s.ChannelStore.GetTeamMembersForChannel(channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetTopChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetTopChannelsForTeamSince(teamID, userID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetTopChannelsForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetTopChannelsForUserSince(userID, teamID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetTopInactiveChannelsForTeamSince(teamID, userID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetTopInactiveChannelsForUserSince(teamID, userID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GroupSyncedChannelCount() (int64, error) {
tries := 0
for {
result, err := s.ChannelStore.GroupSyncedChannelCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) IncrementMentionCount(channelID string, userIDs []string, isRoot bool, isUrgent bool) error {
tries := 0
for {
err := s.ChannelStore.IncrementMentionCount(channelID, userIDs, isRoot, isUrgent)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) InvalidateAllChannelMembersForUser(userID string) {
s.ChannelStore.InvalidateAllChannelMembersForUser(userID)
}
func (s *RetryLayerChannelStore) InvalidateCacheForChannelMembersNotifyProps(channelID string) {
s.ChannelStore.InvalidateCacheForChannelMembersNotifyProps(channelID)
}
func (s *RetryLayerChannelStore) InvalidateChannel(id string) {
s.ChannelStore.InvalidateChannel(id)
}
func (s *RetryLayerChannelStore) InvalidateChannelByName(teamID string, name string) {
s.ChannelStore.InvalidateChannelByName(teamID, name)
}
func (s *RetryLayerChannelStore) InvalidateGuestCount(channelID string) {
s.ChannelStore.InvalidateGuestCount(channelID)
}
func (s *RetryLayerChannelStore) InvalidateMemberCount(channelID string) {
s.ChannelStore.InvalidateMemberCount(channelID)
}
func (s *RetryLayerChannelStore) InvalidatePinnedPostCount(channelID string) {
s.ChannelStore.InvalidatePinnedPostCount(channelID)
}
func (s *RetryLayerChannelStore) IsUserInChannelUseCache(userID string, channelID string) bool {
return s.ChannelStore.IsUserInChannelUseCache(userID, channelID)
}
func (s *RetryLayerChannelStore) MigrateChannelMembers(fromChannelID string, fromUserID string) (map[string]string, error) {
tries := 0
for {
result, err := s.ChannelStore.MigrateChannelMembers(fromChannelID, fromUserID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) PermanentDelete(channelID string) error {
tries := 0
for {
err := s.ChannelStore.PermanentDelete(channelID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) PermanentDeleteByTeam(teamID string) error {
tries := 0
for {
err := s.ChannelStore.PermanentDeleteByTeam(teamID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) PermanentDeleteMembersByChannel(channelID string) error {
tries := 0
for {
err := s.ChannelStore.PermanentDeleteMembersByChannel(channelID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) PermanentDeleteMembersByUser(userID string) error {
tries := 0
for {
err := s.ChannelStore.PermanentDeleteMembersByUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) PostCountsByDuration(channelIDs []string, sinceUnixMillis int64, userID *string, duration model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, error) {
tries := 0
for {
result, err := s.ChannelStore.PostCountsByDuration(channelIDs, sinceUnixMillis, userID, duration, groupingLocation)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) RemoveAllDeactivatedMembers(channelID string) error {
tries := 0
for {
err := s.ChannelStore.RemoveAllDeactivatedMembers(channelID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) RemoveMember(channelID string, userID string) error {
tries := 0
for {
err := s.ChannelStore.RemoveMember(channelID, userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) RemoveMembers(channelID string, userIds []string) error {
tries := 0
for {
err := s.ChannelStore.RemoveMembers(channelID, userIds)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) ResetAllChannelSchemes() error {
tries := 0
for {
err := s.ChannelStore.ResetAllChannelSchemes()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) Restore(channelID string, timestamp int64) error {
tries := 0
for {
err := s.ChannelStore.Restore(channelID, timestamp)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) Save(channel *model.Channel, maxChannelsPerTeam int64) (*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.Save(channel, maxChannelsPerTeam)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SaveDirectChannel(channel *model.Channel, member1 *model.ChannelMember, member2 *model.ChannelMember) (*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.SaveDirectChannel(channel, member1, member2)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SaveMember(member *model.ChannelMember) (*model.ChannelMember, error) {
tries := 0
for {
result, err := s.ChannelStore.SaveMember(member)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SaveMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
tries := 0
for {
result, err := s.ChannelStore.SaveMultipleMembers(members)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SearchAllChannels(term string, opts store.ChannelSearchOpts) (model.ChannelListWithTeamData, int64, error) {
tries := 0
for {
result, resultVar1, err := s.ChannelStore.SearchAllChannels(term, opts)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SearchArchivedInTeam(teamID string, term string, userID string) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.SearchArchivedInTeam(teamID, term, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SearchForUserInTeam(userID string, teamID string, term string, includeDeleted bool) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.SearchForUserInTeam(userID, teamID, term, includeDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SearchGroupChannels(userID string, term string) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.SearchGroupChannels(userID, term)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SearchInTeam(teamID string, term string, includeDeleted bool) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.SearchInTeam(teamID, term, includeDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SearchMore(userID string, teamID string, term string) (model.ChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.SearchMore(userID, teamID, term)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SetDeleteAt(channelID string, deleteAt int64, updateAt int64) error {
tries := 0
for {
err := s.ChannelStore.SetDeleteAt(channelID, deleteAt, updateAt)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) SetShared(channelId string, shared bool) error {
tries := 0
for {
err := s.ChannelStore.SetShared(channelId, shared)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) Update(channel *model.Channel) (*model.Channel, error) {
tries := 0
for {
result, err := s.ChannelStore.Update(channel)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UpdateLastViewedAt(channelIds []string, userID string) (map[string]int64, error) {
tries := 0
for {
result, err := s.ChannelStore.UpdateLastViewedAt(channelIds, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, urgentMentionCount int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) {
tries := 0
for {
result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, mentionCountRoot, urgentMentionCount, setUnreadCountRoot)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UpdateMember(member *model.ChannelMember) (*model.ChannelMember, error) {
tries := 0
for {
result, err := s.ChannelStore.UpdateMember(member)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UpdateMemberNotifyProps(channelID string, userID string, props map[string]string) (*model.ChannelMember, error) {
tries := 0
for {
result, err := s.ChannelStore.UpdateMemberNotifyProps(channelID, userID, props)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UpdateMembersRole(channelID string, userIDs []string) error {
tries := 0
for {
err := s.ChannelStore.UpdateMembersRole(channelID, userIDs)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UpdateMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
tries := 0
for {
result, err := s.ChannelStore.UpdateMultipleMembers(members)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UpdateSidebarCategories(userID string, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) {
tries := 0
for {
result, resultVar1, err := s.ChannelStore.UpdateSidebarCategories(userID, teamID, categories)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UpdateSidebarCategoryOrder(userID string, teamID string, categoryOrder []string) error {
tries := 0
for {
err := s.ChannelStore.UpdateSidebarCategoryOrder(userID, teamID, categoryOrder)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UpdateSidebarChannelCategoryOnMove(channel *model.Channel, newTeamID string) error {
tries := 0
for {
err := s.ChannelStore.UpdateSidebarChannelCategoryOnMove(channel, newTeamID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UpdateSidebarChannelsByPreferences(preferences model.Preferences) error {
tries := 0
for {
err := s.ChannelStore.UpdateSidebarChannelsByPreferences(preferences)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) UserBelongsToChannels(userID string, channelIds []string) (bool, error) {
tries := 0
for {
result, err := s.ChannelStore.UserBelongsToChannels(userID, channelIds)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelMemberHistoryStore) DeleteOrphanedRows(limit int) (int64, error) {
tries := 0
for {
result, err := s.ChannelMemberHistoryStore.DeleteOrphanedRows(limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelMemberHistoryStore) GetChannelsLeftSince(userID string, since int64) ([]string, error) {
tries := 0
for {
result, err := s.ChannelMemberHistoryStore.GetChannelsLeftSince(userID, since)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelID string) ([]*model.ChannelMemberHistoryResult, error) {
tries := 0
for {
result, err := s.ChannelMemberHistoryStore.GetUsersInChannelDuring(startTime, endTime, channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelMemberHistoryStore) LogJoinEvent(userID string, channelID string, joinTime int64) error {
tries := 0
for {
err := s.ChannelMemberHistoryStore.LogJoinEvent(userID, channelID, joinTime)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelMemberHistoryStore) LogLeaveEvent(userID string, channelID string, leaveTime int64) error {
tries := 0
for {
err := s.ChannelMemberHistoryStore.LogLeaveEvent(userID, channelID, leaveTime)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelMemberHistoryStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
tries := 0
for {
result, err := s.ChannelMemberHistoryStore.PermanentDeleteBatch(endTime, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
tries := 0
for {
result, resultVar1, err := s.ChannelMemberHistoryStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerClusterDiscoveryStore) Cleanup() error {
tries := 0
for {
err := s.ClusterDiscoveryStore.Cleanup()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerClusterDiscoveryStore) Delete(discovery *model.ClusterDiscovery) (bool, error) {
tries := 0
for {
result, err := s.ClusterDiscoveryStore.Delete(discovery)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerClusterDiscoveryStore) Exists(discovery *model.ClusterDiscovery) (bool, error) {
tries := 0
for {
result, err := s.ClusterDiscoveryStore.Exists(discovery)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerClusterDiscoveryStore) GetAll(discoveryType string, clusterName string) ([]*model.ClusterDiscovery, error) {
tries := 0
for {
result, err := s.ClusterDiscoveryStore.GetAll(discoveryType, clusterName)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerClusterDiscoveryStore) Save(discovery *model.ClusterDiscovery) error {
tries := 0
for {
err := s.ClusterDiscoveryStore.Save(discovery)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerClusterDiscoveryStore) SetLastPingAt(discovery *model.ClusterDiscovery) error {
tries := 0
for {
err := s.ClusterDiscoveryStore.SetLastPingAt(discovery)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandStore) AnalyticsCommandCount(teamID string) (int64, error) {
tries := 0
for {
result, err := s.CommandStore.AnalyticsCommandCount(teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandStore) Delete(commandID string, timestamp int64) error {
tries := 0
for {
err := s.CommandStore.Delete(commandID, timestamp)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandStore) Get(id string) (*model.Command, error) {
tries := 0
for {
result, err := s.CommandStore.Get(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandStore) GetByTeam(teamID string) ([]*model.Command, error) {
tries := 0
for {
result, err := s.CommandStore.GetByTeam(teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandStore) GetByTrigger(teamID string, trigger string) (*model.Command, error) {
tries := 0
for {
result, err := s.CommandStore.GetByTrigger(teamID, trigger)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandStore) PermanentDeleteByTeam(teamID string) error {
tries := 0
for {
err := s.CommandStore.PermanentDeleteByTeam(teamID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandStore) PermanentDeleteByUser(userID string) error {
tries := 0
for {
err := s.CommandStore.PermanentDeleteByUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandStore) Save(webhook *model.Command) (*model.Command, error) {
tries := 0
for {
result, err := s.CommandStore.Save(webhook)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandStore) Update(hook *model.Command) (*model.Command, error) {
tries := 0
for {
result, err := s.CommandStore.Update(hook)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandWebhookStore) Cleanup() {
s.CommandWebhookStore.Cleanup()
}
func (s *RetryLayerCommandWebhookStore) Get(id string) (*model.CommandWebhook, error) {
tries := 0
for {
result, err := s.CommandWebhookStore.Get(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandWebhookStore) Save(webhook *model.CommandWebhook) (*model.CommandWebhook, error) {
tries := 0
for {
result, err := s.CommandWebhookStore.Save(webhook)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerCommandWebhookStore) TryUse(id string, limit int) error {
tries := 0
for {
err := s.CommandWebhookStore.TryUse(id, limit)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerComplianceStore) ComplianceExport(compliance *model.Compliance, cursor model.ComplianceExportCursor, limit int) ([]*model.CompliancePost, model.ComplianceExportCursor, error) {
tries := 0
for {
result, resultVar1, err := s.ComplianceStore.ComplianceExport(compliance, cursor, limit)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerComplianceStore) Get(id string) (*model.Compliance, error) {
tries := 0
for {
result, err := s.ComplianceStore.Get(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerComplianceStore) GetAll(offset int, limit int) (model.Compliances, error) {
tries := 0
for {
result, err := s.ComplianceStore.GetAll(offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerComplianceStore) MessageExport(ctx context.Context, cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) {
tries := 0
for {
result, resultVar1, err := s.ComplianceStore.MessageExport(ctx, cursor, limit)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerComplianceStore) Save(compliance *model.Compliance) (*model.Compliance, error) {
tries := 0
for {
result, err := s.ComplianceStore.Save(compliance)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerComplianceStore) Update(compliance *model.Compliance) (*model.Compliance, error) {
tries := 0
for {
result, err := s.ComplianceStore.Update(compliance)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerDraftStore) Delete(userID string, channelID string, rootID string) error {
tries := 0
for {
err := s.DraftStore.Delete(userID, channelID, rootID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerDraftStore) Get(userID string, channelID string, rootID string, includeDeleted bool) (*model.Draft, error) {
tries := 0
for {
result, err := s.DraftStore.Get(userID, channelID, rootID, includeDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerDraftStore) GetDraftsForUser(userID string, teamID string) ([]*model.Draft, error) {
tries := 0
for {
result, err := s.DraftStore.GetDraftsForUser(userID, teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerDraftStore) Save(d *model.Draft) (*model.Draft, error) {
tries := 0
for {
result, err := s.DraftStore.Save(d)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerDraftStore) Update(d *model.Draft) (*model.Draft, error) {
tries := 0
for {
result, err := s.DraftStore.Update(d)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerEmojiStore) Delete(emoji *model.Emoji, timestamp int64) error {
tries := 0
for {
err := s.EmojiStore.Delete(emoji, timestamp)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerEmojiStore) Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error) {
tries := 0
for {
result, err := s.EmojiStore.Get(ctx, id, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerEmojiStore) GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error) {
tries := 0
for {
result, err := s.EmojiStore.GetByName(ctx, name, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerEmojiStore) GetList(offset int, limit int, sort string) ([]*model.Emoji, error) {
tries := 0
for {
result, err := s.EmojiStore.GetList(offset, limit, sort)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerEmojiStore) GetMultipleByName(names []string) ([]*model.Emoji, error) {
tries := 0
for {
result, err := s.EmojiStore.GetMultipleByName(names)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerEmojiStore) Save(emoji *model.Emoji) (*model.Emoji, error) {
tries := 0
for {
result, err := s.EmojiStore.Save(emoji)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerEmojiStore) Search(name string, prefixOnly bool, limit int) ([]*model.Emoji, error) {
tries := 0
for {
result, err := s.EmojiStore.Search(name, prefixOnly, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) AttachToPost(fileID string, postID string, channelID string, creatorID string) error {
tries := 0
for {
err := s.FileInfoStore.AttachToPost(fileID, postID, channelID, creatorID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) ClearCaches() {
s.FileInfoStore.ClearCaches()
}
func (s *RetryLayerFileInfoStore) CountAll() (int64, error) {
tries := 0
for {
result, err := s.FileInfoStore.CountAll()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) DeleteForPost(postID string) (string, error) {
tries := 0
for {
result, err := s.FileInfoStore.DeleteForPost(postID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) Get(id string) (*model.FileInfo, error) {
tries := 0
for {
result, err := s.FileInfoStore.Get(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) GetByIds(ids []string) ([]*model.FileInfo, error) {
tries := 0
for {
result, err := s.FileInfoStore.GetByIds(ids)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) GetByPath(path string) (*model.FileInfo, error) {
tries := 0
for {
result, err := s.FileInfoStore.GetByPath(path)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) GetFilesBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.FileForIndexing, error) {
tries := 0
for {
result, err := s.FileInfoStore.GetFilesBatchForIndexing(startTime, startFileID, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) GetForPost(postID string, readFromMaster bool, includeDeleted bool, allowFromCache bool) ([]*model.FileInfo, error) {
tries := 0
for {
result, err := s.FileInfoStore.GetForPost(postID, readFromMaster, includeDeleted, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) GetForUser(userID string) ([]*model.FileInfo, error) {
tries := 0
for {
result, err := s.FileInfoStore.GetForUser(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) GetFromMaster(id string) (*model.FileInfo, error) {
tries := 0
for {
result, err := s.FileInfoStore.GetFromMaster(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) GetStorageUsage(allowFromCache bool, includeDeleted bool) (int64, error) {
tries := 0
for {
result, err := s.FileInfoStore.GetStorageUsage(allowFromCache, includeDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) GetUptoNSizeFileTime(n int64) (int64, error) {
tries := 0
for {
result, err := s.FileInfoStore.GetUptoNSizeFileTime(n)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) GetWithOptions(page int, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, error) {
tries := 0
for {
result, err := s.FileInfoStore.GetWithOptions(page, perPage, opt)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) InvalidateFileInfosForPostCache(postID string, deleted bool) {
s.FileInfoStore.InvalidateFileInfosForPostCache(postID, deleted)
}
func (s *RetryLayerFileInfoStore) PermanentDelete(fileID string) error {
tries := 0
for {
err := s.FileInfoStore.PermanentDelete(fileID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
tries := 0
for {
result, err := s.FileInfoStore.PermanentDeleteBatch(endTime, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) PermanentDeleteByUser(userID string) (int64, error) {
tries := 0
for {
result, err := s.FileInfoStore.PermanentDeleteByUser(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) Save(info *model.FileInfo) (*model.FileInfo, error) {
tries := 0
for {
result, err := s.FileInfoStore.Save(info)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) Search(paramsList []*model.SearchParams, userID string, teamID string, page int, perPage int) (*model.FileInfoList, error) {
tries := 0
for {
result, err := s.FileInfoStore.Search(paramsList, userID, teamID, page, perPage)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) SetContent(fileID string, content string) error {
tries := 0
for {
err := s.FileInfoStore.SetContent(fileID, content)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerFileInfoStore) Upsert(info *model.FileInfo) (*model.FileInfo, error) {
tries := 0
for {
result, err := s.FileInfoStore.Upsert(info)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) AdminRoleGroupsForSyncableMember(userID string, syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
tries := 0
for {
result, err := s.GroupStore.AdminRoleGroupsForSyncableMember(userID, syncableID, syncableType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, error) {
tries := 0
for {
result, err := s.GroupStore.ChannelMembersMinusGroupMembers(channelID, groupIDs, page, perPage)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) ChannelMembersToAdd(since int64, channelID *string, includeRemovedMembers bool) ([]*model.UserChannelIDPair, error) {
tries := 0
for {
result, err := s.GroupStore.ChannelMembersToAdd(since, channelID, includeRemovedMembers)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) ChannelMembersToRemove(channelID *string) ([]*model.ChannelMember, error) {
tries := 0
for {
result, err := s.GroupStore.ChannelMembersToRemove(channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) CountChannelMembersMinusGroupMembers(channelID string, groupIDs []string) (int64, error) {
tries := 0
for {
result, err := s.GroupStore.CountChannelMembersMinusGroupMembers(channelID, groupIDs)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) CountGroupsByChannel(channelID string, opts model.GroupSearchOpts) (int64, error) {
tries := 0
for {
result, err := s.GroupStore.CountGroupsByChannel(channelID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) CountGroupsByTeam(teamID string, opts model.GroupSearchOpts) (int64, error) {
tries := 0
for {
result, err := s.GroupStore.CountGroupsByTeam(teamID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) CountTeamMembersMinusGroupMembers(teamID string, groupIDs []string) (int64, error) {
tries := 0
for {
result, err := s.GroupStore.CountTeamMembersMinusGroupMembers(teamID, groupIDs)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) Create(group *model.Group) (*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.Create(group)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) CreateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
tries := 0
for {
result, err := s.GroupStore.CreateGroupSyncable(groupSyncable)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) CreateWithUserIds(group *model.GroupWithUserIds) (*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.CreateWithUserIds(group)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) Delete(groupID string) (*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.Delete(groupID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
tries := 0
for {
result, err := s.GroupStore.DeleteGroupSyncable(groupID, syncableID, syncableType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) DeleteMember(groupID string, userID string) (*model.GroupMember, error) {
tries := 0
for {
result, err := s.GroupStore.DeleteMember(groupID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) DeleteMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
tries := 0
for {
result, err := s.GroupStore.DeleteMembers(groupID, userIDs)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) DistinctGroupMemberCount() (int64, error) {
tries := 0
for {
result, err := s.GroupStore.DistinctGroupMemberCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) DistinctGroupMemberCountForSource(source model.GroupSource) (int64, error) {
tries := 0
for {
result, err := s.GroupStore.DistinctGroupMemberCountForSource(source)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) Get(groupID string) (*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.Get(groupID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetAllBySource(groupSource model.GroupSource) ([]*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.GetAllBySource(groupSource)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetAllGroupSyncablesByGroupId(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, error) {
tries := 0
for {
result, err := s.GroupStore.GetAllGroupSyncablesByGroupId(groupID, syncableType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetByIDs(groupIDs []string) ([]*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.GetByIDs(groupIDs)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetByName(name string, opts model.GroupSearchOpts) (*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.GetByName(name, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.GetByRemoteID(remoteID, groupSource)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetByUser(userID string) ([]*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.GetByUser(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
tries := 0
for {
result, err := s.GroupStore.GetGroupSyncable(groupID, syncableID, syncableType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetGroups(page int, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.GetGroups(page, perPage, opts, viewRestrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetGroupsAssociatedToChannelsByTeam(teamID string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, error) {
tries := 0
for {
result, err := s.GroupStore.GetGroupsAssociatedToChannelsByTeam(teamID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetGroupsByChannel(channelID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
tries := 0
for {
result, err := s.GroupStore.GetGroupsByChannel(channelID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetGroupsByTeam(teamID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
tries := 0
for {
result, err := s.GroupStore.GetGroupsByTeam(teamID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetMember(groupID string, userID string) (*model.GroupMember, error) {
tries := 0
for {
result, err := s.GroupStore.GetMember(groupID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetMemberCount(groupID string) (int64, error) {
tries := 0
for {
result, err := s.GroupStore.GetMemberCount(groupID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetMemberCountWithRestrictions(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, error) {
tries := 0
for {
result, err := s.GroupStore.GetMemberCountWithRestrictions(groupID, viewRestrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetMemberUsers(groupID string) ([]*model.User, error) {
tries := 0
for {
result, err := s.GroupStore.GetMemberUsers(groupID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetMemberUsersInTeam(groupID string, teamID string) ([]*model.User, error) {
tries := 0
for {
result, err := s.GroupStore.GetMemberUsersInTeam(groupID, teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetMemberUsersNotInChannel(groupID string, channelID string) ([]*model.User, error) {
tries := 0
for {
result, err := s.GroupStore.GetMemberUsersNotInChannel(groupID, channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
tries := 0
for {
result, err := s.GroupStore.GetMemberUsersPage(groupID, page, perPage, viewRestrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, error) {
tries := 0
for {
result, err := s.GroupStore.GetMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, teammateNameDisplay)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
tries := 0
for {
result, err := s.GroupStore.GetNonMemberUsersPage(groupID, page, perPage, viewRestrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GroupChannelCount() (int64, error) {
tries := 0
for {
result, err := s.GroupStore.GroupChannelCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GroupCount() (int64, error) {
tries := 0
for {
result, err := s.GroupStore.GroupCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GroupCountBySource(source model.GroupSource) (int64, error) {
tries := 0
for {
result, err := s.GroupStore.GroupCountBySource(source)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GroupCountWithAllowReference() (int64, error) {
tries := 0
for {
result, err := s.GroupStore.GroupCountWithAllowReference()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GroupMemberCount() (int64, error) {
tries := 0
for {
result, err := s.GroupStore.GroupMemberCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) GroupTeamCount() (int64, error) {
tries := 0
for {
result, err := s.GroupStore.GroupTeamCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) PermanentDeleteMembersByUser(userID string) error {
tries := 0
for {
err := s.GroupStore.PermanentDeleteMembersByUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) PermittedSyncableAdmins(syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
tries := 0
for {
result, err := s.GroupStore.PermittedSyncableAdmins(syncableID, syncableType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) Restore(groupID string) (*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.Restore(groupID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, error) {
tries := 0
for {
result, err := s.GroupStore.TeamMembersMinusGroupMembers(teamID, groupIDs, page, perPage)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) TeamMembersToAdd(since int64, teamID *string, includeRemovedMembers bool) ([]*model.UserTeamIDPair, error) {
tries := 0
for {
result, err := s.GroupStore.TeamMembersToAdd(since, teamID, includeRemovedMembers)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) TeamMembersToRemove(teamID *string) ([]*model.TeamMember, error) {
tries := 0
for {
result, err := s.GroupStore.TeamMembersToRemove(teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) Update(group *model.Group) (*model.Group, error) {
tries := 0
for {
result, err := s.GroupStore.Update(group)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
tries := 0
for {
result, err := s.GroupStore.UpdateGroupSyncable(groupSyncable)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) UpsertMember(groupID string, userID string) (*model.GroupMember, error) {
tries := 0
for {
result, err := s.GroupStore.UpsertMember(groupID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerGroupStore) UpsertMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
tries := 0
for {
result, err := s.GroupStore.UpsertMembers(groupID, userIDs)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) Cleanup(expiryTime int64, batchSize int) error {
tries := 0
for {
err := s.JobStore.Cleanup(expiryTime, batchSize)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) Delete(id string) (string, error) {
tries := 0
for {
result, err := s.JobStore.Delete(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) Get(id string) (*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.Get(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) GetAllByStatus(status string) ([]*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.GetAllByStatus(status)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) GetAllByType(jobType string) ([]*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.GetAllByType(jobType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) GetAllByTypeAndStatus(jobType string, status string) ([]*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.GetAllByTypeAndStatus(jobType, status)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) GetAllByTypePage(jobType string, offset int, limit int) ([]*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.GetAllByTypePage(jobType, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) GetAllByTypesPage(jobTypes []string, offset int, limit int) ([]*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.GetAllByTypesPage(jobTypes, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) GetAllPage(offset int, limit int) ([]*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.GetAllPage(offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) GetCountByStatusAndType(status string, jobType string) (int64, error) {
tries := 0
for {
result, err := s.JobStore.GetCountByStatusAndType(status, jobType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) GetNewestJobByStatusAndType(status string, jobType string) (*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.GetNewestJobByStatusAndType(status, jobType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) GetNewestJobByStatusesAndType(statuses []string, jobType string) (*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.GetNewestJobByStatusesAndType(statuses, jobType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) Save(job *model.Job) (*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.Save(job)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) UpdateOptimistically(job *model.Job, currentStatus string) (bool, error) {
tries := 0
for {
result, err := s.JobStore.UpdateOptimistically(job, currentStatus)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) UpdateStatus(id string, status string) (*model.Job, error) {
tries := 0
for {
result, err := s.JobStore.UpdateStatus(id, status)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerJobStore) UpdateStatusOptimistically(id string, currentStatus string, newStatus string) (bool, error) {
tries := 0
for {
result, err := s.JobStore.UpdateStatusOptimistically(id, currentStatus, newStatus)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerLicenseStore) Get(id string) (*model.LicenseRecord, error) {
tries := 0
for {
result, err := s.LicenseStore.Get(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerLicenseStore) GetAll() ([]*model.LicenseRecord, error) {
tries := 0
for {
result, err := s.LicenseStore.GetAll()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerLicenseStore) Save(license *model.LicenseRecord) (*model.LicenseRecord, error) {
tries := 0
for {
result, err := s.LicenseStore.Save(license)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerLinkMetadataStore) Get(url string, timestamp int64) (*model.LinkMetadata, error) {
tries := 0
for {
result, err := s.LinkMetadataStore.Get(url, timestamp)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerLinkMetadataStore) Save(linkMetadata *model.LinkMetadata) (*model.LinkMetadata, error) {
tries := 0
for {
result, err := s.LinkMetadataStore.Save(linkMetadata)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerNotifyAdminStore) DeleteBefore(trial bool, now int64) error {
tries := 0
for {
err := s.NotifyAdminStore.DeleteBefore(trial, now)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerNotifyAdminStore) Get(trial bool) ([]*model.NotifyAdminData, error) {
tries := 0
for {
result, err := s.NotifyAdminStore.Get(trial)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostFeature) ([]*model.NotifyAdminData, error) {
tries := 0
for {
result, err := s.NotifyAdminStore.GetDataByUserIdAndFeature(userId, feature)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerNotifyAdminStore) Save(data *model.NotifyAdminData) (*model.NotifyAdminData, error) {
tries := 0
for {
result, err := s.NotifyAdminStore.Save(data)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerNotifyAdminStore) Update(userId string, requiredPlan string, requiredFeature model.MattermostFeature, now int64) error {
tries := 0
for {
err := s.NotifyAdminStore.Update(userId, requiredPlan, requiredFeature, now)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) DeleteApp(id string) error {
tries := 0
for {
err := s.OAuthStore.DeleteApp(id)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) GetAccessData(token string) (*model.AccessData, error) {
tries := 0
for {
result, err := s.OAuthStore.GetAccessData(token)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) GetAccessDataByRefreshToken(token string) (*model.AccessData, error) {
tries := 0
for {
result, err := s.OAuthStore.GetAccessDataByRefreshToken(token)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) GetAccessDataByUserForApp(userID string, clientId string) ([]*model.AccessData, error) {
tries := 0
for {
result, err := s.OAuthStore.GetAccessDataByUserForApp(userID, clientId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) GetApp(id string) (*model.OAuthApp, error) {
tries := 0
for {
result, err := s.OAuthStore.GetApp(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) GetAppByUser(userID string, offset int, limit int) ([]*model.OAuthApp, error) {
tries := 0
for {
result, err := s.OAuthStore.GetAppByUser(userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) GetApps(offset int, limit int) ([]*model.OAuthApp, error) {
tries := 0
for {
result, err := s.OAuthStore.GetApps(offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) GetAuthData(code string) (*model.AuthData, error) {
tries := 0
for {
result, err := s.OAuthStore.GetAuthData(code)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) GetAuthorizedApps(userID string, offset int, limit int) ([]*model.OAuthApp, error) {
tries := 0
for {
result, err := s.OAuthStore.GetAuthorizedApps(userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) GetPreviousAccessData(userID string, clientId string) (*model.AccessData, error) {
tries := 0
for {
result, err := s.OAuthStore.GetPreviousAccessData(userID, clientId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) PermanentDeleteAuthDataByUser(userID string) error {
tries := 0
for {
err := s.OAuthStore.PermanentDeleteAuthDataByUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) RemoveAccessData(token string) error {
tries := 0
for {
err := s.OAuthStore.RemoveAccessData(token)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) RemoveAllAccessData() error {
tries := 0
for {
err := s.OAuthStore.RemoveAllAccessData()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) RemoveAuthData(code string) error {
tries := 0
for {
err := s.OAuthStore.RemoveAuthData(code)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) RemoveAuthDataByClientId(clientId string, userId string) error {
tries := 0
for {
err := s.OAuthStore.RemoveAuthDataByClientId(clientId, userId)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) SaveAccessData(accessData *model.AccessData) (*model.AccessData, error) {
tries := 0
for {
result, err := s.OAuthStore.SaveAccessData(accessData)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) SaveApp(app *model.OAuthApp) (*model.OAuthApp, error) {
tries := 0
for {
result, err := s.OAuthStore.SaveApp(app)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) SaveAuthData(authData *model.AuthData) (*model.AuthData, error) {
tries := 0
for {
result, err := s.OAuthStore.SaveAuthData(authData)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) UpdateAccessData(accessData *model.AccessData) (*model.AccessData, error) {
tries := 0
for {
result, err := s.OAuthStore.UpdateAccessData(accessData)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) UpdateApp(app *model.OAuthApp) (*model.OAuthApp, error) {
tries := 0
for {
result, err := s.OAuthStore.UpdateApp(app)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPluginStore) CompareAndDelete(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error) {
tries := 0
for {
result, err := s.PluginStore.CompareAndDelete(keyVal, oldValue)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPluginStore) CompareAndSet(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error) {
tries := 0
for {
result, err := s.PluginStore.CompareAndSet(keyVal, oldValue)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPluginStore) Delete(pluginID string, key string) error {
tries := 0
for {
err := s.PluginStore.Delete(pluginID, key)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPluginStore) DeleteAllExpired() error {
tries := 0
for {
err := s.PluginStore.DeleteAllExpired()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPluginStore) DeleteAllForPlugin(PluginID string) error {
tries := 0
for {
err := s.PluginStore.DeleteAllForPlugin(PluginID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPluginStore) Get(pluginID string, key string) (*model.PluginKeyValue, error) {
tries := 0
for {
result, err := s.PluginStore.Get(pluginID, key)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPluginStore) List(pluginID string, page int, perPage int) ([]string, error) {
tries := 0
for {
result, err := s.PluginStore.List(pluginID, page, perPage)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPluginStore) SaveOrUpdate(keyVal *model.PluginKeyValue) (*model.PluginKeyValue, error) {
tries := 0
for {
result, err := s.PluginStore.SaveOrUpdate(keyVal)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPluginStore) SetWithOptions(pluginID string, key string, value []byte, options model.PluginKVSetOptions) (bool, error) {
tries := 0
for {
result, err := s.PluginStore.SetWithOptions(pluginID, key, value, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) AnalyticsPostCount(options *model.PostCountOptions) (int64, error) {
tries := 0
for {
result, err := s.PostStore.AnalyticsPostCount(options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) AnalyticsPostCountsByDay(options *model.AnalyticsPostCountsOptions) (model.AnalyticsRows, error) {
tries := 0
for {
result, err := s.PostStore.AnalyticsPostCountsByDay(options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) AnalyticsUserCountsWithPostsByDay(teamID string) (model.AnalyticsRows, error) {
tries := 0
for {
result, err := s.PostStore.AnalyticsUserCountsWithPostsByDay(teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) ClearCaches() {
s.PostStore.ClearCaches()
}
func (s *RetryLayerPostStore) Delete(postID string, timestamp int64, deleteByID string) error {
tries := 0
for {
err := s.PostStore.Delete(postID, timestamp, deleteByID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) DeleteOrphanedRows(limit int) (int64, error) {
tries := 0
for {
result, err := s.PostStore.DeleteOrphanedRows(limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) {
tries := 0
for {
result, err := s.PostStore.Get(ctx, id, opts, userID, sanitizeOptions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetDirectPostParentsForExportAfter(limit int, afterID string) ([]*model.DirectPostForExport, error) {
tries := 0
for {
result, err := s.PostStore.GetDirectPostParentsForExportAfter(limit, afterID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetEditHistoryForPost(postId string) ([]*model.Post, error) {
tries := 0
for {
result, err := s.PostStore.GetEditHistoryForPost(postId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetEtag(channelID string, allowFromCache bool, collapsedThreads bool) string {
return s.PostStore.GetEtag(channelID, allowFromCache, collapsedThreads)
}
func (s *RetryLayerPostStore) GetFlaggedPosts(userID string, offset int, limit int) (*model.PostList, error) {
tries := 0
for {
result, err := s.PostStore.GetFlaggedPosts(userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetFlaggedPostsForChannel(userID string, channelID string, offset int, limit int) (*model.PostList, error) {
tries := 0
for {
result, err := s.PostStore.GetFlaggedPostsForChannel(userID, channelID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetFlaggedPostsForTeam(userID string, teamID string, offset int, limit int) (*model.PostList, error) {
tries := 0
for {
result, err := s.PostStore.GetFlaggedPostsForTeam(userID, teamID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetMaxPostSize() int {
return s.PostStore.GetMaxPostSize()
}
func (s *RetryLayerPostStore) GetNthRecentPostTime(n int64) (int64, error) {
tries := 0
for {
result, err := s.PostStore.GetNthRecentPostTime(n)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetOldest() (*model.Post, error) {
tries := 0
for {
result, err := s.PostStore.GetOldest()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetOldestEntityCreationTime() (int64, error) {
tries := 0
for {
result, err := s.PostStore.GetOldestEntityCreationTime()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetParentsForExportAfter(limit int, afterID string) ([]*model.PostForExport, error) {
tries := 0
for {
result, err := s.PostStore.GetParentsForExportAfter(limit, afterID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostAfterTime(channelID string, timestamp int64, collapsedThreads bool) (*model.Post, error) {
tries := 0
for {
result, err := s.PostStore.GetPostAfterTime(channelID, timestamp, collapsedThreads)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostIdAfterTime(channelID string, timestamp int64, collapsedThreads bool) (string, error) {
tries := 0
for {
result, err := s.PostStore.GetPostIdAfterTime(channelID, timestamp, collapsedThreads)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostIdBeforeTime(channelID string, timestamp int64, collapsedThreads bool) (string, error) {
tries := 0
for {
result, err := s.PostStore.GetPostIdBeforeTime(channelID, timestamp, collapsedThreads)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostReminderMetadata(postID string) (*store.PostReminderMetadata, error) {
tries := 0
for {
result, err := s.PostStore.GetPostReminderMetadata(postID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostReminders(now int64) ([]*model.PostReminder, error) {
tries := 0
for {
result, err := s.PostStore.GetPostReminders(now)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPosts(options model.GetPostsOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
tries := 0
for {
result, err := s.PostStore.GetPosts(options, allowFromCache, sanitizeOptions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostsAfter(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
tries := 0
for {
result, err := s.PostStore.GetPostsAfter(options, sanitizeOptions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostsBatchForIndexing(startTime int64, startPostID string, limit int) ([]*model.PostForIndexing, error) {
tries := 0
for {
result, err := s.PostStore.GetPostsBatchForIndexing(startTime, startPostID, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostsBefore(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
tries := 0
for {
result, err := s.PostStore.GetPostsBefore(options, sanitizeOptions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostsByIds(postIds []string) ([]*model.Post, error) {
tries := 0
for {
result, err := s.PostStore.GetPostsByIds(postIds)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostsByThread(threadID string, since int64) ([]*model.Post, error) {
tries := 0
for {
result, err := s.PostStore.GetPostsByThread(threadID, since)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostsCreatedAt(channelID string, timestamp int64) ([]*model.Post, error) {
tries := 0
for {
result, err := s.PostStore.GetPostsCreatedAt(channelID, timestamp)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostsSince(options model.GetPostsSinceOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
tries := 0
for {
result, err := s.PostStore.GetPostsSince(options, allowFromCache, sanitizeOptions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetPostsSinceForSync(options model.GetPostsSinceForSyncOptions, cursor model.GetPostsSinceForSyncCursor, limit int) ([]*model.Post, model.GetPostsSinceForSyncCursor, error) {
tries := 0
for {
result, resultVar1, err := s.PostStore.GetPostsSinceForSync(options, cursor, limit)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) {
tries := 0
for {
result, err := s.PostStore.GetRecentSearchesForUser(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetRepliesForExport(parentID string) ([]*model.ReplyForExport, error) {
tries := 0
for {
result, err := s.PostStore.GetRepliesForExport(parentID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetSingle(id string, inclDeleted bool) (*model.Post, error) {
tries := 0
for {
result, err := s.PostStore.GetSingle(id, inclDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error) {
tries := 0
for {
result, err := s.PostStore.GetTopDMsForUserSince(userID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
tries := 0
for {
result, err := s.PostStore.HasAutoResponsePostByUserSince(options, userId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) InvalidateLastPostTimeCache(channelID string) {
s.PostStore.InvalidateLastPostTimeCache(channelID)
}
func (s *RetryLayerPostStore) LogRecentSearch(userID string, searchQuery []byte, createAt int64) error {
tries := 0
for {
err := s.PostStore.LogRecentSearch(userID, searchQuery, createAt)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) Overwrite(post *model.Post) (*model.Post, error) {
tries := 0
for {
result, err := s.PostStore.Overwrite(post)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) OverwriteMultiple(posts []*model.Post) ([]*model.Post, int, error) {
tries := 0
for {
result, resultVar1, err := s.PostStore.OverwriteMultiple(posts)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
tries := 0
for {
result, err := s.PostStore.PermanentDeleteBatch(endTime, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
tries := 0
for {
result, resultVar1, err := s.PostStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) PermanentDeleteByChannel(channelID string) error {
tries := 0
for {
err := s.PostStore.PermanentDeleteByChannel(channelID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) PermanentDeleteByUser(userID string) error {
tries := 0
for {
err := s.PostStore.PermanentDeleteByUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) Save(post *model.Post) (*model.Post, error) {
tries := 0
for {
result, err := s.PostStore.Save(post)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, int, error) {
tries := 0
for {
result, resultVar1, err := s.PostStore.SaveMultiple(posts)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) Search(teamID string, userID string, params *model.SearchParams) (*model.PostList, error) {
tries := 0
for {
result, err := s.PostStore.Search(teamID, userID, params)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) SearchPostsForUser(paramsList []*model.SearchParams, userID string, teamID string, page int, perPage int) (*model.PostSearchResults, error) {
tries := 0
for {
result, err := s.PostStore.SearchPostsForUser(paramsList, userID, teamID, page, perPage)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) SetPostReminder(reminder *model.PostReminder) error {
tries := 0
for {
err := s.PostStore.SetPostReminder(reminder)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) Update(newPost *model.Post, oldPost *model.Post) (*model.Post, error) {
tries := 0
for {
result, err := s.PostStore.Update(newPost, oldPost)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostAcknowledgementStore) Delete(acknowledgement *model.PostAcknowledgement) error {
tries := 0
for {
err := s.PostAcknowledgementStore.Delete(acknowledgement)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostAcknowledgementStore) Get(postID string, userID string) (*model.PostAcknowledgement, error) {
tries := 0
for {
result, err := s.PostAcknowledgementStore.Get(postID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostAcknowledgementStore) GetForPost(postID string) ([]*model.PostAcknowledgement, error) {
tries := 0
for {
result, err := s.PostAcknowledgementStore.GetForPost(postID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostAcknowledgementStore) GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) {
tries := 0
for {
result, err := s.PostAcknowledgementStore.GetForPosts(postIds)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostAcknowledgementStore) Save(postID string, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) {
tries := 0
for {
result, err := s.PostAcknowledgementStore.Save(postID, userID, acknowledgedAt)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) {
tries := 0
for {
result, err := s.PostPriorityStore.GetForPost(postId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostPriorityStore) GetForPosts(ids []string) ([]*model.PostPriority, error) {
tries := 0
for {
result, err := s.PostPriorityStore.GetForPosts(ids)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) CleanupFlagsBatch(limit int64) (int64, error) {
tries := 0
for {
result, err := s.PreferenceStore.CleanupFlagsBatch(limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) Delete(userID string, category string, name string) error {
tries := 0
for {
err := s.PreferenceStore.Delete(userID, category, name)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) DeleteCategory(userID string, category string) error {
tries := 0
for {
err := s.PreferenceStore.DeleteCategory(userID, category)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) DeleteCategoryAndName(category string, name string) error {
tries := 0
for {
err := s.PreferenceStore.DeleteCategoryAndName(category, name)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) DeleteOrphanedRows(limit int) (int64, error) {
tries := 0
for {
result, err := s.PreferenceStore.DeleteOrphanedRows(limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) Get(userID string, category string, name string) (*model.Preference, error) {
tries := 0
for {
result, err := s.PreferenceStore.Get(userID, category, name)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) GetAll(userID string) (model.Preferences, error) {
tries := 0
for {
result, err := s.PreferenceStore.GetAll(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) GetCategory(userID string, category string) (model.Preferences, error) {
tries := 0
for {
result, err := s.PreferenceStore.GetCategory(userID, category)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) GetCategoryAndName(category string, nane string) (model.Preferences, error) {
tries := 0
for {
result, err := s.PreferenceStore.GetCategoryAndName(category, nane)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) PermanentDeleteByUser(userID string) error {
tries := 0
for {
err := s.PreferenceStore.PermanentDeleteByUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPreferenceStore) Save(preferences model.Preferences) error {
tries := 0
for {
err := s.PreferenceStore.Save(preferences)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerProductNoticesStore) Clear(notices []string) error {
tries := 0
for {
err := s.ProductNoticesStore.Clear(notices)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerProductNoticesStore) ClearOldNotices(currentNotices model.ProductNotices) error {
tries := 0
for {
err := s.ProductNoticesStore.ClearOldNotices(currentNotices)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerProductNoticesStore) GetViews(userID string) ([]model.ProductNoticeViewState, error) {
tries := 0
for {
result, err := s.ProductNoticesStore.GetViews(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerProductNoticesStore) View(userID string, notices []string) error {
tries := 0
for {
err := s.ProductNoticesStore.View(userID, notices)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) BulkGetForPosts(postIds []string) ([]*model.Reaction, error) {
tries := 0
for {
result, err := s.ReactionStore.BulkGetForPosts(postIds)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) Delete(reaction *model.Reaction) (*model.Reaction, error) {
tries := 0
for {
result, err := s.ReactionStore.Delete(reaction)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) DeleteAllWithEmojiName(emojiName string) error {
tries := 0
for {
err := s.ReactionStore.DeleteAllWithEmojiName(emojiName)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) DeleteOrphanedRows(limit int) (int64, error) {
tries := 0
for {
result, err := s.ReactionStore.DeleteOrphanedRows(limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) GetForPost(postID string, allowFromCache bool) ([]*model.Reaction, error) {
tries := 0
for {
result, err := s.ReactionStore.GetForPost(postID, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) GetForPostSince(postId string, since int64, excludeRemoteId string, inclDeleted bool) ([]*model.Reaction, error) {
tries := 0
for {
result, err := s.ReactionStore.GetForPostSince(postId, since, excludeRemoteId, inclDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) GetTopForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
tries := 0
for {
result, err := s.ReactionStore.GetTopForTeamSince(teamID, userID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) GetTopForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
tries := 0
for {
result, err := s.ReactionStore.GetTopForUserSince(userID, teamID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
tries := 0
for {
result, err := s.ReactionStore.PermanentDeleteBatch(endTime, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) {
tries := 0
for {
result, err := s.ReactionStore.Save(reaction)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRemoteClusterStore) Delete(remoteClusterId string) (bool, error) {
tries := 0
for {
result, err := s.RemoteClusterStore.Delete(remoteClusterId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRemoteClusterStore) Get(remoteClusterId string) (*model.RemoteCluster, error) {
tries := 0
for {
result, err := s.RemoteClusterStore.Get(remoteClusterId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRemoteClusterStore) GetAll(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
tries := 0
for {
result, err := s.RemoteClusterStore.GetAll(filter)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRemoteClusterStore) Save(rc *model.RemoteCluster) (*model.RemoteCluster, error) {
tries := 0
for {
result, err := s.RemoteClusterStore.Save(rc)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRemoteClusterStore) SetLastPingAt(remoteClusterId string) error {
tries := 0
for {
err := s.RemoteClusterStore.SetLastPingAt(remoteClusterId)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRemoteClusterStore) Update(rc *model.RemoteCluster) (*model.RemoteCluster, error) {
tries := 0
for {
result, err := s.RemoteClusterStore.Update(rc)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRemoteClusterStore) UpdateTopics(remoteClusterId string, topics string) (*model.RemoteCluster, error) {
tries := 0
for {
result, err := s.RemoteClusterStore.UpdateTopics(remoteClusterId, topics)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) AddChannels(policyId string, channelIds []string) error {
tries := 0
for {
err := s.RetentionPolicyStore.AddChannels(policyId, channelIds)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) AddTeams(policyId string, teamIds []string) error {
tries := 0
for {
err := s.RetentionPolicyStore.AddTeams(policyId, teamIds)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) Delete(id string) error {
tries := 0
for {
err := s.RetentionPolicyStore.Delete(id)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) DeleteOrphanedRows(limit int) (int64, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.DeleteOrphanedRows(limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) Get(id string) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.Get(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) GetAll(offset int, limit int) ([]*model.RetentionPolicyWithTeamAndChannelCounts, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.GetAll(offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) GetChannelPoliciesCountForUser(userID string) (int64, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.GetChannelPoliciesCountForUser(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) GetChannelPoliciesForUser(userID string, offset int, limit int) ([]*model.RetentionPolicyForChannel, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.GetChannelPoliciesForUser(userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) GetChannels(policyId string, offset int, limit int) (model.ChannelListWithTeamData, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.GetChannels(policyId, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) GetChannelsCount(policyId string) (int64, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.GetChannelsCount(policyId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) GetCount() (int64, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.GetCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.GetTeamPoliciesCountForUser(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) GetTeamPoliciesForUser(userID string, offset int, limit int) ([]*model.RetentionPolicyForTeam, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.GetTeamPoliciesForUser(userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) GetTeams(policyId string, offset int, limit int) ([]*model.Team, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.GetTeams(policyId, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) GetTeamsCount(policyId string) (int64, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.GetTeamsCount(policyId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) Patch(patch *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.Patch(patch)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) RemoveChannels(policyId string, channelIds []string) error {
tries := 0
for {
err := s.RetentionPolicyStore.RemoveChannels(policyId, channelIds)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) RemoveTeams(policyId string, teamIds []string) error {
tries := 0
for {
err := s.RetentionPolicyStore.RemoveTeams(policyId, teamIds)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRetentionPolicyStore) Save(policy *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
tries := 0
for {
result, err := s.RetentionPolicyStore.Save(policy)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRoleStore) AllChannelSchemeRoles() ([]*model.Role, error) {
tries := 0
for {
result, err := s.RoleStore.AllChannelSchemeRoles()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRoleStore) ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, error) {
tries := 0
for {
result, err := s.RoleStore.ChannelHigherScopedPermissions(roleNames)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRoleStore) ChannelRolesUnderTeamRole(roleName string) ([]*model.Role, error) {
tries := 0
for {
result, err := s.RoleStore.ChannelRolesUnderTeamRole(roleName)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRoleStore) Delete(roleID string) (*model.Role, error) {
tries := 0
for {
result, err := s.RoleStore.Delete(roleID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRoleStore) Get(roleID string) (*model.Role, error) {
tries := 0
for {
result, err := s.RoleStore.Get(roleID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRoleStore) GetAll() ([]*model.Role, error) {
tries := 0
for {
result, err := s.RoleStore.GetAll()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRoleStore) GetByName(ctx context.Context, name string) (*model.Role, error) {
tries := 0
for {
result, err := s.RoleStore.GetByName(ctx, name)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRoleStore) GetByNames(names []string) ([]*model.Role, error) {
tries := 0
for {
result, err := s.RoleStore.GetByNames(names)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRoleStore) PermanentDeleteAll() error {
tries := 0
for {
err := s.RoleStore.PermanentDeleteAll()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerRoleStore) Save(role *model.Role) (*model.Role, error) {
tries := 0
for {
result, err := s.RoleStore.Save(role)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSchemeStore) CountByScope(scope string) (int64, error) {
tries := 0
for {
result, err := s.SchemeStore.CountByScope(scope)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSchemeStore) CountWithoutPermission(scope string, permissionID string, roleScope model.RoleScope, roleType model.RoleType) (int64, error) {
tries := 0
for {
result, err := s.SchemeStore.CountWithoutPermission(scope, permissionID, roleScope, roleType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSchemeStore) Delete(schemeID string) (*model.Scheme, error) {
tries := 0
for {
result, err := s.SchemeStore.Delete(schemeID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSchemeStore) Get(schemeID string) (*model.Scheme, error) {
tries := 0
for {
result, err := s.SchemeStore.Get(schemeID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSchemeStore) GetAllPage(scope string, offset int, limit int) ([]*model.Scheme, error) {
tries := 0
for {
result, err := s.SchemeStore.GetAllPage(scope, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSchemeStore) GetByName(schemeName string) (*model.Scheme, error) {
tries := 0
for {
result, err := s.SchemeStore.GetByName(schemeName)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSchemeStore) PermanentDeleteAll() error {
tries := 0
for {
err := s.SchemeStore.PermanentDeleteAll()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSchemeStore) Save(scheme *model.Scheme) (*model.Scheme, error) {
tries := 0
for {
result, err := s.SchemeStore.Save(scheme)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) AnalyticsSessionCount() (int64, error) {
tries := 0
for {
result, err := s.SessionStore.AnalyticsSessionCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) Cleanup(expiryTime int64, batchSize int64) error {
tries := 0
for {
err := s.SessionStore.Cleanup(expiryTime, batchSize)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) Get(ctx context.Context, sessionIDOrToken string) (*model.Session, error) {
tries := 0
for {
result, err := s.SessionStore.Get(ctx, sessionIDOrToken)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) GetSessions(userID string) ([]*model.Session, error) {
tries := 0
for {
result, err := s.SessionStore.GetSessions(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) GetSessionsExpired(thresholdMillis int64, mobileOnly bool, unnotifiedOnly bool) ([]*model.Session, error) {
tries := 0
for {
result, err := s.SessionStore.GetSessionsExpired(thresholdMillis, mobileOnly, unnotifiedOnly)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) GetSessionsWithActiveDeviceIds(userID string) ([]*model.Session, error) {
tries := 0
for {
result, err := s.SessionStore.GetSessionsWithActiveDeviceIds(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) PermanentDeleteSessionsByUser(teamID string) error {
tries := 0
for {
err := s.SessionStore.PermanentDeleteSessionsByUser(teamID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) Remove(sessionIDOrToken string) error {
tries := 0
for {
err := s.SessionStore.Remove(sessionIDOrToken)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) RemoveAllSessions() error {
tries := 0
for {
err := s.SessionStore.RemoveAllSessions()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) Save(session *model.Session) (*model.Session, error) {
tries := 0
for {
result, err := s.SessionStore.Save(session)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) UpdateDeviceId(id string, deviceID string, expiresAt int64) (string, error) {
tries := 0
for {
result, err := s.SessionStore.UpdateDeviceId(id, deviceID, expiresAt)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) UpdateExpiredNotify(sessionid string, notified bool) error {
tries := 0
for {
err := s.SessionStore.UpdateExpiredNotify(sessionid, notified)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) UpdateExpiresAt(sessionID string, timestamp int64) error {
tries := 0
for {
err := s.SessionStore.UpdateExpiresAt(sessionID, timestamp)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) UpdateLastActivityAt(sessionID string, timestamp int64) error {
tries := 0
for {
err := s.SessionStore.UpdateLastActivityAt(sessionID, timestamp)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) UpdateProps(session *model.Session) error {
tries := 0
for {
err := s.SessionStore.UpdateProps(session)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) UpdateRoles(userID string, roles string) (string, error) {
tries := 0
for {
result, err := s.SessionStore.UpdateRoles(userID, roles)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) Delete(channelId string) (bool, error) {
tries := 0
for {
result, err := s.SharedChannelStore.Delete(channelId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) DeleteRemote(remoteId string) (bool, error) {
tries := 0
for {
result, err := s.SharedChannelStore.DeleteRemote(remoteId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) Get(channelId string) (*model.SharedChannel, error) {
tries := 0
for {
result, err := s.SharedChannelStore.Get(channelId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetAll(offset int, limit int, opts model.SharedChannelFilterOpts) ([]*model.SharedChannel, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetAll(offset, limit, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetAllCount(opts model.SharedChannelFilterOpts) (int64, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetAllCount(opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetAttachment(fileId string, remoteId string) (*model.SharedChannelAttachment, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetAttachment(fileId, remoteId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetRemote(id string) (*model.SharedChannelRemote, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetRemote(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetRemoteByIds(channelId string, remoteId string) (*model.SharedChannelRemote, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetRemoteByIds(channelId, remoteId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetRemoteForUser(remoteId string, userId string) (*model.RemoteCluster, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetRemoteForUser(remoteId, userId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetRemotes(opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetRemotesStatus(channelId string) ([]*model.SharedChannelRemoteStatus, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetRemotesStatus(channelId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetSingleUser(userID string, channelID string, remoteID string) (*model.SharedChannelUser, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetSingleUser(userID, channelID, remoteID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetUsersForSync(filter model.GetUsersForSyncFilter) ([]*model.User, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetUsersForSync(filter)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) GetUsersForUser(userID string) ([]*model.SharedChannelUser, error) {
tries := 0
for {
result, err := s.SharedChannelStore.GetUsersForUser(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) HasChannel(channelID string) (bool, error) {
tries := 0
for {
result, err := s.SharedChannelStore.HasChannel(channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) HasRemote(channelID string, remoteId string) (bool, error) {
tries := 0
for {
result, err := s.SharedChannelStore.HasRemote(channelID, remoteId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) Save(sc *model.SharedChannel) (*model.SharedChannel, error) {
tries := 0
for {
result, err := s.SharedChannelStore.Save(sc)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) SaveAttachment(remote *model.SharedChannelAttachment) (*model.SharedChannelAttachment, error) {
tries := 0
for {
result, err := s.SharedChannelStore.SaveAttachment(remote)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) SaveRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
tries := 0
for {
result, err := s.SharedChannelStore.SaveRemote(remote)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) SaveUser(remote *model.SharedChannelUser) (*model.SharedChannelUser, error) {
tries := 0
for {
result, err := s.SharedChannelStore.SaveUser(remote)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) Update(sc *model.SharedChannel) (*model.SharedChannel, error) {
tries := 0
for {
result, err := s.SharedChannelStore.Update(sc)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) UpdateAttachmentLastSyncAt(id string, syncTime int64) error {
tries := 0
for {
err := s.SharedChannelStore.UpdateAttachmentLastSyncAt(id, syncTime)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) UpdateRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
tries := 0
for {
result, err := s.SharedChannelStore.UpdateRemote(remote)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) UpdateRemoteCursor(id string, cursor model.GetPostsSinceForSyncCursor) error {
tries := 0
for {
err := s.SharedChannelStore.UpdateRemoteCursor(id, cursor)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) UpdateUserLastSyncAt(userID string, channelID string, remoteID string) error {
tries := 0
for {
err := s.SharedChannelStore.UpdateUserLastSyncAt(userID, channelID, remoteID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSharedChannelStore) UpsertAttachment(remote *model.SharedChannelAttachment) (string, error) {
tries := 0
for {
result, err := s.SharedChannelStore.UpsertAttachment(remote)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerStatusStore) Get(userID string) (*model.Status, error) {
tries := 0
for {
result, err := s.StatusStore.Get(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerStatusStore) GetByIds(userIds []string) ([]*model.Status, error) {
tries := 0
for {
result, err := s.StatusStore.GetByIds(userIds)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerStatusStore) GetTotalActiveUsersCount() (int64, error) {
tries := 0
for {
result, err := s.StatusStore.GetTotalActiveUsersCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerStatusStore) ResetAll() error {
tries := 0
for {
err := s.StatusStore.ResetAll()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerStatusStore) SaveOrUpdate(status *model.Status) error {
tries := 0
for {
err := s.StatusStore.SaveOrUpdate(status)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerStatusStore) UpdateExpiredDNDStatuses() ([]*model.Status, error) {
tries := 0
for {
result, err := s.StatusStore.UpdateExpiredDNDStatuses()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerStatusStore) UpdateLastActivityAt(userID string, lastActivityAt int64) error {
tries := 0
for {
err := s.StatusStore.UpdateLastActivityAt(userID, lastActivityAt)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSystemStore) Get() (model.StringMap, error) {
tries := 0
for {
result, err := s.SystemStore.Get()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSystemStore) GetByName(name string) (*model.System, error) {
tries := 0
for {
result, err := s.SystemStore.GetByName(name)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSystemStore) InsertIfExists(system *model.System) (*model.System, error) {
tries := 0
for {
result, err := s.SystemStore.InsertIfExists(system)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSystemStore) PermanentDeleteByName(name string) (*model.System, error) {
tries := 0
for {
result, err := s.SystemStore.PermanentDeleteByName(name)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSystemStore) Save(system *model.System) error {
tries := 0
for {
err := s.SystemStore.Save(system)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSystemStore) SaveOrUpdate(system *model.System) error {
tries := 0
for {
err := s.SystemStore.SaveOrUpdate(system)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSystemStore) SaveOrUpdateWithWarnMetricHandling(system *model.System) error {
tries := 0
for {
err := s.SystemStore.SaveOrUpdateWithWarnMetricHandling(system)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSystemStore) Update(system *model.System) error {
tries := 0
for {
err := s.SystemStore.Update(system)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) AnalyticsGetTeamCountForScheme(schemeID string) (int64, error) {
tries := 0
for {
result, err := s.TeamStore.AnalyticsGetTeamCountForScheme(schemeID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) AnalyticsTeamCount(opts *model.TeamSearch) (int64, error) {
tries := 0
for {
result, err := s.TeamStore.AnalyticsTeamCount(opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) ClearAllCustomRoleAssignments() error {
tries := 0
for {
err := s.TeamStore.ClearAllCustomRoleAssignments()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) ClearCaches() {
s.TeamStore.ClearCaches()
}
func (s *RetryLayerTeamStore) Get(id string) (*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.Get(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetActiveMemberCount(teamID string, restrictions *model.ViewUsersRestrictions) (int64, error) {
tries := 0
for {
result, err := s.TeamStore.GetActiveMemberCount(teamID, restrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetAll() ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetAll()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetAllForExportAfter(limit int, afterID string) ([]*model.TeamForExport, error) {
tries := 0
for {
result, err := s.TeamStore.GetAllForExportAfter(limit, afterID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetAllPage(offset int, limit int, opts *model.TeamSearch) ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetAllPage(offset, limit, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetAllPrivateTeamListing() ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetAllPrivateTeamListing()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetAllTeamListing() ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetAllTeamListing()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetByEmptyInviteID() ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetByEmptyInviteID()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetByInviteId(inviteID string) (*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetByInviteId(inviteID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetByName(name string) (*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetByName(name)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetByNames(name []string) ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetByNames(name)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetChannelUnreadsForAllTeams(excludeTeamID string, userID string) ([]*model.ChannelUnread, error) {
tries := 0
for {
result, err := s.TeamStore.GetChannelUnreadsForAllTeams(excludeTeamID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetChannelUnreadsForTeam(teamID string, userID string) ([]*model.ChannelUnread, error) {
tries := 0
for {
result, err := s.TeamStore.GetChannelUnreadsForTeam(teamID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetCommonTeamIDsForTwoUsers(userID string, otherUserID string) ([]string, error) {
tries := 0
for {
result, err := s.TeamStore.GetCommonTeamIDsForTwoUsers(userID, otherUserID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetMany(ids []string) ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetMany(ids)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetMember(ctx context.Context, teamID string, userID string) (*model.TeamMember, error) {
tries := 0
for {
result, err := s.TeamStore.GetMember(ctx, teamID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetMembers(teamID string, offset int, limit int, teamMembersGetOptions *model.TeamMembersGetOptions) ([]*model.TeamMember, error) {
tries := 0
for {
result, err := s.TeamStore.GetMembers(teamID, offset, limit, teamMembersGetOptions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetMembersByIds(teamID string, userIds []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, error) {
tries := 0
for {
result, err := s.TeamStore.GetMembersByIds(teamID, userIds, restrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetNewTeamMembersSince(teamID string, since int64, offset int, limit int) (*model.NewTeamMembersList, int64, error) {
tries := 0
for {
result, resultVar1, err := s.TeamStore.GetNewTeamMembersSince(teamID, since, offset, limit)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetTeamMembersForExport(userID string) ([]*model.TeamMemberForExport, error) {
tries := 0
for {
result, err := s.TeamStore.GetTeamMembersForExport(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetTeamsByScheme(schemeID string, offset int, limit int) ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetTeamsByScheme(schemeID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetTeamsByUserId(userID string) ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.GetTeamsByUserId(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetTeamsForUser(ctx context.Context, userID string, excludeTeamID string, includeDeleted bool) ([]*model.TeamMember, error) {
tries := 0
for {
result, err := s.TeamStore.GetTeamsForUser(ctx, userID, excludeTeamID, includeDeleted)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetTeamsForUserWithPagination(userID string, page int, perPage int) ([]*model.TeamMember, error) {
tries := 0
for {
result, err := s.TeamStore.GetTeamsForUserWithPagination(userID, page, perPage)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetTotalMemberCount(teamID string, restrictions *model.ViewUsersRestrictions) (int64, error) {
tries := 0
for {
result, err := s.TeamStore.GetTotalMemberCount(teamID, restrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetUserTeamIds(userID string, allowFromCache bool) ([]string, error) {
tries := 0
for {
result, err := s.TeamStore.GetUserTeamIds(userID, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GroupSyncedTeamCount() (int64, error) {
tries := 0
for {
result, err := s.TeamStore.GroupSyncedTeamCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) InvalidateAllTeamIdsForUser(userID string) {
s.TeamStore.InvalidateAllTeamIdsForUser(userID)
}
func (s *RetryLayerTeamStore) MigrateTeamMembers(fromTeamID string, fromUserID string) (map[string]string, error) {
tries := 0
for {
result, err := s.TeamStore.MigrateTeamMembers(fromTeamID, fromUserID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) PermanentDelete(teamID string) error {
tries := 0
for {
err := s.TeamStore.PermanentDelete(teamID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) RemoveAllMembersByTeam(teamID string) error {
tries := 0
for {
err := s.TeamStore.RemoveAllMembersByTeam(teamID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) RemoveAllMembersByUser(userID string) error {
tries := 0
for {
err := s.TeamStore.RemoveAllMembersByUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) RemoveMember(teamID string, userID string) error {
tries := 0
for {
err := s.TeamStore.RemoveMember(teamID, userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) RemoveMembers(teamID string, userIds []string) error {
tries := 0
for {
err := s.TeamStore.RemoveMembers(teamID, userIds)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) ResetAllTeamSchemes() error {
tries := 0
for {
err := s.TeamStore.ResetAllTeamSchemes()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) Save(team *model.Team) (*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.Save(team)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) SaveMember(member *model.TeamMember, maxUsersPerTeam int) (*model.TeamMember, error) {
tries := 0
for {
result, err := s.TeamStore.SaveMember(member, maxUsersPerTeam)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) SaveMultipleMembers(members []*model.TeamMember, maxUsersPerTeam int) ([]*model.TeamMember, error) {
tries := 0
for {
result, err := s.TeamStore.SaveMultipleMembers(members, maxUsersPerTeam)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) SearchAll(opts *model.TeamSearch) ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.SearchAll(opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) SearchAllPaged(opts *model.TeamSearch) ([]*model.Team, int64, error) {
tries := 0
for {
result, resultVar1, err := s.TeamStore.SearchAllPaged(opts)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) SearchOpen(opts *model.TeamSearch) ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.SearchOpen(opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) SearchPrivate(opts *model.TeamSearch) ([]*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.SearchPrivate(opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) Update(team *model.Team) (*model.Team, error) {
tries := 0
for {
result, err := s.TeamStore.Update(team)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) UpdateLastTeamIconUpdate(teamID string, curTime int64) error {
tries := 0
for {
err := s.TeamStore.UpdateLastTeamIconUpdate(teamID, curTime)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) UpdateMember(member *model.TeamMember) (*model.TeamMember, error) {
tries := 0
for {
result, err := s.TeamStore.UpdateMember(member)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) UpdateMembersRole(teamID string, userIDs []string) error {
tries := 0
for {
err := s.TeamStore.UpdateMembersRole(teamID, userIDs)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) UpdateMultipleMembers(members []*model.TeamMember) ([]*model.TeamMember, error) {
tries := 0
for {
result, err := s.TeamStore.UpdateMultipleMembers(members)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) UserBelongsToTeams(userID string, teamIds []string) (bool, error) {
tries := 0
for {
result, err := s.TeamStore.UserBelongsToTeams(userID, teamIds)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTermsOfServiceStore) Get(id string, allowFromCache bool) (*model.TermsOfService, error) {
tries := 0
for {
result, err := s.TermsOfServiceStore.Get(id, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTermsOfServiceStore) GetLatest(allowFromCache bool) (*model.TermsOfService, error) {
tries := 0
for {
result, err := s.TermsOfServiceStore.GetLatest(allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTermsOfServiceStore) Save(termsOfService *model.TermsOfService) (*model.TermsOfService, error) {
tries := 0
for {
result, err := s.TermsOfServiceStore.Save(termsOfService)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) DeleteMembershipForUser(userId string, postID string) error {
tries := 0
for {
err := s.ThreadStore.DeleteMembershipForUser(userId, postID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
tries := 0
for {
result, err := s.ThreadStore.DeleteOrphanedRows(limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) Get(id string) (*model.Thread, error) {
tries := 0
for {
result, err := s.ThreadStore.Get(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetMembershipForUser(userId string, postID string) (*model.ThreadMembership, error) {
tries := 0
for {
result, err := s.ThreadStore.GetMembershipForUser(userId, postID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetMembershipsForUser(userId string, teamID string) ([]*model.ThreadMembership, error) {
tries := 0
for {
result, err := s.ThreadStore.GetMembershipsForUser(userId, teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string, includeUrgentMentionCount bool) (map[string]*model.TeamUnread, error) {
tries := 0
for {
result, err := s.ThreadStore.GetTeamsUnreadForUser(userID, teamIDs, includeUrgentMentionCount)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetThreadFollowers(threadID string, fetchOnlyActive bool) ([]string, error) {
tries := 0
for {
result, err := s.ThreadStore.GetThreadFollowers(threadID, fetchOnlyActive)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool, postPriorityIsEnabled bool) (*model.ThreadResponse, error) {
tries := 0
for {
result, err := s.ThreadStore.GetThreadForUser(threadMembership, extended, postPriorityIsEnabled)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error) {
tries := 0
for {
result, err := s.ThreadStore.GetThreadUnreadReplyCount(threadMembership)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetThreadsForUser(userId string, teamID string, opts model.GetUserThreadsOpts) ([]*model.ThreadResponse, error) {
tries := 0
for {
result, err := s.ThreadStore.GetThreadsForUser(userId, teamID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
tries := 0
for {
result, err := s.ThreadStore.GetTopThreadsForTeamSince(teamID, userID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
tries := 0
for {
result, err := s.ThreadStore.GetTopThreadsForUserSince(teamID, userID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetTotalThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
tries := 0
for {
result, err := s.ThreadStore.GetTotalThreads(userId, teamID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetTotalUnreadMentions(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
tries := 0
for {
result, err := s.ThreadStore.GetTotalUnreadMentions(userId, teamID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetTotalUnreadThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
tries := 0
for {
result, err := s.ThreadStore.GetTotalUnreadThreads(userId, teamID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetTotalUnreadUrgentMentions(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
tries := 0
for {
result, err := s.ThreadStore.GetTotalUnreadUrgentMentions(userId, teamID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) MaintainMembership(userID string, postID string, opts store.ThreadMembershipOpts) (*model.ThreadMembership, error) {
tries := 0
for {
result, err := s.ThreadStore.MaintainMembership(userID, postID, opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) MarkAllAsRead(userID string, threadIds []string) error {
tries := 0
for {
err := s.ThreadStore.MarkAllAsRead(userID, threadIds)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) MarkAllAsReadByChannels(userID string, channelIDs []string) error {
tries := 0
for {
err := s.ThreadStore.MarkAllAsReadByChannels(userID, channelIDs)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) MarkAllAsReadByTeam(userID string, teamID string) error {
tries := 0
for {
err := s.ThreadStore.MarkAllAsReadByTeam(userID, teamID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) MarkAsRead(userID string, threadID string, timestamp int64) error {
tries := 0
for {
err := s.ThreadStore.MarkAsRead(userID, threadID, timestamp)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
tries := 0
for {
result, resultVar1, err := s.ThreadStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
tries := 0
for {
result, resultVar1, err := s.ThreadStore.PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) UpdateMembership(membership *model.ThreadMembership) (*model.ThreadMembership, error) {
tries := 0
for {
result, err := s.ThreadStore.UpdateMembership(membership)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTokenStore) Cleanup(expiryTime int64) {
s.TokenStore.Cleanup(expiryTime)
}
func (s *RetryLayerTokenStore) Delete(token string) error {
tries := 0
for {
err := s.TokenStore.Delete(token)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTokenStore) GetAllTokensByType(tokenType string) ([]*model.Token, error) {
tries := 0
for {
result, err := s.TokenStore.GetAllTokensByType(tokenType)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTokenStore) GetByToken(token string) (*model.Token, error) {
tries := 0
for {
result, err := s.TokenStore.GetByToken(token)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTokenStore) RemoveAllTokensByType(tokenType string) error {
tries := 0
for {
err := s.TokenStore.RemoveAllTokensByType(tokenType)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTokenStore) Save(recovery *model.Token) error {
tries := 0
for {
err := s.TokenStore.Save(recovery)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTrueUpReviewStore) CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
tries := 0
for {
result, err := s.TrueUpReviewStore.CreateTrueUpReviewStatusRecord(reviewStatus)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTrueUpReviewStore) GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error) {
tries := 0
for {
result, err := s.TrueUpReviewStore.GetTrueUpReviewStatus(dueDate)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTrueUpReviewStore) Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
tries := 0
for {
result, err := s.TrueUpReviewStore.Update(reviewStatus)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUploadSessionStore) Delete(id string) error {
tries := 0
for {
err := s.UploadSessionStore.Delete(id)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUploadSessionStore) Get(ctx context.Context, id string) (*model.UploadSession, error) {
tries := 0
for {
result, err := s.UploadSessionStore.Get(ctx, id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUploadSessionStore) GetForUser(userID string) ([]*model.UploadSession, error) {
tries := 0
for {
result, err := s.UploadSessionStore.GetForUser(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUploadSessionStore) Save(session *model.UploadSession) (*model.UploadSession, error) {
tries := 0
for {
result, err := s.UploadSessionStore.Save(session)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUploadSessionStore) Update(session *model.UploadSession) error {
tries := 0
for {
err := s.UploadSessionStore.Update(session)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) AnalyticsActiveCount(timestamp int64, options model.UserCountOptions) (int64, error) {
tries := 0
for {
result, err := s.UserStore.AnalyticsActiveCount(timestamp, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) AnalyticsActiveCountForPeriod(startTime int64, endTime int64, options model.UserCountOptions) (int64, error) {
tries := 0
for {
result, err := s.UserStore.AnalyticsActiveCountForPeriod(startTime, endTime, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) AnalyticsGetExternalUsers(hostDomain string) (bool, error) {
tries := 0
for {
result, err := s.UserStore.AnalyticsGetExternalUsers(hostDomain)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) AnalyticsGetGuestCount() (int64, error) {
tries := 0
for {
result, err := s.UserStore.AnalyticsGetGuestCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) AnalyticsGetInactiveUsersCount() (int64, error) {
tries := 0
for {
result, err := s.UserStore.AnalyticsGetInactiveUsersCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) AnalyticsGetSystemAdminCount() (int64, error) {
tries := 0
for {
result, err := s.UserStore.AnalyticsGetSystemAdminCount()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) AutocompleteUsersInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error) {
tries := 0
for {
result, err := s.UserStore.AutocompleteUsersInChannel(teamID, channelID, term, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) ClearAllCustomRoleAssignments() error {
tries := 0
for {
err := s.UserStore.ClearAllCustomRoleAssignments()
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) ClearCaches() {
s.UserStore.ClearCaches()
}
func (s *RetryLayerUserStore) Count(options model.UserCountOptions) (int64, error) {
tries := 0
for {
result, err := s.UserStore.Count(options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) DeactivateGuests() ([]string, error) {
tries := 0
for {
result, err := s.UserStore.DeactivateGuests()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) DemoteUserToGuest(userID string) (*model.User, error) {
tries := 0
for {
result, err := s.UserStore.DemoteUserToGuest(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) Get(ctx context.Context, id string) (*model.User, error) {
tries := 0
for {
result, err := s.UserStore.Get(ctx, id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetAll() ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetAll()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetAllAfter(limit int, afterID string) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetAllAfter(limit, afterID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetAllNotInAuthService(authServices []string) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetAllNotInAuthService(authServices)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetAllProfiles(options *model.UserGetOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetAllProfiles(options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetAllProfilesInChannel(ctx context.Context, channelID string, allowFromCache bool) (map[string]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetAllProfilesInChannel(ctx, channelID, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetAllUsingAuthService(authService string) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetAllUsingAuthService(authService)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetAnyUnreadPostCountForChannel(userID string, channelID string) (int64, error) {
tries := 0
for {
result, err := s.UserStore.GetAnyUnreadPostCountForChannel(userID, channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetByAuth(authData *string, authService string) (*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetByAuth(authData, authService)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetByEmail(email string) (*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetByEmail(email)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetByUsername(username string) (*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetByUsername(username)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetChannelGroupUsers(channelID string) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetChannelGroupUsers(channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetEtagForAllProfiles() string {
return s.UserStore.GetEtagForAllProfiles()
}
func (s *RetryLayerUserStore) GetEtagForProfiles(teamID string) string {
return s.UserStore.GetEtagForProfiles(teamID)
}
func (s *RetryLayerUserStore) GetEtagForProfilesNotInTeam(teamID string) string {
return s.UserStore.GetEtagForProfilesNotInTeam(teamID)
}
func (s *RetryLayerUserStore) GetFirstSystemAdminID() (string, error) {
tries := 0
for {
result, err := s.UserStore.GetFirstSystemAdminID()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetForLogin(loginID string, allowSignInWithUsername bool, allowSignInWithEmail bool) (*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetForLogin(loginID, allowSignInWithUsername, allowSignInWithEmail)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetKnownUsers(userID string) ([]string, error) {
tries := 0
for {
result, err := s.UserStore.GetKnownUsers(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetMany(ctx context.Context, ids []string) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetMany(ctx, ids)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetNewUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetNewUsersForTeam(teamID, offset, limit, viewRestrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetProfileByGroupChannelIdsForUser(userID string, channelIds []string) (map[string][]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetProfileByGroupChannelIdsForUser(userID, channelIds)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetProfileByIds(ctx context.Context, userIds []string, options *store.UserGetByIdsOpts, allowFromCache bool) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetProfileByIds(ctx, userIds, options, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetProfiles(options *model.UserGetOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetProfiles(options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetProfilesByUsernames(usernames []string, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetProfilesByUsernames(usernames, viewRestrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetProfilesInChannel(options *model.UserGetOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetProfilesInChannel(options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetProfilesInChannelByAdmin(options *model.UserGetOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetProfilesInChannelByAdmin(options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetProfilesInChannelByStatus(options *model.UserGetOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetProfilesInChannelByStatus(options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetProfilesNotInChannel(teamID string, channelId string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetProfilesNotInChannel(teamID, channelId, groupConstrained, offset, limit, viewRestrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetProfilesNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetProfilesNotInTeam(teamID, groupConstrained, offset, limit, viewRestrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetProfilesWithoutTeam(options *model.UserGetOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetProfilesWithoutTeam(options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetRecentlyActiveUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetRecentlyActiveUsersForTeam(teamID, offset, limit, viewRestrictions)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetSystemAdminProfiles() (map[string]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetSystemAdminProfiles()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetTeamGroupUsers(teamID string) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetTeamGroupUsers(teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetUnreadCount(userID string, isCRTEnabled bool) (int64, error) {
tries := 0
for {
result, err := s.UserStore.GetUnreadCount(userID, isCRTEnabled)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetUnreadCountForChannel(userID string, channelID string) (int64, error) {
tries := 0
for {
result, err := s.UserStore.GetUnreadCountForChannel(userID, channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
tries := 0
for {
result, err := s.UserStore.GetUsersBatchForIndexing(startTime, startFileID, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetUsersWithInvalidEmails(page int, perPage int, restrictedDomains string) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.GetUsersWithInvalidEmails(page, perPage, restrictedDomains)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) InferSystemInstallDate() (int64, error) {
tries := 0
for {
result, err := s.UserStore.InferSystemInstallDate()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) InsertUsers(users []*model.User) error {
tries := 0
for {
err := s.UserStore.InsertUsers(users)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) InvalidateProfileCacheForUser(userID string) {
s.UserStore.InvalidateProfileCacheForUser(userID)
}
func (s *RetryLayerUserStore) InvalidateProfilesInChannelCache(channelID string) {
s.UserStore.InvalidateProfilesInChannelCache(channelID)
}
func (s *RetryLayerUserStore) InvalidateProfilesInChannelCacheByUser(userID string) {
s.UserStore.InvalidateProfilesInChannelCacheByUser(userID)
}
func (s *RetryLayerUserStore) IsEmpty(excludeBots bool) (bool, error) {
tries := 0
for {
result, err := s.UserStore.IsEmpty(excludeBots)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) PermanentDelete(userID string) error {
tries := 0
for {
err := s.UserStore.PermanentDelete(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) PromoteGuestToUser(userID string) error {
tries := 0
for {
err := s.UserStore.PromoteGuestToUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) ResetAuthDataToEmailForUsers(service string, userIDs []string, includeDeleted bool, dryRun bool) (int, error) {
tries := 0
for {
result, err := s.UserStore.ResetAuthDataToEmailForUsers(service, userIDs, includeDeleted, dryRun)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) ResetLastPictureUpdate(userID string) error {
tries := 0
for {
err := s.UserStore.ResetLastPictureUpdate(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) Save(user *model.User) (*model.User, error) {
tries := 0
for {
result, err := s.UserStore.Save(user)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) Search(teamID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.Search(teamID, term, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) SearchInChannel(channelID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.SearchInChannel(channelID, term, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) SearchInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.SearchInGroup(groupID, term, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) SearchNotInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.SearchNotInChannel(teamID, channelID, term, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) SearchNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.SearchNotInGroup(groupID, term, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) SearchNotInTeam(notInTeamID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.SearchNotInTeam(notInTeamID, term, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) SearchWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, error) {
tries := 0
for {
result, err := s.UserStore.SearchWithoutTeam(term, options)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) Update(user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error) {
tries := 0
for {
result, err := s.UserStore.Update(user, allowRoleUpdate)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) UpdateAuthData(userID string, service string, authData *string, email string, resetMfa bool) (string, error) {
tries := 0
for {
result, err := s.UserStore.UpdateAuthData(userID, service, authData, email, resetMfa)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) UpdateFailedPasswordAttempts(userID string, attempts int) error {
tries := 0
for {
err := s.UserStore.UpdateFailedPasswordAttempts(userID, attempts)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) UpdateLastPictureUpdate(userID string) error {
tries := 0
for {
err := s.UserStore.UpdateLastPictureUpdate(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) UpdateMfaActive(userID string, active bool) error {
tries := 0
for {
err := s.UserStore.UpdateMfaActive(userID, active)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) UpdateMfaSecret(userID string, secret string) error {
tries := 0
for {
err := s.UserStore.UpdateMfaSecret(userID, secret)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) UpdateNotifyProps(userID string, props map[string]string) error {
tries := 0
for {
err := s.UserStore.UpdateNotifyProps(userID, props)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) UpdatePassword(userID string, newPassword string) error {
tries := 0
for {
err := s.UserStore.UpdatePassword(userID, newPassword)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) UpdateUpdateAt(userID string) (int64, error) {
tries := 0
for {
result, err := s.UserStore.UpdateUpdateAt(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) VerifyEmail(userID string, email string) (string, error) {
tries := 0
for {
result, err := s.UserStore.VerifyEmail(userID, email)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserAccessTokenStore) Delete(tokenID string) error {
tries := 0
for {
err := s.UserAccessTokenStore.Delete(tokenID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserAccessTokenStore) DeleteAllForUser(userID string) error {
tries := 0
for {
err := s.UserAccessTokenStore.DeleteAllForUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserAccessTokenStore) Get(tokenID string) (*model.UserAccessToken, error) {
tries := 0
for {
result, err := s.UserAccessTokenStore.Get(tokenID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserAccessTokenStore) GetAll(offset int, limit int) ([]*model.UserAccessToken, error) {
tries := 0
for {
result, err := s.UserAccessTokenStore.GetAll(offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserAccessTokenStore) GetByToken(tokenString string) (*model.UserAccessToken, error) {
tries := 0
for {
result, err := s.UserAccessTokenStore.GetByToken(tokenString)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserAccessTokenStore) GetByUser(userID string, page int, perPage int) ([]*model.UserAccessToken, error) {
tries := 0
for {
result, err := s.UserAccessTokenStore.GetByUser(userID, page, perPage)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserAccessTokenStore) Save(token *model.UserAccessToken) (*model.UserAccessToken, error) {
tries := 0
for {
result, err := s.UserAccessTokenStore.Save(token)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserAccessTokenStore) Search(term string) ([]*model.UserAccessToken, error) {
tries := 0
for {
result, err := s.UserAccessTokenStore.Search(term)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserAccessTokenStore) UpdateTokenDisable(tokenID string) error {
tries := 0
for {
err := s.UserAccessTokenStore.UpdateTokenDisable(tokenID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserAccessTokenStore) UpdateTokenEnable(tokenID string) error {
tries := 0
for {
err := s.UserAccessTokenStore.UpdateTokenEnable(tokenID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserTermsOfServiceStore) Delete(userID string, termsOfServiceId string) error {
tries := 0
for {
err := s.UserTermsOfServiceStore.Delete(userID, termsOfServiceId)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserTermsOfServiceStore) GetByUser(userID string) (*model.UserTermsOfService, error) {
tries := 0
for {
result, err := s.UserTermsOfServiceStore.GetByUser(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserTermsOfServiceStore) Save(userTermsOfService *model.UserTermsOfService) (*model.UserTermsOfService, error) {
tries := 0
for {
result, err := s.UserTermsOfServiceStore.Save(userTermsOfService)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) AnalyticsIncomingCount(teamID string) (int64, error) {
tries := 0
for {
result, err := s.WebhookStore.AnalyticsIncomingCount(teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) AnalyticsOutgoingCount(teamID string) (int64, error) {
tries := 0
for {
result, err := s.WebhookStore.AnalyticsOutgoingCount(teamID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) ClearCaches() {
s.WebhookStore.ClearCaches()
}
func (s *RetryLayerWebhookStore) DeleteIncoming(webhookID string, timestamp int64) error {
tries := 0
for {
err := s.WebhookStore.DeleteIncoming(webhookID, timestamp)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) DeleteOutgoing(webhookID string, timestamp int64) error {
tries := 0
for {
err := s.WebhookStore.DeleteOutgoing(webhookID, timestamp)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetIncoming(id string, allowFromCache bool) (*model.IncomingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetIncoming(id, allowFromCache)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetIncomingByChannel(channelID string) ([]*model.IncomingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetIncomingByChannel(channelID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetIncomingByTeam(teamID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetIncomingByTeam(teamID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetIncomingByTeamByUser(teamID string, userID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetIncomingByTeamByUser(teamID, userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetIncomingList(offset int, limit int) ([]*model.IncomingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetIncomingList(offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetIncomingListByUser(userID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetIncomingListByUser(userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetOutgoing(id string) (*model.OutgoingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetOutgoing(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetOutgoingByChannel(channelID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetOutgoingByChannel(channelID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetOutgoingByChannelByUser(channelID string, userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetOutgoingByChannelByUser(channelID, userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetOutgoingByTeam(teamID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetOutgoingByTeam(teamID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetOutgoingByTeamByUser(teamID string, userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetOutgoingByTeamByUser(teamID, userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetOutgoingList(offset int, limit int) ([]*model.OutgoingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetOutgoingList(offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) GetOutgoingListByUser(userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.GetOutgoingListByUser(userID, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) InvalidateWebhookCache(webhook string) {
s.WebhookStore.InvalidateWebhookCache(webhook)
}
func (s *RetryLayerWebhookStore) PermanentDeleteIncomingByChannel(channelID string) error {
tries := 0
for {
err := s.WebhookStore.PermanentDeleteIncomingByChannel(channelID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) PermanentDeleteIncomingByUser(userID string) error {
tries := 0
for {
err := s.WebhookStore.PermanentDeleteIncomingByUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) PermanentDeleteOutgoingByChannel(channelID string) error {
tries := 0
for {
err := s.WebhookStore.PermanentDeleteOutgoingByChannel(channelID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) PermanentDeleteOutgoingByUser(userID string) error {
tries := 0
for {
err := s.WebhookStore.PermanentDeleteOutgoingByUser(userID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) SaveIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.SaveIncoming(webhook)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) SaveOutgoing(webhook *model.OutgoingWebhook) (*model.OutgoingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.SaveOutgoing(webhook)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) UpdateIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.UpdateIncoming(webhook)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerWebhookStore) UpdateOutgoing(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, error) {
tries := 0
for {
result, err := s.WebhookStore.UpdateOutgoing(hook)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayer) Close() {
s.Store.Close()
}
func (s *RetryLayer) DropAllTables() {
s.Store.DropAllTables()
}
func (s *RetryLayer) LockToMaster() {
s.Store.LockToMaster()
}
func (s *RetryLayer) MarkSystemRanUnitTests() {
s.Store.MarkSystemRanUnitTests()
}
func (s *RetryLayer) SetContext(context context.Context) {
s.Store.SetContext(context)
}
func (s *RetryLayer) TotalMasterDbConnections() int {
return s.Store.TotalMasterDbConnections()
}
func (s *RetryLayer) TotalReadDbConnections() int {
return s.Store.TotalReadDbConnections()
}
func (s *RetryLayer) TotalSearchDbConnections() int {
return s.Store.TotalSearchDbConnections()
}
func (s *RetryLayer) UnlockFromMaster() {
s.Store.UnlockFromMaster()
}
func New(childStore store.Store) *RetryLayer {
newStore := RetryLayer{
Store: childStore,
}
newStore.AuditStore = &RetryLayerAuditStore{AuditStore: childStore.Audit(), Root: &newStore}
newStore.BotStore = &RetryLayerBotStore{BotStore: childStore.Bot(), Root: &newStore}
newStore.ChannelStore = &RetryLayerChannelStore{ChannelStore: childStore.Channel(), Root: &newStore}
newStore.ChannelMemberHistoryStore = &RetryLayerChannelMemberHistoryStore{ChannelMemberHistoryStore: childStore.ChannelMemberHistory(), Root: &newStore}
newStore.ClusterDiscoveryStore = &RetryLayerClusterDiscoveryStore{ClusterDiscoveryStore: childStore.ClusterDiscovery(), Root: &newStore}
newStore.CommandStore = &RetryLayerCommandStore{CommandStore: childStore.Command(), Root: &newStore}
newStore.CommandWebhookStore = &RetryLayerCommandWebhookStore{CommandWebhookStore: childStore.CommandWebhook(), Root: &newStore}
newStore.ComplianceStore = &RetryLayerComplianceStore{ComplianceStore: childStore.Compliance(), Root: &newStore}
newStore.DraftStore = &RetryLayerDraftStore{DraftStore: childStore.Draft(), Root: &newStore}
newStore.EmojiStore = &RetryLayerEmojiStore{EmojiStore: childStore.Emoji(), Root: &newStore}
newStore.FileInfoStore = &RetryLayerFileInfoStore{FileInfoStore: childStore.FileInfo(), Root: &newStore}
newStore.GroupStore = &RetryLayerGroupStore{GroupStore: childStore.Group(), Root: &newStore}
newStore.JobStore = &RetryLayerJobStore{JobStore: childStore.Job(), Root: &newStore}
newStore.LicenseStore = &RetryLayerLicenseStore{LicenseStore: childStore.License(), Root: &newStore}
newStore.LinkMetadataStore = &RetryLayerLinkMetadataStore{LinkMetadataStore: childStore.LinkMetadata(), Root: &newStore}
newStore.NotifyAdminStore = &RetryLayerNotifyAdminStore{NotifyAdminStore: childStore.NotifyAdmin(), Root: &newStore}
newStore.OAuthStore = &RetryLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore}
newStore.PluginStore = &RetryLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
newStore.PostStore = &RetryLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
newStore.PostAcknowledgementStore = &RetryLayerPostAcknowledgementStore{PostAcknowledgementStore: childStore.PostAcknowledgement(), Root: &newStore}
newStore.PostPriorityStore = &RetryLayerPostPriorityStore{PostPriorityStore: childStore.PostPriority(), Root: &newStore}
newStore.PreferenceStore = &RetryLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore}
newStore.ProductNoticesStore = &RetryLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore}
newStore.ReactionStore = &RetryLayerReactionStore{ReactionStore: childStore.Reaction(), Root: &newStore}
newStore.RemoteClusterStore = &RetryLayerRemoteClusterStore{RemoteClusterStore: childStore.RemoteCluster(), Root: &newStore}
newStore.RetentionPolicyStore = &RetryLayerRetentionPolicyStore{RetentionPolicyStore: childStore.RetentionPolicy(), Root: &newStore}
newStore.RoleStore = &RetryLayerRoleStore{RoleStore: childStore.Role(), Root: &newStore}
newStore.SchemeStore = &RetryLayerSchemeStore{SchemeStore: childStore.Scheme(), Root: &newStore}
newStore.SessionStore = &RetryLayerSessionStore{SessionStore: childStore.Session(), Root: &newStore}
newStore.SharedChannelStore = &RetryLayerSharedChannelStore{SharedChannelStore: childStore.SharedChannel(), Root: &newStore}
newStore.StatusStore = &RetryLayerStatusStore{StatusStore: childStore.Status(), Root: &newStore}
newStore.SystemStore = &RetryLayerSystemStore{SystemStore: childStore.System(), Root: &newStore}
newStore.TeamStore = &RetryLayerTeamStore{TeamStore: childStore.Team(), Root: &newStore}
newStore.TermsOfServiceStore = &RetryLayerTermsOfServiceStore{TermsOfServiceStore: childStore.TermsOfService(), Root: &newStore}
newStore.ThreadStore = &RetryLayerThreadStore{ThreadStore: childStore.Thread(), Root: &newStore}
newStore.TokenStore = &RetryLayerTokenStore{TokenStore: childStore.Token(), Root: &newStore}
newStore.TrueUpReviewStore = &RetryLayerTrueUpReviewStore{TrueUpReviewStore: childStore.TrueUpReview(), Root: &newStore}
newStore.UploadSessionStore = &RetryLayerUploadSessionStore{UploadSessionStore: childStore.UploadSession(), Root: &newStore}
newStore.UserStore = &RetryLayerUserStore{UserStore: childStore.User(), Root: &newStore}
newStore.UserAccessTokenStore = &RetryLayerUserAccessTokenStore{UserAccessTokenStore: childStore.UserAccessToken(), Root: &newStore}
newStore.UserTermsOfServiceStore = &RetryLayerUserTermsOfServiceStore{UserTermsOfServiceStore: childStore.UserTermsOfService(), Root: &newStore}
newStore.WebhookStore = &RetryLayerWebhookStore{WebhookStore: childStore.Webhook(), Root: &newStore}
return &newStore
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer
import (
"context"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SearchChannelStore struct {
store.ChannelStore
rootStore *SearchStore
}
func (c *SearchChannelStore) deleteChannelIndex(channel *model.Channel) {
if channel.Type == model.ChannelTypeOpen {
for _, engine := range c.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeleteChannel(channel); err != nil {
mlog.Warn("Encountered error deleting channel", mlog.String("channel_id", channel.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
mlog.Debug("Removed channel from index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("channel_id", channel.Id))
})
}
}
}
}
func (c *SearchChannelStore) indexChannel(channel *model.Channel) {
var userIDs, teamMemberIDs []string
var err error
if channel.Type == model.ChannelTypePrivate {
userIDs, err = c.GetAllChannelMembersById(channel.Id)
if err != nil {
mlog.Warn("Encountered error while indexing channel", mlog.String("channel_id", channel.Id), mlog.Err(err))
return
}
}
teamMemberIDs, err = c.GetTeamMembersForChannel(channel.Id)
if err != nil {
mlog.Warn("Encountered error while indexing channel", mlog.String("channel_id", channel.Id), mlog.Err(err))
return
}
for _, engine := range c.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.IndexChannel(channel, userIDs, teamMemberIDs); err != nil {
mlog.Warn("Encountered error indexing channel", mlog.String("channel_id", channel.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
mlog.Debug("Indexed channel in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("channel_id", channel.Id))
})
}
}
}
func (c *SearchChannelStore) Save(channel *model.Channel, maxChannels int64) (*model.Channel, error) {
newChannel, err := c.ChannelStore.Save(channel, maxChannels)
if err == nil {
c.indexChannel(newChannel)
}
return newChannel, err
}
func (c *SearchChannelStore) Update(channel *model.Channel) (*model.Channel, error) {
updatedChannel, err := c.ChannelStore.Update(channel)
if err == nil {
c.indexChannel(updatedChannel)
}
return updatedChannel, err
}
func (c *SearchChannelStore) UpdateMember(cm *model.ChannelMember) (*model.ChannelMember, error) {
member, err := c.ChannelStore.UpdateMember(cm)
if err == nil {
c.rootStore.indexUserFromID(cm.UserId)
channel, channelErr := c.ChannelStore.Get(member.ChannelId, true)
if channelErr != nil {
mlog.Warn("Encountered error indexing user in channel", mlog.String("channel_id", member.ChannelId), mlog.Err(channelErr))
} else {
c.indexChannel(channel)
c.rootStore.indexUserFromID(channel.CreatorId)
}
}
return member, err
}
func (c *SearchChannelStore) SaveMember(cm *model.ChannelMember) (*model.ChannelMember, error) {
member, err := c.ChannelStore.SaveMember(cm)
if err == nil {
c.rootStore.indexUserFromID(cm.UserId)
channel, channelErr := c.ChannelStore.Get(member.ChannelId, true)
if channelErr != nil {
mlog.Warn("Encountered error indexing user in channel", mlog.String("channel_id", member.ChannelId), mlog.Err(channelErr))
} else {
c.indexChannel(channel)
c.rootStore.indexUserFromID(channel.CreatorId)
}
}
return member, err
}
func (c *SearchChannelStore) RemoveMember(channelID, userIdToRemove string) error {
err := c.ChannelStore.RemoveMember(channelID, userIdToRemove)
if err == nil {
c.rootStore.indexUserFromID(userIdToRemove)
}
channel, err := c.ChannelStore.Get(channelID, true)
if err == nil {
c.indexChannel(channel)
}
return err
}
func (c *SearchChannelStore) RemoveMembers(channelID string, userIds []string) error {
if err := c.ChannelStore.RemoveMembers(channelID, userIds); err != nil {
return err
}
channel, err := c.ChannelStore.Get(channelID, true)
if err == nil {
c.indexChannel(channel)
}
for _, uid := range userIds {
c.rootStore.indexUserFromID(uid)
}
return nil
}
func (c *SearchChannelStore) CreateDirectChannel(user *model.User, otherUser *model.User, channelOptions ...model.ChannelOption) (*model.Channel, error) {
channel, err := c.ChannelStore.CreateDirectChannel(user, otherUser, channelOptions...)
if err == nil {
c.rootStore.indexUserFromID(user.Id)
c.rootStore.indexUserFromID(otherUser.Id)
c.indexChannel(channel)
}
return channel, err
}
func (c *SearchChannelStore) SaveDirectChannel(directchannel *model.Channel, member1 *model.ChannelMember, member2 *model.ChannelMember) (*model.Channel, error) {
channel, err := c.ChannelStore.SaveDirectChannel(directchannel, member1, member2)
if err == nil {
c.rootStore.indexUserFromID(member1.UserId)
c.rootStore.indexUserFromID(member2.UserId)
c.indexChannel(channel)
}
return channel, err
}
func (c *SearchChannelStore) Autocomplete(userID, term string, includeDeleted, isGuest bool) (model.ChannelListWithTeamData, error) {
var channelList model.ChannelListWithTeamData
var err error
allFailed := true
for _, engine := range c.rootStore.searchEngine.GetActiveEngines() {
if engine.IsAutocompletionEnabled() {
channelList, err = c.searchAutocompleteChannelsAllTeams(engine, userID, term, includeDeleted, isGuest)
if err != nil {
mlog.Warn("Encountered error on AutocompleteChannels through SearchEngine. Falling back to default autocompletion.", mlog.String("search_engine", engine.GetName()), mlog.Err(err))
continue
}
allFailed = false
mlog.Debug("Using the first available search engine", mlog.String("search_engine", engine.GetName()))
break
}
}
if allFailed {
mlog.Debug("Using database search because no other search engine is available")
channelList, err = c.ChannelStore.Autocomplete(userID, term, includeDeleted, isGuest)
if err != nil {
return nil, errors.Wrap(err, "Failed to autocomplete channels in team")
}
}
if err != nil {
return channelList, err
}
return channelList, nil
}
func (c *SearchChannelStore) AutocompleteInTeam(teamID, userID, term string, includeDeleted, isGuest bool) (model.ChannelList, error) {
var channelList model.ChannelList
var err error
allFailed := true
for _, engine := range c.rootStore.searchEngine.GetActiveEngines() {
if engine.IsAutocompletionEnabled() {
channelList, err = c.searchAutocompleteChannels(engine, teamID, userID, term, includeDeleted, isGuest)
if err != nil {
mlog.Warn("Encountered error on AutocompleteChannels through SearchEngine. Falling back to default autocompletion.", mlog.String("search_engine", engine.GetName()), mlog.Err(err))
continue
}
allFailed = false
mlog.Debug("Using the first available search engine", mlog.String("search_engine", engine.GetName()))
break
}
}
if allFailed {
mlog.Debug("Using database search because no other search engine is available")
channelList, err = c.ChannelStore.AutocompleteInTeam(teamID, userID, term, includeDeleted, isGuest)
if err != nil {
return nil, errors.Wrap(err, "Failed to autocomplete channels in team")
}
}
if err != nil {
return channelList, err
}
return channelList, nil
}
func (c *SearchChannelStore) searchAutocompleteChannels(engine searchengine.SearchEngineInterface, teamId, userID, term string, includeDeleted, isGuest bool) (model.ChannelList, error) {
channelIds, err := engine.SearchChannels(teamId, userID, term, isGuest)
if err != nil {
return nil, err
}
channelList := model.ChannelList{}
var nErr error
if len(channelIds) > 0 {
channelList, nErr = c.ChannelStore.GetChannelsByIds(channelIds, includeDeleted)
if nErr != nil {
return nil, errors.Wrap(nErr, "Failed to get channels by ids")
}
}
return channelList, nil
}
func (c *SearchChannelStore) searchAutocompleteChannelsAllTeams(engine searchengine.SearchEngineInterface, userID, term string, includeDeleted, isGuest bool) (model.ChannelListWithTeamData, error) {
channelIds, err := engine.SearchChannels("", userID, term, isGuest)
if err != nil {
return nil, err
}
channelList := model.ChannelListWithTeamData{}
var nErr error
if len(channelIds) > 0 {
channelList, nErr = c.ChannelStore.GetChannelsWithTeamDataByIds(channelIds, includeDeleted)
if nErr != nil {
return nil, errors.Wrap(nErr, "Failed to get channels by ids")
}
}
return channelList, nil
}
func (c *SearchChannelStore) PermanentDeleteMembersByUser(userId string) error {
channels, errGetChannels := c.ChannelStore.GetChannelsByUser(userId, false, 0, -1, "")
if errGetChannels != nil {
mlog.Warn("Encountered error indexing channel after removing user", mlog.String("user_id", userId), mlog.Err(errGetChannels))
}
err := c.ChannelStore.PermanentDeleteMembersByUser(userId)
if err == nil {
c.rootStore.indexUserFromID(userId)
if errGetChannels == nil {
for _, ch := range channels {
c.indexChannel(ch)
}
}
}
return err
}
func (c *SearchChannelStore) RemoveAllDeactivatedMembers(channelId string) error {
profiles, errProfiles := c.rootStore.User().GetAllProfilesInChannel(context.Background(), channelId, true)
if errProfiles != nil {
mlog.Warn("Encountered error indexing users for channel", mlog.String("channel_id", channelId), mlog.Err(errProfiles))
}
err := c.ChannelStore.RemoveAllDeactivatedMembers(channelId)
if err == nil && errProfiles == nil {
for _, user := range profiles {
if user.DeleteAt != 0 {
c.rootStore.indexUser(user)
}
}
}
return err
}
func (c *SearchChannelStore) PermanentDeleteMembersByChannel(channelId string) error {
profiles, errProfiles := c.rootStore.User().GetAllProfilesInChannel(context.Background(), channelId, true)
if errProfiles != nil {
mlog.Warn("Encountered error indexing users for channel", mlog.String("channel_id", channelId), mlog.Err(errProfiles))
}
err := c.ChannelStore.PermanentDeleteMembersByChannel(channelId)
if err == nil && errProfiles == nil {
for _, user := range profiles {
c.rootStore.indexUser(user)
}
}
return err
}
func (c *SearchChannelStore) PermanentDelete(channelId string) error {
channel, channelErr := c.ChannelStore.Get(channelId, true)
if channelErr != nil {
mlog.Warn("Encountered error deleting channel", mlog.String("channel_id", channelId), mlog.Err(channelErr))
}
err := c.ChannelStore.PermanentDelete(channelId)
if err == nil && channelErr == nil {
c.deleteChannelIndex(channel)
}
return err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SearchFileInfoStore struct {
store.FileInfoStore
rootStore *SearchStore
}
func (s SearchFileInfoStore) indexFile(file *model.FileInfo) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if file.PostId == "" {
return
}
post, postErr := s.rootStore.Post().GetSingle(file.PostId, false)
if postErr != nil {
mlog.Error("Couldn't get post for file for SearchEngine indexing.", mlog.String("post_id", file.PostId), mlog.String("search_engine", engineCopy.GetName()), mlog.String("file_info_id", file.Id), mlog.Err(postErr))
return
}
if err := engineCopy.IndexFile(file, post.ChannelId); err != nil {
mlog.Error("Encountered error indexing file", mlog.String("file_info_id", file.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
})
}
}
}
func (s SearchFileInfoStore) deleteFileIndex(fileID string) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeleteFile(fileID); err != nil {
mlog.Error("Encountered error deleting file", mlog.String("file_info_id", fileID), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
})
}
}
}
func (s SearchFileInfoStore) deleteFileIndexForUser(userID string) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeleteUserFiles(userID); err != nil {
mlog.Error("Encountered error deleting files for user", mlog.String("user_id", userID), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
mlog.Debug("Removed user's files from the index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("user_id", userID))
})
}
}
}
func (s SearchFileInfoStore) deleteFileIndexForPost(postID string) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeletePostFiles(postID); err != nil {
mlog.Error("Encountered error deleting files for post", mlog.String("post_id", postID), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
mlog.Debug("Removed post's files from the index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("post_id", postID))
})
}
}
}
func (s SearchFileInfoStore) deleteFileIndexBatch(endTime, limit int64) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeleteFilesBatch(endTime, limit); err != nil {
mlog.Error("Encountered error deleting a batch of files", mlog.Int64("limit", limit), mlog.Int64("end_time", endTime), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
mlog.Debug("Removed batch of files from the index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.Int64("end_time", endTime), mlog.Int64("limit", limit))
})
}
}
}
func (s SearchFileInfoStore) Save(info *model.FileInfo) (*model.FileInfo, error) {
nfile, err := s.FileInfoStore.Save(info)
if err == nil {
s.indexFile(nfile)
}
return nfile, err
}
func (s SearchFileInfoStore) SetContent(fileID, content string) error {
err := s.FileInfoStore.SetContent(fileID, content)
if err == nil {
nfile, err2 := s.FileInfoStore.GetFromMaster(fileID)
if err2 == nil {
nfile.Content = content
s.indexFile(nfile)
}
}
return err
}
func (s SearchFileInfoStore) AttachToPost(fileId, postId, channelId, creatorId string) error {
err := s.FileInfoStore.AttachToPost(fileId, postId, channelId, creatorId)
if err == nil {
nFileInfo, err2 := s.FileInfoStore.GetFromMaster(fileId)
if err2 == nil {
s.indexFile(nFileInfo)
}
}
return err
}
func (s SearchFileInfoStore) DeleteForPost(postId string) (string, error) {
result, err := s.FileInfoStore.DeleteForPost(postId)
if err == nil {
s.deleteFileIndexForPost(postId)
}
return result, err
}
func (s SearchFileInfoStore) PermanentDelete(fileId string) error {
err := s.FileInfoStore.PermanentDelete(fileId)
if err == nil {
s.deleteFileIndex(fileId)
}
return err
}
func (s SearchFileInfoStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
result, err := s.FileInfoStore.PermanentDeleteBatch(endTime, limit)
if err == nil {
s.deleteFileIndexBatch(endTime, limit)
}
return result, err
}
func (s SearchFileInfoStore) PermanentDeleteByUser(userId string) (int64, error) {
result, err := s.FileInfoStore.PermanentDeleteByUser(userId)
if err == nil {
s.deleteFileIndexForUser(userId)
}
return result, err
}
func (s SearchFileInfoStore) Search(paramsList []*model.SearchParams, userId, teamId string, page, perPage int) (*model.FileInfoList, error) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsSearchEnabled() {
userChannels, nErr := s.rootStore.Channel().GetChannels(teamId, userId, &model.ChannelSearchOpts{
IncludeDeleted: paramsList[0].IncludeDeletedChannels,
LastDeleteAt: 0,
})
if nErr != nil {
return nil, nErr
}
fileIds, appErr := engine.SearchFiles(userChannels, paramsList, page, perPage)
if appErr != nil {
mlog.Error("Encountered error on Search.", mlog.String("search_engine", engine.GetName()), mlog.Err(appErr))
continue
}
// Get the files
filesList := model.NewFileInfoList()
if len(fileIds) > 0 {
files, nErr := s.FileInfoStore.GetByIds(fileIds)
if nErr != nil {
return nil, nErr
}
for _, f := range files {
filesList.AddFileInfo(f)
filesList.AddOrder(f.Id)
}
}
return filesList, nil
}
}
if *s.rootStore.getConfig().SqlSettings.DisableDatabaseSearch {
return model.NewFileInfoList(), nil
}
return s.FileInfoStore.Search(paramsList, userId, teamId, page, perPage)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer
import (
"context"
"sync/atomic"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SearchStore struct {
store.Store
searchEngine *searchengine.Broker
user *SearchUserStore
team *SearchTeamStore
channel *SearchChannelStore
post *SearchPostStore
fileInfo *SearchFileInfoStore
configValue atomic.Value
}
func NewSearchLayer(baseStore store.Store, searchEngine *searchengine.Broker, cfg *model.Config) *SearchStore {
searchStore := &SearchStore{
Store: baseStore,
searchEngine: searchEngine,
}
searchStore.configValue.Store(cfg)
searchStore.channel = &SearchChannelStore{ChannelStore: baseStore.Channel(), rootStore: searchStore}
searchStore.post = &SearchPostStore{PostStore: baseStore.Post(), rootStore: searchStore}
searchStore.team = &SearchTeamStore{TeamStore: baseStore.Team(), rootStore: searchStore}
searchStore.user = &SearchUserStore{UserStore: baseStore.User(), rootStore: searchStore}
searchStore.fileInfo = &SearchFileInfoStore{FileInfoStore: baseStore.FileInfo(), rootStore: searchStore}
return searchStore
}
func (s *SearchStore) UpdateConfig(cfg *model.Config) {
s.configValue.Store(cfg)
}
func (s *SearchStore) getConfig() *model.Config {
return s.configValue.Load().(*model.Config)
}
func (s *SearchStore) Channel() store.ChannelStore {
return s.channel
}
func (s *SearchStore) Post() store.PostStore {
return s.post
}
func (s *SearchStore) FileInfo() store.FileInfoStore {
return s.fileInfo
}
func (s *SearchStore) Team() store.TeamStore {
return s.team
}
func (s *SearchStore) User() store.UserStore {
return s.user
}
func (s *SearchStore) indexUserFromID(userId string) {
user, err := s.User().Get(context.Background(), userId)
if err != nil {
return
}
s.indexUser(user)
}
func (s *SearchStore) indexUser(user *model.User) {
for _, engine := range s.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
userTeams, nErr := s.Team().GetTeamsByUserId(user.Id)
if nErr != nil {
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(nErr))
return
}
userTeamsIds := []string{}
for _, team := range userTeams {
userTeamsIds = append(userTeamsIds, team.Id)
}
userChannelMembers, err := s.Channel().GetAllChannelMembersForUser(user.Id, false, true)
if err != nil {
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
userChannelsIds := []string{}
for channelId := range userChannelMembers {
userChannelsIds = append(userChannelsIds, channelId)
}
if err := engineCopy.IndexUser(user, userTeamsIds, userChannelsIds); err != nil {
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
mlog.Debug("Indexed user in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("user_id", user.Id))
})
}
}
}
// Runs an indexing function synchronously or asynchronously depending on the engine
func runIndexFn(engine searchengine.SearchEngineInterface, indexFn func(searchengine.SearchEngineInterface)) {
if engine.IsIndexingSync() {
indexFn(engine)
if err := engine.RefreshIndexes(); err != nil {
mlog.Error("Encountered error refresh the indexes", mlog.Err(err))
}
} else {
go (func(engineCopy searchengine.SearchEngineInterface) {
indexFn(engineCopy)
})(engine)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer
import (
"context"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SearchPostStore struct {
store.PostStore
rootStore *SearchStore
}
func (s SearchPostStore) indexPost(post *model.Post) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
channel, chanErr := s.rootStore.Channel().Get(post.ChannelId, true)
if chanErr != nil {
mlog.Error("Couldn't get channel for post for SearchEngine indexing.", mlog.String("channel_id", post.ChannelId), mlog.String("search_engine", engineCopy.GetName()), mlog.String("post_id", post.Id), mlog.Err(chanErr))
return
}
if err := engineCopy.IndexPost(post, channel.TeamId); err != nil {
mlog.Warn("Encountered error indexing post", mlog.String("post_id", post.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
})
}
}
}
func (s SearchPostStore) deletePostIndex(post *model.Post) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeletePost(post); err != nil {
mlog.Warn("Encountered error deleting post", mlog.String("post_id", post.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
})
}
}
}
func (s SearchPostStore) deleteChannelPostsIndex(channelID string) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeleteChannelPosts(channelID); err != nil {
mlog.Warn("Encountered error deleting channel posts", mlog.String("channel_id", channelID), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
mlog.Debug("Removed all channel posts from the index in search engine", mlog.String("channel_id", channelID), mlog.String("search_engine", engineCopy.GetName()))
})
}
}
}
func (s SearchPostStore) deleteUserPostsIndex(userID string) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeleteUserPosts(userID); err != nil {
mlog.Warn("Encountered error deleting user posts", mlog.String("user_id", userID), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
mlog.Debug("Removed all user posts from the index in search engine", mlog.String("user_id", userID), mlog.String("search_engine", engineCopy.GetName()))
})
}
}
}
func (s SearchPostStore) Update(newPost, oldPost *model.Post) (*model.Post, error) {
post, err := s.PostStore.Update(newPost, oldPost)
if err == nil {
s.indexPost(post)
}
return post, err
}
func (s *SearchPostStore) Overwrite(post *model.Post) (*model.Post, error) {
post, err := s.PostStore.Overwrite(post)
if err == nil {
s.indexPost(post)
}
return post, err
}
func (s SearchPostStore) Save(post *model.Post) (*model.Post, error) {
npost, err := s.PostStore.Save(post)
if err == nil {
s.indexPost(npost)
}
return npost, err
}
func (s SearchPostStore) Delete(postId string, date int64, deletedByID string) error {
err := s.PostStore.Delete(postId, date, deletedByID)
if err == nil {
opts := model.GetPostsOptions{
SkipFetchThreads: true,
}
postList, err2 := s.PostStore.Get(context.Background(), postId, opts, "", map[string]bool{})
if postList != nil && len(postList.Order) > 0 {
if err2 != nil {
s.deletePostIndex(postList.Posts[postList.Order[0]])
}
}
}
return err
}
func (s SearchPostStore) PermanentDeleteByUser(userID string) error {
err := s.PostStore.PermanentDeleteByUser(userID)
if err == nil {
s.deleteUserPostsIndex(userID)
}
return err
}
func (s SearchPostStore) PermanentDeleteByChannel(channelID string) error {
err := s.PostStore.PermanentDeleteByChannel(channelID)
if err == nil {
s.deleteChannelPostsIndex(channelID)
}
return err
}
func (s SearchPostStore) searchPostsForUserByEngine(engine searchengine.SearchEngineInterface, paramsList []*model.SearchParams, userId, teamId string, page, perPage int) (*model.PostSearchResults, error) {
if err := model.IsSearchParamsListValid(paramsList); err != nil {
return nil, err
}
// We only allow the user to search in channels they are a member of.
userChannels, err2 := s.rootStore.Channel().GetChannels(teamId, userId,
&model.ChannelSearchOpts{
IncludeDeleted: paramsList[0].IncludeDeletedChannels,
LastDeleteAt: 0,
})
if err2 != nil {
return nil, errors.Wrap(err2, "error getting channel for user")
}
postIds, matches, err := engine.SearchPosts(userChannels, paramsList, page, perPage)
if err != nil {
return nil, err
}
// Get the posts
postList := model.NewPostList()
if len(postIds) > 0 {
posts, err := s.PostStore.GetPostsByIds(postIds)
if err != nil {
return nil, err
}
for _, p := range posts {
if p.DeleteAt == 0 {
postList.AddPost(p)
postList.AddOrder(p.Id)
}
}
}
return model.MakePostSearchResults(postList, matches), nil
}
func (s SearchPostStore) SearchPostsForUser(paramsList []*model.SearchParams, userId, teamId string, page, perPage int) (*model.PostSearchResults, error) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsSearchEnabled() {
results, err := s.searchPostsForUserByEngine(engine, paramsList, userId, teamId, page, perPage)
if err != nil {
mlog.Warn("Encountered error on SearchPostsInTeamForUser.", mlog.String("search_engine", engine.GetName()), mlog.Err(err))
continue
}
return results, err
}
}
if *s.rootStore.getConfig().SqlSettings.DisableDatabaseSearch {
return &model.PostSearchResults{PostList: model.NewPostList(), Matches: model.PostSearchMatches{}}, nil
}
return s.PostStore.SearchPostsForUser(paramsList, userId, teamId, page, perPage)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer
import (
model "github.com/mattermost/mattermost-server/v6/model"
store "github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SearchTeamStore struct {
store.TeamStore
rootStore *SearchStore
}
func (s SearchTeamStore) SaveMember(teamMember *model.TeamMember, maxUsersPerTeam int) (*model.TeamMember, error) {
member, err := s.TeamStore.SaveMember(teamMember, maxUsersPerTeam)
if err == nil {
s.rootStore.indexUserFromID(member.UserId)
}
return member, err
}
func (s SearchTeamStore) UpdateMember(teamMember *model.TeamMember) (*model.TeamMember, error) {
member, err := s.TeamStore.UpdateMember(teamMember)
if err == nil {
s.rootStore.indexUserFromID(member.UserId)
}
return member, err
}
func (s SearchTeamStore) RemoveMember(teamId string, userId string) error {
err := s.TeamStore.RemoveMember(teamId, userId)
if err == nil {
s.rootStore.indexUserFromID(userId)
}
return err
}
func (s SearchTeamStore) RemoveAllMembersByUser(userId string) error {
err := s.TeamStore.RemoveAllMembersByUser(userId)
if err == nil {
s.rootStore.indexUserFromID(userId)
}
return err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer
import (
"context"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SearchUserStore struct {
store.UserStore
rootStore *SearchStore
}
func (s *SearchUserStore) deleteUserIndex(user *model.User) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsIndexingEnabled() {
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
if err := engineCopy.DeleteUser(user); err != nil {
mlog.Error("Encountered error deleting user", mlog.String("user_id", user.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
return
}
mlog.Debug("Removed user from the index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("user_id", user.Id))
})
}
}
}
func (s *SearchUserStore) Search(teamId, term string, options *model.UserSearchOptions) ([]*model.User, error) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsSearchEnabled() {
listOfAllowedChannels, nErr := s.getListOfAllowedChannels(teamId, "", options.ViewRestrictions)
if nErr != nil {
mlog.Warn("Encountered error on Search.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
continue
}
if listOfAllowedChannels != nil && len(listOfAllowedChannels) == 0 {
return []*model.User{}, nil
}
sanitizedTerm := sanitizeSearchTerm(term)
usersIds, err := engine.SearchUsersInTeam(teamId, listOfAllowedChannels, sanitizedTerm, options)
if err != nil {
mlog.Warn("Encountered error on Search", mlog.String("search_engine", engine.GetName()), mlog.Err(err))
continue
}
users, nErr := s.UserStore.GetProfileByIds(context.Background(), usersIds, nil, false)
if nErr != nil {
mlog.Warn("Encountered error on Search", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
continue
}
mlog.Debug("Using the first available search engine", mlog.String("search_engine", engine.GetName()))
return users, nil
}
}
mlog.Debug("Using database search because no other search engine is available")
return s.UserStore.Search(teamId, term, options)
}
func (s *SearchUserStore) Update(user *model.User, trustedUpdateData bool) (*model.UserUpdate, error) {
userUpdate, err := s.UserStore.Update(user, trustedUpdateData)
if err == nil {
s.rootStore.indexUser(userUpdate.New)
}
return userUpdate, err
}
func (s *SearchUserStore) Save(user *model.User) (*model.User, error) {
nuser, err := s.UserStore.Save(user)
if err == nil {
s.rootStore.indexUser(nuser)
}
return nuser, err
}
func (s *SearchUserStore) PermanentDelete(userId string) error {
user, userErr := s.UserStore.Get(context.Background(), userId)
if userErr != nil {
mlog.Warn("Encountered error deleting user", mlog.String("user_id", userId), mlog.Err(userErr))
}
err := s.UserStore.PermanentDelete(userId)
if err == nil && userErr == nil {
s.deleteUserIndex(user)
}
return err
}
func (s *SearchUserStore) autocompleteUsersInChannelByEngine(engine searchengine.SearchEngineInterface, teamId, channelId, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error) {
var err *model.AppError
uchanIds := []string{}
nuchanIds := []string{}
sanitizedTerm := sanitizeSearchTerm(term)
if channelId != "" && options.ListOfAllowedChannels != nil && !strings.Contains(strings.Join(options.ListOfAllowedChannels, "."), channelId) {
nuchanIds, err = engine.SearchUsersInTeam(teamId, options.ListOfAllowedChannels, sanitizedTerm, options)
} else {
uchanIds, nuchanIds, err = engine.SearchUsersInChannel(teamId, channelId, options.ListOfAllowedChannels, sanitizedTerm, options)
}
if err != nil {
return nil, err
}
uchan := make(chan store.StoreResult, 1)
go func() {
users, nErr := s.UserStore.GetProfileByIds(context.Background(), uchanIds, nil, false)
uchan <- store.StoreResult{Data: users, NErr: nErr}
close(uchan)
}()
nuchan := make(chan store.StoreResult, 1)
go func() {
users, nErr := s.UserStore.GetProfileByIds(context.Background(), nuchanIds, nil, false)
nuchan <- store.StoreResult{Data: users, NErr: nErr}
close(nuchan)
}()
autocomplete := &model.UserAutocompleteInChannel{}
result := <-uchan
if result.NErr != nil {
return nil, errors.Wrap(result.NErr, "failed to get user profiles by ids")
}
inUsers := result.Data.([]*model.User)
autocomplete.InChannel = inUsers
result = <-nuchan
if result.NErr != nil {
return nil, errors.Wrap(result.NErr, "failed to get user profiles by ids")
}
outUsers := result.Data.([]*model.User)
autocomplete.OutOfChannel = outUsers
return autocomplete, nil
}
// getListOfAllowedChannels return the list of allowed channels to search user based on the
//
// next scenarios:
// - If there isn't view restrictions (team or channel) and no team id to filter them, then all
// channels are allowed (nil return)
// - If we receive a team Id and either we don't have view restrictions or the provided team id is included in the
// list of restricted teams, then we return all the team channels
// - If we don't receive team id or the provided team id is not in the list of allowed teams to search of and we
// don't have channel restrictions then we return an empty result because we cannot get channels
// - If we receive channels restrictions we get:
// - If we don't have team id, we get those restricted channels (guest accounts and quick search)
// - If we have a team id then we only return those restricted channels that belongs to that team
func (s *SearchUserStore) getListOfAllowedChannels(teamId, channelId string, viewRestrictions *model.ViewUsersRestrictions) ([]string, error) {
var listOfAllowedChannels []string
if viewRestrictions == nil && teamId == "" {
// nil return without error means all channels are allowed
return nil, nil
}
if teamId != "" && (viewRestrictions == nil || strings.Contains(strings.Join(viewRestrictions.Teams, "."), teamId)) {
channels, err := s.rootStore.Channel().GetTeamChannels(teamId)
if err != nil {
return nil, errors.Wrap(err, "failed to get team channels")
}
for _, channel := range channels {
listOfAllowedChannels = append(listOfAllowedChannels, channel.Id)
}
if channelId != "" {
ch, err := s.rootStore.Channel().Get(channelId, true)
if err != nil {
return nil, errors.Wrapf(err, "failed to get channel with id: %s", channelId)
}
// Check if DM/GM channel, and add to the list.
// This is because GetTeamChannels does not return DM/GM channels.
// And since the channelId is passed from the API layer, it is already
// auth checked to confirm that the user has permission.
if ch.IsGroupOrDirect() {
listOfAllowedChannels = append(listOfAllowedChannels, channelId)
}
}
return listOfAllowedChannels, nil
}
if len(viewRestrictions.Channels) > 0 {
channels, err := s.rootStore.Channel().GetChannelsByIds(viewRestrictions.Channels, false)
if err != nil {
return nil, errors.Wrap(err, "failed to get channels by ids")
}
for _, c := range channels {
if teamId == "" || (teamId != "" && c.TeamId == teamId) {
listOfAllowedChannels = append(listOfAllowedChannels, c.Id)
}
}
return listOfAllowedChannels, nil
}
return []string{}, nil
}
func (s *SearchUserStore) AutocompleteUsersInChannel(teamId, channelId, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsAutocompletionEnabled() {
listOfAllowedChannels, nErr := s.getListOfAllowedChannels(teamId, channelId, options.ViewRestrictions)
if nErr != nil {
mlog.Warn("Encountered error on AutocompleteUsersInChannel.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
continue
}
if listOfAllowedChannels != nil && len(listOfAllowedChannels) == 0 {
return &model.UserAutocompleteInChannel{}, nil
}
options.ListOfAllowedChannels = listOfAllowedChannels
autocomplete, nErr := s.autocompleteUsersInChannelByEngine(engine, teamId, channelId, term, options)
if nErr != nil {
mlog.Warn("Encountered error on AutocompleteUsersInChannel.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
continue
}
mlog.Debug("Using the first available search engine", mlog.String("search_engine", engine.GetName()))
return autocomplete, nil
}
}
mlog.Debug("Using database search because no other search engine is available")
return s.UserStore.AutocompleteUsersInChannel(teamId, channelId, term, options)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchlayer
import (
"strings"
)
func sanitizeSearchTerm(term string) string {
return strings.TrimLeft(term, "@")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchtest
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
var searchChannelStoreTests = []searchTest{
{
Name: "Should be able to autocomplete a channel by name",
Fn: testAutocompleteChannelByName,
Tags: []string{EngineMySql, EngineElasticSearch, EngineBleve},
},
{
Name: "Should be able to autocomplete a channel by name (Postgres)",
Fn: testAutocompleteChannelByNamePostgres,
Tags: []string{EnginePostgres},
},
{
Name: "Should be able to autocomplete a channel by display name",
Fn: testAutocompleteChannelByDisplayName,
Tags: []string{EngineAll},
},
{
Name: "Should be able to autocomplete a channel by a part of its name when has parts splitted by - character",
Fn: testAutocompleteChannelByNameSplittedWithDashChar,
Tags: []string{EngineMySql, EngineElasticSearch, EngineBleve},
},
{
Name: "Should be able to autocomplete a channel by a part of its name when has parts splitted by - character (Postgres)",
Fn: testAutocompleteChannelByNameSplittedWithDashCharPostgres,
Tags: []string{EnginePostgres},
},
{
Name: "Should be able to autocomplete a channel by a part of its name when has parts splitted by _ character",
Fn: testAutocompleteChannelByNameSplittedWithUnderscoreChar,
Tags: []string{EngineMySql, EngineElasticSearch, EngineBleve},
},
{
Name: "Should be able to autocomplete a channel by a part of its display name when has parts splitted by whitespace character",
Fn: testAutocompleteChannelByDisplayNameSplittedByWhitespaces,
Tags: []string{EngineMySql, EngineElasticSearch, EngineBleve},
},
{
Name: "Should be able to autocomplete retrieving all channels if the term is empty",
Fn: testAutocompleteAllChannelsIfTermIsEmpty,
Tags: []string{EngineAll},
},
{
Name: "Should be able to autocomplete channels in a case insensitive manner",
Fn: testSearchChannelsInCaseInsensitiveManner,
Tags: []string{EngineMySql, EngineElasticSearch, EngineBleve},
},
{
Name: "Should be able to autocomplete channels in a case insensitive manner (Postgres)",
Fn: testSearchChannelsInCaseInsensitiveMannerPostgres,
Tags: []string{EnginePostgres},
},
{
Name: "Should support to autocomplete having a hyphen as the last character",
Fn: testSearchShouldSupportHavingHyphenAsLastCharacter,
Tags: []string{EngineAll},
},
{
Name: "Should support to autocomplete with archived channels",
Fn: testSearchShouldSupportAutocompleteWithArchivedChannels,
Tags: []string{EngineAll},
},
}
func TestSearchChannelStore(t *testing.T, s store.Store, testEngine *SearchTestEngine) {
th := &SearchTestHelper{
Store: s,
}
err := th.SetupBasicFixtures()
require.NoError(t, err)
defer th.CleanFixtures()
runTestSearch(t, testEngine, searchChannelStoreTests, th)
}
func testAutocompleteChannelByName(t *testing.T, th *SearchTestHelper) {
alternate, err := th.createChannel(th.Team.Id, "channel-alternate", "Channel Alternate", "Channel Alternate", model.ChannelTypeOpen, th.User, false)
require.NoError(t, err)
defer th.deleteChannel(alternate)
private, err := th.createChannel(th.Team.Id, "channel-altprivate", "Channel AltPrivate", "Channel Private", model.ChannelTypePrivate, th.User, false)
require.NoError(t, err)
defer th.deleteChannel(private)
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "channel-a", false, false)
require.NoError(t, err)
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, alternate.Id, private.Id}, res)
res2, err := th.Store.Channel().Autocomplete(th.User.Id, "channel-a", false, false)
require.NoError(t, err)
th.checkChannelIdsMatchWithTeamData(t, []string{th.ChannelBasic.Id, alternate.Id, private.Id, th.ChannelAnotherTeam.Id}, res2)
}
func testAutocompleteChannelByNamePostgres(t *testing.T, th *SearchTestHelper) {
alternate, err := th.createChannel(th.Team.Id, "channel-alternate", "Channel Alternate", "Channel Alternate", model.ChannelTypeOpen, th.User, false)
require.NoError(t, err)
defer th.deleteChannel(alternate)
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "channel-a", false, false)
require.NoError(t, err)
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, th.ChannelPrivate.Id, alternate.Id}, res)
}
func testAutocompleteChannelByDisplayName(t *testing.T, th *SearchTestHelper) {
alternate, err := th.createChannel(th.Team.Id, "channel-alternate", "ChannelAlternate", "", model.ChannelTypeOpen, th.User, false)
require.NoError(t, err)
defer th.deleteChannel(alternate)
private, err := th.createChannel(th.Team.Id, "channel-altprivate", "ChannelAltPrivate", "Channel Private", model.ChannelTypePrivate, th.User, false)
require.NoError(t, err)
defer th.deleteChannel(private)
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "ChannelA", false, false)
require.NoError(t, err)
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, alternate.Id, private.Id}, res)
res2, err := th.Store.Channel().Autocomplete(th.User.Id, "ChannelA", false, false)
require.NoError(t, err)
th.checkChannelIdsMatchWithTeamData(t, []string{th.ChannelBasic.Id, alternate.Id, private.Id, th.ChannelAnotherTeam.Id}, res2)
}
func testAutocompleteChannelByNameSplittedWithDashChar(t *testing.T, th *SearchTestHelper) {
alternate, err := th.createChannel(th.Team.Id, "channel-alternate", "ChannelAlternate", "", model.ChannelTypeOpen, th.User, false)
require.NoError(t, err)
defer th.deleteChannel(alternate)
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "channel-a", false, false)
require.NoError(t, err)
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, alternate.Id}, res)
}
func testAutocompleteChannelByNameSplittedWithDashCharPostgres(t *testing.T, th *SearchTestHelper) {
alternate, err := th.createChannel(th.Team.Id, "channel-alternate", "ChannelAlternate", "", model.ChannelTypeOpen, th.User, false)
require.NoError(t, err)
defer th.deleteChannel(alternate)
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "channel-a", false, false)
require.NoError(t, err)
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, th.ChannelPrivate.Id, alternate.Id}, res)
}
func testAutocompleteChannelByNameSplittedWithUnderscoreChar(t *testing.T, th *SearchTestHelper) {
alternate, err := th.createChannel(th.Team.Id, "channel_alternate", "ChannelAlternate", "", model.ChannelTypeOpen, th.User, false)
require.NoError(t, err)
defer th.deleteChannel(alternate)
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "channel_a", false, false)
require.NoError(t, err)
th.checkChannelIdsMatch(t, []string{alternate.Id}, res)
res2, err := th.Store.Channel().Autocomplete(th.User.Id, "channel_a", false, false)
require.NoError(t, err)
th.checkChannelIdsMatchWithTeamData(t, []string{alternate.Id}, res2)
}
func testAutocompleteChannelByDisplayNameSplittedByWhitespaces(t *testing.T, th *SearchTestHelper) {
alternate, err := th.createChannel(th.Team.Id, "channel-alternate", "Channel Alternate", "", model.ChannelTypeOpen, th.User, false)
require.NoError(t, err)
defer th.deleteChannel(alternate)
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "Channel A", false, false)
require.NoError(t, err)
th.checkChannelIdsMatch(t, []string{alternate.Id}, res)
}
func testAutocompleteAllChannelsIfTermIsEmpty(t *testing.T, th *SearchTestHelper) {
alternate, err := th.createChannel(th.Team.Id, "channel-alternate", "Channel Alternate", "", model.ChannelTypeOpen, th.User, false)
require.NoError(t, err)
other, err := th.createChannel(th.Team.Id, "other-channel", "Other Channel", "", model.ChannelTypeOpen, th.User, false)
require.NoError(t, err)
defer th.deleteChannel(alternate)
defer th.deleteChannel(other)
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "", false, false)
require.NoError(t, err)
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, th.ChannelPrivate.Id, alternate.Id, other.Id}, res)
}
func testSearchChannelsInCaseInsensitiveManner(t *testing.T, th *SearchTestHelper) {
alternate, err := th.createChannel(th.Team.Id, "channel-alternate", "ChannelAlternate", "", model.ChannelTypeOpen, th.User, false)
require.NoError(t, err)
defer th.deleteChannel(alternate)
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "channela", false, false)
require.NoError(t, err)
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, alternate.Id}, res)
res, err = th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "ChAnNeL-a", false, false)
require.NoError(t, err)
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, alternate.Id}, res)
res2, err := th.Store.Channel().Autocomplete(th.User.Id, "channela", false, false)
require.NoError(t, err)
th.checkChannelIdsMatchWithTeamData(t, []string{th.ChannelAnotherTeam.Id, th.ChannelBasic.Id, alternate.Id}, res2)
res2, err = th.Store.Channel().Autocomplete(th.User.Id, "ChAnNeL-a", false, false)
require.NoError(t, err)
th.checkChannelIdsMatchWithTeamData(t, []string{th.ChannelAnotherTeam.Id, th.ChannelBasic.Id, alternate.Id}, res2)
}
func testSearchChannelsInCaseInsensitiveMannerPostgres(t *testing.T, th *SearchTestHelper) {
alternate, err := th.createChannel(th.Team.Id, "channel-alternate", "ChannelAlternate", "", model.ChannelTypeOpen, th.User, false)
require.NoError(t, err)
defer th.deleteChannel(alternate)
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "channela", false, false)
require.NoError(t, err)
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, alternate.Id}, res)
res, err = th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "ChAnNeL-a", false, false)
require.NoError(t, err)
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, th.ChannelPrivate.Id, alternate.Id}, res)
}
func testSearchShouldSupportHavingHyphenAsLastCharacter(t *testing.T, th *SearchTestHelper) {
alternate, err := th.createChannel(th.Team.Id, "channel-alternate", "ChannelAlternate", "", model.ChannelTypeOpen, th.User, false)
require.NoError(t, err)
defer th.deleteChannel(alternate)
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "channel-", false, false)
require.NoError(t, err)
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, th.ChannelPrivate.Id, alternate.Id}, res)
res2, err := th.Store.Channel().Autocomplete(th.User.Id, "channel-", false, false)
require.NoError(t, err)
th.checkChannelIdsMatchWithTeamData(t, []string{th.ChannelAnotherTeam.Id, th.ChannelBasic.Id, th.ChannelPrivate.Id, alternate.Id}, res2)
}
func testSearchShouldSupportAutocompleteWithArchivedChannels(t *testing.T, th *SearchTestHelper) {
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "channel-", true, false)
require.NoError(t, err)
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, th.ChannelPrivate.Id, th.ChannelDeleted.Id}, res)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchtest
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
var searchFileInfoStoreTests = []searchTest{
{
Name: "Should be able to search posts including results from DMs",
Fn: testFileInfoSearchFileInfosIncludingDMs,
Tags: []string{EngineAll},
},
{
Name: "Should be able to search posts using pagination",
Fn: testFileInfoSearchFileInfosWithPagination,
Tags: []string{EngineElasticSearch, EngineBleve},
},
{
Name: "Should be able to search for exact phrases in quotes",
Fn: testFileInfoSearchExactPhraseInQuotes,
Tags: []string{EnginePostgres, EngineMySql, EngineElasticSearch},
},
{
Name: "Should be able to search for email addresses with or without quotes",
Fn: testFileInfoSearchEmailAddresses,
Tags: []string{EngineElasticSearch},
},
{
Name: "Should be able to search when markdown underscores are applied",
Fn: testFileInfoSearchMarkdownUnderscores,
Tags: []string{EnginePostgres, EngineElasticSearch},
},
{
Name: "Should be able to search for non-latin words",
Fn: testFileInfoSearchNonLatinWords,
Tags: []string{EngineElasticSearch},
},
{
Name: "Should be able to search for alternative spellings of words",
Fn: testFileInfoSearchAlternativeSpellings,
Tags: []string{EngineElasticSearch},
},
{
Name: "Should be able to search for alternative spellings of words with and without accents",
Fn: testFileInfoSearchAlternativeSpellingsAccents,
Tags: []string{EngineElasticSearch},
},
{
Name: "Should be able to search or exclude messages written by a specific user",
Fn: testFileInfoSearchOrExcludeFileInfosBySpecificUser,
Tags: []string{EngineAll},
},
{
Name: "Should be able to search or exclude messages written in a specific channel",
Fn: testFileInfoSearchOrExcludeFileInfosInChannel,
Tags: []string{EngineAll},
},
{
Name: "Should be able to search or exclude messages written in a DM or GM",
Fn: testFileInfoSearchOrExcludeFileInfosInDMGM,
Tags: []string{EngineAll},
},
{
Name: "Should be able to search or exclude files by extensions",
Fn: testFileInfoSearchOrExcludeByExtensions,
Tags: []string{EngineAll},
},
{
Name: "Should be able to filter messages written after a specific date",
Fn: testFileInfoFilterFilesAfterSpecificDate,
Tags: []string{EngineAll},
},
{
Name: "Should be able to filter messages written before a specific date",
Fn: testFileInfoFilterFilesBeforeSpecificDate,
Tags: []string{EngineAll},
},
{
Name: "Should be able to filter messages written on a specific date",
Fn: testFileInfoFilterFilesInSpecificDate,
Tags: []string{EngineAll},
},
{
Name: "Should be able to exclude messages that contain a search term",
Fn: testFileInfoFilterFilesWithATerm,
Tags: []string{EngineMySql, EnginePostgres},
},
{
Name: "Should be able to search using boolean operators",
Fn: testFileInfoSearchUsingBooleanOperators,
Tags: []string{EngineMySql, EnginePostgres, EngineElasticSearch},
},
{
Name: "Should be able to search with combined filters",
Fn: testFileInfoSearchUsingCombinedFilters,
Tags: []string{EngineAll},
},
{
Name: "Should be able to ignore stop words",
Fn: testFileInfoSearchIgnoringStopWords,
Tags: []string{EngineMySql, EngineElasticSearch},
},
{
Name: "Should support search stemming",
Fn: testFileInfoSupportStemming,
Tags: []string{EnginePostgres, EngineElasticSearch},
},
{
Name: "Should support search with wildcards",
Fn: testFileInfoSupportWildcards,
Tags: []string{EngineAll},
},
{
Name: "Should not support search with preceding wildcards",
Fn: testFileInfoNotSupportPrecedingWildcards,
Tags: []string{EngineAll},
},
{
Name: "Should discard a wildcard if it's not placed immediately by text",
Fn: testFileInfoSearchDiscardWildcardAlone,
Tags: []string{EngineAll},
},
{
Name: "Should support terms with dash",
Fn: testFileInfoSupportTermsWithDash,
Tags: []string{EngineAll},
Skip: true,
},
{
Name: "Should support terms with underscore",
Fn: testFileInfoSupportTermsWithUnderscore,
Tags: []string{EngineMySql, EngineElasticSearch},
},
{
Name: "Should be able to search in deleted/archived channels",
Fn: testFileInfoSearchInDeletedOrArchivedChannels,
Tags: []string{EngineMySql, EnginePostgres},
},
{
Name: "Should be able to search terms with dashes",
Fn: testFileInfoSearchTermsWithDashes,
Tags: []string{EngineAll},
Skip: true,
SkipMessage: "Not working",
},
{
Name: "Should be able to search terms with dots",
Fn: testFileInfoSearchTermsWithDots,
Tags: []string{EnginePostgres, EngineElasticSearch},
},
{
Name: "Should be able to search terms with underscores",
Fn: testFileInfoSearchTermsWithUnderscores,
Tags: []string{EngineMySql, EngineElasticSearch},
},
{
Name: "Should be able to combine stemming and wildcards",
Fn: testFileInfoSupportStemmingAndWildcards,
Tags: []string{EngineElasticSearch},
},
{
Name: "Should support wildcard outside quotes",
Fn: testFileInfoSupportWildcardOutsideQuotes,
Tags: []string{EngineElasticSearch},
},
{
Name: "Should not support slash as character separator",
Fn: testFileInfoSlashShouldNotBeCharSeparator,
Tags: []string{EngineMySql, EngineElasticSearch},
},
{
Name: "Should be able to search emails without quoting them",
Fn: testFileInfoSearchEmailsWithoutQuotes,
Tags: []string{EngineElasticSearch},
},
}
func TestSearchFileInfoStore(t *testing.T, s store.Store, testEngine *SearchTestEngine) {
th := &SearchTestHelper{
Store: s,
}
err := th.SetupBasicFixtures()
require.NoError(t, err)
defer th.CleanFixtures()
runTestSearch(t, testEngine, searchFileInfoStoreTests, th)
}
func testFileInfoSearchFileInfosIncludingDMs(t *testing.T, th *SearchTestHelper) {
direct, err := th.createDirectChannel(th.Team.Id, "direct-"+th.Team.Id, "direct-"+th.Team.Id, []*model.User{th.User, th.User2})
require.NoError(t, err)
defer th.deleteChannel(direct)
post, err := th.createPost(th.User.Id, direct.Id, "dm test", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
post2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "dm test", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "dm test filename", "dm contenttest filename", "jpg", "image/jpeg", 0, 1)
require.NoError(t, err)
_, err = th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "dm other filename", "dm other filename", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p2, err := th.createFileInfo(th.User.Id, post2.Id, post2.ChannelId, "channel test filename", "channel contenttest filename", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("by-name", func(t *testing.T) {
params := &model.SearchParams{Terms: "test"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
t.Run("by-content", func(t *testing.T) {
params := &model.SearchParams{Terms: "contenttest"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
}
func testFileInfoSearchFileInfosWithPagination(t *testing.T, th *SearchTestHelper) {
direct, err := th.createDirectChannel(th.Team.Id, "direct", "direct", []*model.User{th.User, th.User2})
require.NoError(t, err)
defer th.deleteChannel(direct)
post, err := th.createPost(th.User.Id, direct.Id, "dm test", "", model.PostTypeDefault, 10000, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
post2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "dm test", "", model.PostTypeDefault, 20000, false)
require.NoError(t, err)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "dm test filename", "dm contenttest filename", "jpg", "image/jpeg", 10000, 0)
require.NoError(t, err)
_, err = th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "dm other filename", "dm other filename", "jpg", "image/jpeg", 20000, 0)
require.NoError(t, err)
p2, err := th.createFileInfo(th.User.Id, post2.Id, post2.ChannelId, "channel test filename", "channel contenttest filename", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("by-name", func(t *testing.T) {
params := &model.SearchParams{Terms: "test"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 1)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
results, err = th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 1, 1)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("by-content", func(t *testing.T) {
params := &model.SearchParams{Terms: "contenttest"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 1)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
results, err = th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 1, 1)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
}
func testFileInfoSearchExactPhraseInQuotes(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "channel test 1 2 3 filename", "channel content test 1 2 3 filename", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
_, err = th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "channel test 123 filename", "channel content test 123 filename", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("by-name", func(t *testing.T) {
params := &model.SearchParams{Terms: "\"channel test 1 2 3\""}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("by-content", func(t *testing.T) {
params := &model.SearchParams{Terms: "\"channel content test 1 2 3\""}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
}
func testFileInfoSearchEmailAddresses(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "test email test@test.com", "test email test@content.com", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
_, err = th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "test email test2@test.com", "test email test2@content.com", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("by-name", func(t *testing.T) {
t.Run("Should search email addresses enclosed by quotes", func(t *testing.T) {
params := &model.SearchParams{Terms: "\"test@test.com\""}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Should search email addresses without quotes", func(t *testing.T) {
params := &model.SearchParams{Terms: "test@test.com"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
})
t.Run("by-content", func(t *testing.T) {
t.Run("Should search email addresses enclosed by quotes", func(t *testing.T) {
params := &model.SearchParams{Terms: "\"test@content.com\""}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Should search email addresses without quotes", func(t *testing.T) {
params := &model.SearchParams{Terms: "test@content.com"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
})
}
func testFileInfoSearchMarkdownUnderscores(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "_start middle end_ _another_", "_start middle end_ _another_", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("Should search the start inside the markdown underscore", func(t *testing.T) {
params := &model.SearchParams{Terms: "start"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Should search a word in the middle of the markdown underscore", func(t *testing.T) {
params := &model.SearchParams{Terms: "middle"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Should search in the end of the markdown underscore", func(t *testing.T) {
params := &model.SearchParams{Terms: "end"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Should search inside markdown underscore", func(t *testing.T) {
params := &model.SearchParams{Terms: "another"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
}
func testFileInfoSearchNonLatinWords(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Should be able to search chinese words", func(t *testing.T) {
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "你好", "你好", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p2, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "你", "你", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("Should search one word", func(t *testing.T) {
params := &model.SearchParams{Terms: "你"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
t.Run("Should search two words", func(t *testing.T) {
params := &model.SearchParams{Terms: "你好"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Should search with wildcard", func(t *testing.T) {
params := &model.SearchParams{Terms: "你*"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
})
t.Run("Should be able to search cyrillic words", func(t *testing.T) {
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "слово test", "слово test", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("Should search one word", func(t *testing.T) {
params := &model.SearchParams{Terms: "слово"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Should search using wildcard", func(t *testing.T) {
params := &model.SearchParams{Terms: "слов*"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
})
t.Run("Should be able to search japanese words", func(t *testing.T) {
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "本", "本", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p2, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "本木", "本木", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("Should search one word", func(t *testing.T) {
params := &model.SearchParams{Terms: "本"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
t.Run("Should search two words", func(t *testing.T) {
params := &model.SearchParams{Terms: "本木"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
t.Run("Should search with wildcard", func(t *testing.T) {
params := &model.SearchParams{Terms: "本*"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
})
t.Run("Should be able to search korean words", func(t *testing.T) {
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "불", "불", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p2, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "불다", "불다", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("Should search one word", func(t *testing.T) {
params := &model.SearchParams{Terms: "불"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Should search two words", func(t *testing.T) {
params := &model.SearchParams{Terms: "불다"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
t.Run("Should search with wildcard", func(t *testing.T) {
params := &model.SearchParams{Terms: "불*"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
})
}
func testFileInfoSearchAlternativeSpellings(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "Straße test", "Straße test", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p2, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "Strasse test", "Strasse test", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
params := &model.SearchParams{Terms: "Straße"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
params = &model.SearchParams{Terms: "Strasse"}
results, err = th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
}
func testFileInfoSearchAlternativeSpellingsAccents(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "café", "café", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p2, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "café", "café", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
params := &model.SearchParams{Terms: "café"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
params = &model.SearchParams{Terms: "café"}
results, err = th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
params = &model.SearchParams{Terms: "cafe"}
results, err = th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 0)
}
func testFileInfoSearchOrExcludeFileInfosBySpecificUser(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelPrivate.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "test fromuser filename", "test fromuser filename", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
_, err = th.createFileInfo(th.User2.Id, post.Id, post.ChannelId, "test fromuser filename", "test fromuser filename", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
defer th.deleteUserFileInfos(th.User2.Id)
params := &model.SearchParams{Terms: "fromuser", FromUsers: []string{th.User.Id}}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
}
func testFileInfoSearchOrExcludeFileInfosInChannel(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
post2, err := th.createPost(th.User.Id, th.ChannelPrivate.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "test fromuser filename", "test fromuser filename", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
_, err = th.createFileInfo(th.User.Id, post2.Id, post2.ChannelId, "test fromuser filename", "test fromuser filename", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
defer th.deleteUserFileInfos(th.User2.Id)
params := &model.SearchParams{Terms: "fromuser", InChannels: []string{th.ChannelBasic.Id}}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
}
func testFileInfoSearchOrExcludeFileInfosInDMGM(t *testing.T, th *SearchTestHelper) {
direct, err := th.createDirectChannel(th.Team.Id, "direct", "direct", []*model.User{th.User, th.User2})
require.NoError(t, err)
defer th.deleteChannel(direct)
group, err := th.createGroupChannel(th.Team.Id, "test group", []*model.User{th.User, th.User2})
require.NoError(t, err)
defer th.deleteChannel(group)
post1, err := th.createPost(th.User.Id, direct.Id, "test fromuser", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
post2, err := th.createPost(th.User2.Id, group.Id, "test fromuser 2", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
defer th.deleteUserPosts(th.User2.Id)
p1, err := th.createFileInfo(th.User.Id, post1.Id, post1.ChannelId, "test fromuser", "test fromuser", "jpg", "image/jpg", 0, 0)
require.NoError(t, err)
p2, err := th.createFileInfo(th.User2.Id, post2.Id, post2.ChannelId, "test fromuser 2", "test fromuser 2", "jpg", "image/jpg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
defer th.deleteUserFileInfos(th.User2.Id)
t.Run("Should be able to search in both DM and GM channels", func(t *testing.T) {
params := &model.SearchParams{
Terms: "fromuser",
InChannels: []string{direct.Id, group.Id},
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
t.Run("Should be able to search only in DM channel", func(t *testing.T) {
params := &model.SearchParams{
Terms: "fromuser",
InChannels: []string{direct.Id},
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Should be able to search only in GM channel", func(t *testing.T) {
params := &model.SearchParams{
Terms: "fromuser",
InChannels: []string{group.Id},
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
}
func testFileInfoSearchOrExcludeByExtensions(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "test", "test", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p2, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "test", "test", "png", "image/png", 0, 0)
require.NoError(t, err)
p3, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "test", "test", "bmp", "image/bmp", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("Search by one extension", func(t *testing.T) {
params := &model.SearchParams{
Terms: "test",
InChannels: []string{th.ChannelBasic.Id},
Extensions: []string{"jpg"},
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Search by multiple extensions", func(t *testing.T) {
params := &model.SearchParams{
Terms: "test",
InChannels: []string{th.ChannelBasic.Id},
Extensions: []string{"jpg", "bmp"},
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p3.Id, results.FileInfos)
})
t.Run("Search excluding one extension", func(t *testing.T) {
params := &model.SearchParams{
Terms: "test",
InChannels: []string{th.ChannelBasic.Id},
ExcludedExtensions: []string{"jpg"},
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p3.Id, results.FileInfos)
})
t.Run("Search excluding multiple extensions", func(t *testing.T) {
params := &model.SearchParams{
Terms: "test",
InChannels: []string{th.ChannelBasic.Id},
ExcludedExtensions: []string{"jpg", "bmp"},
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
}
func testFileInfoFilterFilesInSpecificDate(t *testing.T, th *SearchTestHelper) {
post1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
post2, err := th.createPost(th.User.Id, th.ChannelPrivate.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
creationDate := model.GetMillisForTime(time.Date(2020, 03, 22, 12, 0, 0, 0, time.UTC))
p1, err := th.createFileInfo(th.User.Id, post1.Id, post1.ChannelId, "test in specific date", "test in specific date", "jpg", "image/jpeg", creationDate, 0)
require.NoError(t, err)
creationDate2 := model.GetMillisForTime(time.Date(2020, 03, 23, 0, 0, 0, 0, time.UTC))
p2, err := th.createFileInfo(th.User.Id, post2.Id, post2.ChannelId, "test in the present", "test in the present", "jpg", "image/jpeg", creationDate2, 0)
require.NoError(t, err)
creationDate3 := model.GetMillisForTime(time.Date(2020, 03, 21, 23, 59, 59, 0, time.UTC))
p3, err := th.createFileInfo(th.User.Id, post1.Id, post1.ChannelId, "test in the present", "test in the present", "jpg", "image/jpeg", creationDate3, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("Should be able to search posts on date", func(t *testing.T) {
params := &model.SearchParams{
Terms: "test",
OnDate: "2020-03-22",
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Should be able to exclude posts on date", func(t *testing.T) {
params := &model.SearchParams{
Terms: "test",
ExcludedDate: "2020-03-22",
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p3.Id, results.FileInfos)
})
}
func testFileInfoFilterFilesBeforeSpecificDate(t *testing.T, th *SearchTestHelper) {
post1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
post2, err := th.createPost(th.User.Id, th.ChannelPrivate.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
creationDate := model.GetMillisForTime(time.Date(2020, 03, 01, 12, 0, 0, 0, time.UTC))
p1, err := th.createFileInfo(th.User.Id, post1.Id, post1.ChannelId, "test in specific date", "test in specific date", "jpg", "image/jpeg", creationDate, 0)
require.NoError(t, err)
creationDate2 := model.GetMillisForTime(time.Date(2020, 03, 22, 23, 59, 59, 0, time.UTC))
p2, err := th.createFileInfo(th.User.Id, post2.Id, post2.ChannelId, "test in specific date 2", "test in specific date 2", "jpg", "image/jpeg", creationDate2, 0)
require.NoError(t, err)
creationDate3 := model.GetMillisForTime(time.Date(2020, 03, 26, 16, 55, 0, 0, time.UTC))
p3, err := th.createFileInfo(th.User.Id, post1.Id, post1.ChannelId, "test in the present", "test in the present", "jpg", "image/jpeg", creationDate3, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("Should be able to search posts before a date", func(t *testing.T) {
params := &model.SearchParams{
Terms: "test",
BeforeDate: "2020-03-23",
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
t.Run("Should be able to exclude posts before a date", func(t *testing.T) {
params := &model.SearchParams{
Terms: "test",
ExcludedBeforeDate: "2020-03-23",
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p3.Id, results.FileInfos)
})
}
func testFileInfoFilterFilesAfterSpecificDate(t *testing.T, th *SearchTestHelper) {
post1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
post2, err := th.createPost(th.User.Id, th.ChannelPrivate.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
creationDate := model.GetMillisForTime(time.Date(2020, 03, 01, 12, 0, 0, 0, time.UTC))
p1, err := th.createFileInfo(th.User.Id, post1.Id, post1.ChannelId, "test in specific date", "test in specific date", "jpg", "image/jpeg", creationDate, 0)
require.NoError(t, err)
creationDate2 := model.GetMillisForTime(time.Date(2020, 03, 22, 23, 59, 59, 0, time.UTC))
p2, err := th.createFileInfo(th.User.Id, post2.Id, post2.ChannelId, "test in specific date 2", "test in specific date 2", "jpg", "image/jpeg", creationDate2, 0)
require.NoError(t, err)
creationDate3 := model.GetMillisForTime(time.Date(2020, 03, 26, 16, 55, 0, 0, time.UTC))
p3, err := th.createFileInfo(th.User.Id, post1.Id, post1.ChannelId, "test in the present", "test in the present", "jpg", "image/jpeg", creationDate3, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("Should be able to search posts after a date", func(t *testing.T) {
params := &model.SearchParams{
Terms: "test",
AfterDate: "2020-03-23",
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p3.Id, results.FileInfos)
})
t.Run("Should be able to exclude posts after a date", func(t *testing.T) {
params := &model.SearchParams{
Terms: "test",
ExcludedAfterDate: "2020-03-23",
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
}
func testFileInfoFilterFilesWithATerm(t *testing.T, th *SearchTestHelper) {
post1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
post2, err := th.createPost(th.User.Id, th.ChannelPrivate.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post1.Id, post1.ChannelId, "one two three", "one two three", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p2, err := th.createFileInfo(th.User.Id, post2.Id, post2.ChannelId, "one four five six", "one four five six", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
_, err = th.createFileInfo(th.User.Id, post1.Id, post1.ChannelId, "one seven eight nine", "one seven eight nine", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("Should exclude terms", func(t *testing.T) {
params := &model.SearchParams{
Terms: "one",
ExcludedTerms: "five eight",
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Should exclude quoted terms", func(t *testing.T) {
params := &model.SearchParams{
Terms: "one",
ExcludedTerms: "\"eight nine\"",
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
}
func testFileInfoSearchUsingBooleanOperators(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "one two three message", "one two three message", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p2, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "two messages", "two messages", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
_, err = th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "another message", "another message", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("Should search posts using OR operator", func(t *testing.T) {
params := &model.SearchParams{
Terms: "one two",
OrTerms: true,
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
t.Run("Should search posts using AND operator", func(t *testing.T) {
params := &model.SearchParams{
Terms: "one two",
OrTerms: false,
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
}
func testFileInfoSearchUsingCombinedFilters(t *testing.T, th *SearchTestHelper) {
post1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
post2, err := th.createPost(th.User.Id, th.ChannelPrivate.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
creationDate := model.GetMillisForTime(time.Date(2020, 03, 01, 12, 0, 0, 0, time.UTC))
p1, err := th.createFileInfo(th.User.Id, post2.Id, post2.ChannelId, "one two three message", "one two three message", "jpg", "image/jpeg", creationDate, 0)
require.NoError(t, err)
creationDate2 := model.GetMillisForTime(time.Date(2020, 03, 10, 12, 0, 0, 0, time.UTC))
p2, err := th.createFileInfo(th.User2.Id, post2.Id, post2.ChannelId, "two messages", "two messages", "jpg", "image/jpeg", creationDate2, 0)
require.NoError(t, err)
creationDate3 := model.GetMillisForTime(time.Date(2020, 03, 20, 12, 0, 0, 0, time.UTC))
p3, err := th.createFileInfo(th.User.Id, post1.Id, post1.ChannelId, "two another message", "two another message", "jpg", "image/jpeg", creationDate3, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
defer th.deleteUserFileInfos(th.User2.Id)
t.Run("Should search combining from user and in channel filters", func(t *testing.T) {
params := &model.SearchParams{
Terms: "two",
FromUsers: []string{th.User2.Id},
InChannels: []string{th.ChannelPrivate.Id},
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
t.Run("Should search combining excluding users and in channel filters", func(t *testing.T) {
params := &model.SearchParams{
Terms: "two",
ExcludedUsers: []string{th.User2.Id},
InChannels: []string{th.ChannelPrivate.Id},
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Should search combining excluding dates and in channel filters", func(t *testing.T) {
params := &model.SearchParams{
Terms: "two",
ExcludedBeforeDate: "2020-03-09",
ExcludedAfterDate: "2020-03-11",
InChannels: []string{th.ChannelPrivate.Id},
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
t.Run("Should search combining excluding dates and in channel filters", func(t *testing.T) {
params := &model.SearchParams{
Terms: "two",
AfterDate: "2020-03-11",
ExcludedChannels: []string{th.ChannelPrivate.Id},
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p3.Id, results.FileInfos)
})
}
func testFileInfoSearchIgnoringStopWords(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "the search for a bunch of stop words", "the search for a bunch of stop words", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p2, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "the objective is to avoid a bunch of stop words", "the objective is to avoid a bunch of stop words", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p3, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "in the a on to where you", "in the a on to where you", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p4, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "where is the car?", "where is the car?", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("Should avoid stop word 'the'", func(t *testing.T) {
params := &model.SearchParams{
Terms: "the search",
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Should avoid stop word 'a'", func(t *testing.T) {
params := &model.SearchParams{
Terms: "a avoid",
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
t.Run("Should avoid stop word 'in'", func(t *testing.T) {
params := &model.SearchParams{
Terms: "in where you",
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p3.Id, results.FileInfos)
})
t.Run("Should avoid stop words 'where', 'is' and 'the'", func(t *testing.T) {
results, err := th.Store.FileInfo().Search([]*model.SearchParams{{Terms: "is the car"}}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p4.Id, results.FileInfos)
})
t.Run("Should remove all terms and return empty list", func(t *testing.T) {
results, err := th.Store.FileInfo().Search([]*model.SearchParams{{Terms: "is the"}}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Empty(t, results.FileInfos)
})
}
func testFileInfoSupportStemming(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "search post", "search post", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p2, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "searching post", "searching post", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
_, err = th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "another post", "another post", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
params := &model.SearchParams{
Terms: "search",
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
}
func testFileInfoSupportWildcards(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "search post", "search post", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p2, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "searching", "searching", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
_, err = th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "another post", "another post", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("Simple wildcard-only search", func(t *testing.T) {
params := &model.SearchParams{
Terms: "search*",
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
t.Run("Wildcard search with another term placed after", func(t *testing.T) {
params := &model.SearchParams{
Terms: "sear* post",
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
}
func testFileInfoNotSupportPrecedingWildcards(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
_, err = th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "search post", "search post", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
_, err = th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "searching post", "searching post", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
_, err = th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "another post", "another post", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
params := &model.SearchParams{
Terms: "*earch",
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 0)
}
func testFileInfoSearchDiscardWildcardAlone(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "qwerty", "qwerty", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
_, err = th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "qwertyjkl", "qwertyjkl", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
params := &model.SearchParams{
Terms: "qwerty *",
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
}
func testFileInfoSupportTermsWithDash(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "search term-with-dash", "search term-with-dash", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
_, err = th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "searching term with dash", "searching term with dash", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("Should search terms with dash", func(t *testing.T) {
params := &model.SearchParams{
Terms: "term-with-dash",
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Should search terms with dash using quotes", func(t *testing.T) {
params := &model.SearchParams{
Terms: "\"term-with-dash\"",
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
}
func testFileInfoSupportTermsWithUnderscore(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "search term_with_underscore", "search term_with_underscore", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
_, err = th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "searching term with underscore", "searching term with underscore", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("Should search terms with underscore", func(t *testing.T) {
params := &model.SearchParams{
Terms: "term_with_underscore",
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Should search terms with underscore using quotes", func(t *testing.T) {
params := &model.SearchParams{
Terms: "\"term_with_underscore\"",
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
}
func testFileInfoSearchInDeletedOrArchivedChannels(t *testing.T, th *SearchTestHelper) {
post1, err := th.createPost(th.User.Id, th.ChannelDeleted.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
post2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
post3, err := th.createPost(th.User.Id, th.ChannelPrivate.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post1.Id, post1.ChannelId, "message in deleted channel", "message in deleted channel", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p2, err := th.createFileInfo(th.User.Id, post2.Id, post2.ChannelId, "message in regular channel", "message in regular channel", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p3, err := th.createFileInfo(th.User.Id, post3.Id, post3.ChannelId, "message in private channel", "message in private channel", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("Doesn't include posts in deleted channels", func(t *testing.T) {
params := &model.SearchParams{Terms: "message", IncludeDeletedChannels: false}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p3.Id, results.FileInfos)
})
t.Run("Include posts in deleted channels", func(t *testing.T) {
params := &model.SearchParams{Terms: "message", IncludeDeletedChannels: true}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 3)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p3.Id, results.FileInfos)
})
t.Run("Include posts in deleted channels using multiple terms", func(t *testing.T) {
params := &model.SearchParams{Terms: "message channel", IncludeDeletedChannels: true}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 3)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p3.Id, results.FileInfos)
})
t.Run("Include posts in deleted channels using multiple OR terms", func(t *testing.T) {
params := &model.SearchParams{
Terms: "message channel",
IncludeDeletedChannels: true,
OrTerms: true,
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 3)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p3.Id, results.FileInfos)
})
t.Run("All IncludeDeletedChannels params should have same value if multiple SearchParams provided", func(t *testing.T) {
params1 := &model.SearchParams{
Terms: "message channel",
IncludeDeletedChannels: true,
}
params2 := &model.SearchParams{
Terms: "#hashtag",
IncludeDeletedChannels: false,
}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params1, params2}, th.User.Id, th.Team.Id, 0, 20)
require.Nil(t, results)
require.Error(t, err)
})
}
func testFileInfoSearchTermsWithDashes(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "message with-dash-term", "message with-dash-term", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p2, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "message with dash term", "message with dash term", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("Search for terms with dash", func(t *testing.T) {
params := &model.SearchParams{Terms: "with-dash-term"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Search for terms with quoted dash", func(t *testing.T) {
params := &model.SearchParams{Terms: "\"with-dash-term\""}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Search for multiple terms with one having dash", func(t *testing.T) {
params := &model.SearchParams{Terms: "with-dash-term message"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Search for multiple OR terms with one having dash", func(t *testing.T) {
params := &model.SearchParams{Terms: "with-dash-term message", OrTerms: true}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
}
func testFileInfoSearchTermsWithDots(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "message with.dots.term", "message with.dots.term", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p2, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "message with dots term", "message with dots term", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("Search for terms with dots", func(t *testing.T) {
params := &model.SearchParams{Terms: "with.dots.term"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Search for terms with quoted dots", func(t *testing.T) {
params := &model.SearchParams{Terms: "\"with.dots.term\""}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Search for multiple terms with one having dots", func(t *testing.T) {
params := &model.SearchParams{Terms: "with.dots.term message"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Search for multiple OR terms with one having dots", func(t *testing.T) {
params := &model.SearchParams{Terms: "with.dots.term message", OrTerms: true}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
}
func testFileInfoSearchTermsWithUnderscores(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "message with_underscores_term", "message with_underscores_term", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p2, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "message with underscores term", "message with underscores term", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("Search for terms with underscores", func(t *testing.T) {
params := &model.SearchParams{Terms: "with_underscores_term"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Search for terms with quoted underscores", func(t *testing.T) {
params := &model.SearchParams{Terms: "\"with_underscores_term\""}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Search for multiple terms with one having underscores", func(t *testing.T) {
params := &model.SearchParams{Terms: "with_underscores_term message"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
})
t.Run("Search for multiple OR terms with one having underscores", func(t *testing.T) {
params := &model.SearchParams{Terms: "with_underscores_term message", OrTerms: true}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
}
func testFileInfoSupportStemmingAndWildcards(t *testing.T, th *SearchTestHelper) {
post1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
post2, err := th.createPost(th.User.Id, th.ChannelPrivate.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post1.Id, post1.ChannelId, "approve", "approve", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p2, err := th.createFileInfo(th.User.Id, post2.Id, post2.ChannelId, "approved", "approved", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p3, err := th.createFileInfo(th.User.Id, post2.Id, post2.ChannelId, "approvedz", "approvedz", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("Should stem appr", func(t *testing.T) {
params := &model.SearchParams{Terms: "appr*"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 3)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p3.Id, results.FileInfos)
})
t.Run("Should stem approve", func(t *testing.T) {
params := &model.SearchParams{Terms: "approve*"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p3.Id, results.FileInfos)
})
}
func testFileInfoSupportWildcardOutsideQuotes(t *testing.T, th *SearchTestHelper) {
post1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
post2, err := th.createPost(th.User.Id, th.ChannelPrivate.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p1, err := th.createFileInfo(th.User.Id, post1.Id, post1.ChannelId, "hello world", "hello world", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
p2, err := th.createFileInfo(th.User.Id, post2.Id, post2.ChannelId, "hell or heaven", "hell or heaven", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
t.Run("Should return results without quotes", func(t *testing.T) {
params := &model.SearchParams{Terms: "hell*"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 2)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
t.Run("Should return just one result with quotes", func(t *testing.T) {
params := &model.SearchParams{Terms: "\"hell\"*"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p2.Id, results.FileInfos)
})
}
func testFileInfoSlashShouldNotBeCharSeparator(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "alpha/beta gamma, theta", "alpha/beta gamma, theta", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
params := &model.SearchParams{Terms: "gamma"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
params = &model.SearchParams{Terms: "beta"}
results, err = th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
params = &model.SearchParams{Terms: "alpha"}
results, err = th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
}
func testFileInfoSearchEmailsWithoutQuotes(t *testing.T, th *SearchTestHelper) {
post, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "testmessage", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
p1, err := th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "message test@test.com", "message test@test.com", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
_, err = th.createFileInfo(th.User.Id, post.Id, post.ChannelId, "message test2@test.com", "message test2@test.com", "jpg", "image/jpeg", 0, 0)
require.NoError(t, err)
defer th.deleteUserFileInfos(th.User.Id)
params := &model.SearchParams{Terms: "test@test.com"}
results, err := th.Store.FileInfo().Search([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.FileInfos, 1)
th.checkFileInfoInSearchResults(t, p1.Id, results.FileInfos)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchtest
import (
"fmt"
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SearchTestHelper struct {
Store store.Store
Team *model.Team
AnotherTeam *model.Team
User *model.User
User2 *model.User
UserAnotherTeam *model.User
ChannelBasic *model.Channel
ChannelPrivate *model.Channel
ChannelAnotherTeam *model.Channel
ChannelDeleted *model.Channel
}
func (th *SearchTestHelper) SetupBasicFixtures() error {
// Remove users from previous tests
err := th.cleanAllUsers()
if err != nil {
return err
}
// Create teams
team, err := th.createTeam("searchtest-team", "Searchtest team", model.TeamOpen)
if err != nil {
return err
}
anotherTeam, err := th.createTeam("another-searchtest-team", "Another Searchtest team", model.TeamOpen)
if err != nil {
return err
}
// Create users
user, err := th.createUser("basicusername1", "basicnickname1", "basicfirstname1", "basiclastname1")
if err != nil {
return err
}
user2, err := th.createUser("basicusername2", "basicnickname2", "basicfirstname2", "basiclastname2")
if err != nil {
return err
}
useranother, err := th.createUser("basicusername3", "basicnickname3", "basicfirstname3", "basiclastname3")
if err != nil {
return err
}
// Create channels
channelBasic, err := th.createChannel(team.Id, "channel-a", "ChannelA", "", model.ChannelTypeOpen, nil, false)
if err != nil {
return err
}
channelPrivate, err := th.createChannel(team.Id, "channel-private", "ChannelPrivate", "", model.ChannelTypePrivate, nil, false)
if err != nil {
return err
}
channelDeleted, err := th.createChannel(team.Id, "channel-deleted", "ChannelA (deleted)", "", model.ChannelTypeOpen, nil, true)
if err != nil {
return err
}
channelAnotherTeam, err := th.createChannel(anotherTeam.Id, "channel-a", "ChannelA", "", model.ChannelTypeOpen, nil, false)
if err != nil {
return err
}
err = th.addUserToTeams(user, []string{team.Id, anotherTeam.Id})
if err != nil {
return err
}
err = th.addUserToTeams(user2, []string{team.Id, anotherTeam.Id})
if err != nil {
return err
}
err = th.addUserToTeams(useranother, []string{anotherTeam.Id})
if err != nil {
return err
}
err = th.addUserToChannels(user, []string{channelBasic.Id, channelPrivate.Id, channelDeleted.Id})
if err != nil {
return err
}
err = th.addUserToChannels(user2, []string{channelPrivate.Id, channelDeleted.Id})
if err != nil {
return err
}
err = th.addUserToChannels(useranother, []string{channelAnotherTeam.Id})
if err != nil {
return err
}
th.Team = team
th.AnotherTeam = anotherTeam
th.User = user
th.User2 = user2
th.UserAnotherTeam = useranother
th.ChannelBasic = channelBasic
th.ChannelPrivate = channelPrivate
th.ChannelAnotherTeam = channelAnotherTeam
th.ChannelDeleted = channelDeleted
return nil
}
func (th *SearchTestHelper) CleanFixtures() error {
err := th.deleteChannels([]*model.Channel{
th.ChannelBasic, th.ChannelPrivate, th.ChannelAnotherTeam, th.ChannelDeleted,
})
if err != nil {
return err
}
err = th.deleteTeam(th.Team)
if err != nil {
return err
}
err = th.deleteTeam(th.AnotherTeam)
if err != nil {
return err
}
err = th.cleanAllUsers()
if err != nil {
return err
}
return nil
}
func (th *SearchTestHelper) createTeam(name, displayName, teamType string) (*model.Team, error) {
return th.Store.Team().Save(&model.Team{
Name: name,
DisplayName: displayName,
Type: teamType,
})
}
func (th *SearchTestHelper) deleteTeam(team *model.Team) error {
err := th.Store.Team().RemoveAllMembersByTeam(team.Id)
if err != nil {
return err
}
return th.Store.Team().PermanentDelete(team.Id)
}
func (th *SearchTestHelper) makeEmail() string {
return "success_" + model.NewId() + "@simulator.amazon.com"
}
func (th *SearchTestHelper) createUser(username, nickname, firstName, lastName string) (*model.User, error) {
return th.Store.User().Save(&model.User{
Username: username,
Password: username,
Nickname: nickname,
FirstName: firstName,
LastName: lastName,
Email: th.makeEmail(),
})
}
func (th *SearchTestHelper) createGuest(username, nickname, firstName, lastName string) (*model.User, error) {
return th.Store.User().Save(&model.User{
Username: username,
Password: username,
Nickname: nickname,
FirstName: firstName,
LastName: lastName,
Email: th.makeEmail(),
Roles: model.SystemGuestRoleId,
})
}
func (th *SearchTestHelper) deleteUser(user *model.User) error {
return th.Store.User().PermanentDelete(user.Id)
}
func (th *SearchTestHelper) deleteBotUser(botID string) error {
if err := th.deleteBot(botID); err != nil {
return err
}
return th.Store.User().PermanentDelete(botID)
}
func (th *SearchTestHelper) cleanAllUsers() error {
users, err := th.Store.User().GetAll()
if err != nil {
return err
}
for _, u := range users {
err := th.deleteUser(u)
if err != nil {
return err
}
}
return nil
}
func (th *SearchTestHelper) createBot(username, displayName, ownerID string) (*model.Bot, error) {
botModel := &model.Bot{
Username: username,
DisplayName: displayName,
OwnerId: ownerID,
}
user, err := th.Store.User().Save(model.UserFromBot(botModel))
if err != nil {
return nil, errors.New(err.Error())
}
botModel.UserId = user.Id
bot, err := th.Store.Bot().Save(botModel)
if err != nil {
th.Store.User().PermanentDelete(bot.UserId)
return nil, errors.New(err.Error())
}
return bot, nil
}
func (th *SearchTestHelper) deleteBot(botID string) error {
err := th.Store.Bot().PermanentDelete(botID)
if err != nil {
return errors.New(err.Error())
}
return nil
}
func (th *SearchTestHelper) createChannel(teamID, name, displayName, purpose string, channelType model.ChannelType, user *model.User, deleted bool) (*model.Channel, error) {
channel, err := th.Store.Channel().Save(&model.Channel{
TeamId: teamID,
DisplayName: displayName,
Name: name,
Type: channelType,
Purpose: purpose,
}, 999)
if err != nil {
return nil, err
}
if user != nil {
err = th.addUserToChannels(user, []string{channel.Id})
if err != nil {
return nil, err
}
}
if deleted {
err := th.Store.Channel().Delete(channel.Id, model.GetMillis())
if err != nil {
return nil, err
}
}
return channel, nil
}
func (th *SearchTestHelper) createDirectChannel(teamID, name, displayName string, users []*model.User) (*model.Channel, error) {
channel := &model.Channel{
TeamId: teamID,
Name: name,
DisplayName: displayName,
Type: model.ChannelTypeDirect,
}
m1 := &model.ChannelMember{}
m1.ChannelId = channel.Id
m1.UserId = users[0].Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := &model.ChannelMember{}
m2.ChannelId = channel.Id
m2.UserId = users[0].Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
channel, err := th.Store.Channel().SaveDirectChannel(channel, m1, m2)
if err != nil {
return nil, err
}
return channel, nil
}
func (th *SearchTestHelper) createGroupChannel(teamID, displayName string, users []*model.User) (*model.Channel, error) {
userIDS := make([]string, len(users))
for _, user := range users {
userIDS = append(userIDS, user.Id)
}
group := &model.Channel{
TeamId: teamID,
Name: model.GetGroupNameFromUserIds(userIDS),
DisplayName: displayName,
Type: model.ChannelTypeGroup,
}
channel, err := th.Store.Channel().Save(group, 10000)
if err != nil {
return nil, errors.New(err.Error())
}
for _, user := range users {
err := th.addUserToChannels(user, []string{channel.Id})
if err != nil {
return nil, err
}
}
return channel, nil
}
func (th *SearchTestHelper) deleteChannel(channel *model.Channel) error {
err := th.Store.Channel().PermanentDeleteMembersByChannel(channel.Id)
if err != nil {
return err
}
return th.Store.Channel().PermanentDelete(channel.Id)
}
func (th *SearchTestHelper) deleteChannels(channels []*model.Channel) error {
for _, channel := range channels {
err := th.deleteChannel(channel)
if err != nil {
return err
}
}
return nil
}
func (th *SearchTestHelper) createPostModel(userID, channelID, message, hashtags, postType string, createAt int64, pinned bool) *model.Post {
return &model.Post{
Message: message,
ChannelId: channelID,
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
UserId: userID,
Hashtags: hashtags,
IsPinned: pinned,
CreateAt: createAt,
Type: postType,
}
}
func (th *SearchTestHelper) createPost(userID, channelID, message, hashtags, postType string, createAt int64, pinned bool) (*model.Post, error) {
var creationTime int64 = 1000000
if createAt > 0 {
creationTime = createAt
}
postModel := th.createPostModel(userID, channelID, message, hashtags, postType, creationTime, pinned)
return th.Store.Post().Save(postModel)
}
func (th *SearchTestHelper) createFileInfoModel(creatorID, postID, channelID, name, content, extension, mimeType string, createAt, size int64) *model.FileInfo {
return &model.FileInfo{
CreatorId: creatorID,
PostId: postID,
ChannelId: channelID,
CreateAt: createAt,
UpdateAt: createAt,
DeleteAt: 0,
Name: name,
Content: content,
Path: name,
Extension: extension,
Size: size,
MimeType: mimeType,
}
}
func (th *SearchTestHelper) createFileInfo(creatorID, postID, channelID, name, content, extension, mimeType string, createAt, size int64) (*model.FileInfo, error) {
var creationTime int64 = 1000000
if createAt > 0 {
creationTime = createAt
}
fileInfoModel := th.createFileInfoModel(creatorID, postID, channelID, name, content, extension, mimeType, creationTime, size)
return th.Store.FileInfo().Save(fileInfoModel)
}
func (th *SearchTestHelper) createReply(userID, message, hashtags string, parent *model.Post, createAt int64, pinned bool) (*model.Post, error) {
replyModel := th.createPostModel(userID, parent.ChannelId, message, hashtags, parent.Type, createAt, pinned)
replyModel.RootId = parent.Id
return th.Store.Post().Save(replyModel)
}
func (th *SearchTestHelper) deleteUserPosts(userID string) error {
err := th.Store.Post().PermanentDeleteByUser(userID)
if err != nil {
return errors.New(err.Error())
}
return nil
}
func (th *SearchTestHelper) deleteUserFileInfos(userID string) error {
if _, err := th.Store.FileInfo().PermanentDeleteByUser(userID); err != nil {
return errors.New(err.Error())
}
return nil
}
func (th *SearchTestHelper) addUserToTeams(user *model.User, teamIDS []string) error {
for _, teamID := range teamIDS {
_, err := th.Store.Team().SaveMember(&model.TeamMember{TeamId: teamID, UserId: user.Id}, -1)
if err != nil {
return errors.New(err.Error())
}
}
return nil
}
func (th *SearchTestHelper) addUserToChannels(user *model.User, channelIDS []string) error {
for _, channelID := range channelIDS {
_, err := th.Store.Channel().SaveMember(&model.ChannelMember{
ChannelId: channelID,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
if err != nil {
return errors.New(err.Error())
}
}
return nil
}
func (th *SearchTestHelper) assertUsersMatchInAnyOrder(t *testing.T, expected, actual []*model.User) {
expectedUsernames := make([]string, 0, len(expected))
for _, user := range expected {
user.Sanitize(map[string]bool{})
expectedUsernames = append(expectedUsernames, user.Username)
}
actualUsernames := make([]string, 0, len(actual))
for _, user := range actual {
user.Sanitize(map[string]bool{})
actualUsernames = append(actualUsernames, user.Username)
}
if assert.ElementsMatch(t, expectedUsernames, actualUsernames) {
assert.ElementsMatch(t, expected, actual)
}
}
func (th *SearchTestHelper) checkPostInSearchResults(t *testing.T, postID string, searchResults map[string]*model.Post) {
t.Helper()
postIDS := make([]string, len(searchResults))
for ID := range searchResults {
postIDS = append(postIDS, ID)
}
assert.Contains(t, postIDS, postID, "Did not find expected post in search results.")
}
func (th *SearchTestHelper) checkFileInfoInSearchResults(t *testing.T, fileID string, searchResults map[string]*model.FileInfo) {
t.Helper()
fileIDS := make([]string, len(searchResults))
for ID := range searchResults {
fileIDS = append(fileIDS, ID)
}
assert.Contains(t, fileIDS, fileID, "Did not find expected file in search results.")
}
func (th *SearchTestHelper) checkChannelIdsMatch(t *testing.T, expected []string, results model.ChannelList) {
t.Helper()
channelIds := make([]string, len(results))
for i, channel := range results {
channelIds[i] = channel.Id
}
require.ElementsMatch(t, expected, channelIds)
}
func (th *SearchTestHelper) checkChannelIdsMatchWithTeamData(t *testing.T, expected []string, results model.ChannelListWithTeamData) {
t.Helper()
channelIds := make([]string, len(results))
for i, channel := range results {
channelIds[i] = channel.Id
}
require.ElementsMatch(t, expected, channelIds)
}
type ByChannelDisplayName model.ChannelList
func (s ByChannelDisplayName) Len() int { return len(s) }
func (s ByChannelDisplayName) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s ByChannelDisplayName) Less(i, j int) bool {
if s[i].DisplayName != s[j].DisplayName {
return s[i].DisplayName < s[j].DisplayName
}
return s[i].Id < s[j].Id
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchtest
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
var searchPostStoreTests = []searchTest{
{
Name: "Should be able to search posts including results from DMs",
Fn: testSearchPostsIncludingDMs,
Tags: []string{EngineAll},
},
{
Name: "Should be able to search posts using pagination",
Fn: testSearchPostsWithPagination,
Tags: []string{EngineElasticSearch, EngineBleve},
},
{
Name: "Should return pinned and unpinned posts",
Fn: testSearchReturnPinnedAndUnpinned,
Tags: []string{EngineAll},
},
{
Name: "Should be able to search for exact phrases in quotes",
Fn: testSearchExactPhraseInQuotes,
Tags: []string{EnginePostgres, EngineMySql, EngineElasticSearch},
},
{
// Postgres supports search with and without quotes
Name: "Should be able to search for email addresses with or without quotes",
Fn: testSearchEmailAddresses,
Tags: []string{EnginePostgres, EngineElasticSearch},
},
{
// MySql supports search with quotes only
Name: "Should be able to search for email addresses with quotes",
Fn: testSearchEmailAddressesWithQuotes,
Tags: []string{EngineMySql},
},
{
Name: "Should be able to search when markdown underscores are applied",
Fn: testSearchMarkdownUnderscores,
Tags: []string{EnginePostgres, EngineElasticSearch},
},
{
Name: "Should be able to search for non-latin words",
Fn: testSearchNonLatinWords,
Tags: []string{EngineElasticSearch},
},
{
Name: "Should be able to search for alternative spellings of words",
Fn: testSearchAlternativeSpellings,
Tags: []string{EngineElasticSearch},
},
{
Name: "Should be able to search for alternative spellings of words with and without accents",
Fn: testSearchAlternativeSpellingsAccents,
Tags: []string{EngineElasticSearch},
},
{
Name: "Should be able to search or exclude messages written by a specific user",
Fn: testSearchOrExcludePostsBySpecificUser,
Tags: []string{EngineAll},
},
{
Name: "Should be able to search or exclude messages written in a specific channel",
Fn: testSearchOrExcludePostsInChannel,
Tags: []string{EngineAll},
},
{
Name: "Should be able to search or exclude messages written in a DM or GM",
Fn: testSearchOrExcludePostsInDMGM,
Tags: []string{EngineAll},
},
{
Name: "Should be able to filter messages written after a specific date",
Fn: testFilterMessagesAfterSpecificDate,
Tags: []string{EngineAll},
},
{
Name: "Should be able to filter messages written before a specific date",
Fn: testFilterMessagesBeforeSpecificDate,
Tags: []string{EngineAll},
},
{
Name: "Should be able to filter messages written on a specific date",
Fn: testFilterMessagesInSpecificDate,
Tags: []string{EngineAll},
},
{
Name: "Should be able to exclude messages that contain a search term",
Fn: testFilterMessagesWithATerm,
Tags: []string{EngineMySql, EnginePostgres},
},
{
Name: "Should be able to search using boolean operators",
Fn: testSearchUsingBooleanOperators,
Tags: []string{EngineMySql, EnginePostgres, EngineElasticSearch},
},
{
Name: "Should be able to search with combined filters",
Fn: testSearchUsingCombinedFilters,
Tags: []string{EngineAll},
},
{
Name: "Should be able to ignore stop words",
Fn: testSearchIgnoringStopWords,
Tags: []string{EngineMySql, EngineElasticSearch},
},
{
Name: "Should support search stemming",
Fn: testSupportStemming,
Tags: []string{EnginePostgres, EngineElasticSearch},
},
{
Name: "Should support search with wildcards",
Fn: testSupportWildcards,
Tags: []string{EngineAll},
},
{
Name: "Should not support search with preceding wildcards",
Fn: testNotSupportPrecedingWildcards,
Tags: []string{EngineAll},
},
{
Name: "Should discard a wildcard if it's not placed immediately by text",
Fn: testSearchDiscardWildcardAlone,
Tags: []string{EngineAll},
},
{
Name: "Should support terms with dash",
Fn: testSupportTermsWithDash,
Tags: []string{EngineAll},
Skip: true,
},
{
Name: "Should support terms with underscore",
Fn: testSupportTermsWithUnderscore,
Tags: []string{EngineMySql, EngineElasticSearch},
},
{
Name: "Should search or exclude post using hashtags",
Fn: testSearchOrExcludePostsWithHashtags,
Tags: []string{EngineAll},
},
{
Name: "Should support searching for hashtags surrounded by markdown",
Fn: testSearchHashtagWithMarkdown,
Tags: []string{EngineAll},
},
{
Name: "Should support searching for multiple hashtags",
Fn: testSearchWithMultipleHashtags,
Tags: []string{EngineElasticSearch},
},
{
Name: "Should support searching hashtags with dots",
Fn: testSearchPostsWithDotsInHashtags,
Tags: []string{EngineAll},
},
{
Name: "Should be able to search or exclude messages with hashtags in a case insensitive manner",
Fn: testSearchHashtagCaseInsensitive,
Tags: []string{EngineAll},
},
{
Name: "Should be able to search by hashtags with dashes",
Fn: testSearchHashtagWithDash,
Tags: []string{EngineAll},
},
{
Name: "Should be able to search by hashtags with numbers",
Fn: testSearchHashtagWithNumbers,
Tags: []string{EngineAll},
},
{
Name: "Should be able to search by hashtags with dots",
Fn: testSearchHashtagWithDots,
Tags: []string{EngineAll},
},
{
Name: "Should be able to search by hashtags with underscores",
Fn: testSearchHashtagWithUnderscores,
Tags: []string{EngineAll},
},
{
Name: "Should not return system messages",
Fn: testSearchShouldExcludeSystemMessages,
Tags: []string{EngineAll},
},
{
Name: "Should be able to search matching by mentions",
Fn: testSearchShouldBeAbleToMatchByMentions,
Tags: []string{EngineAll},
},
{
Name: "Should be able to search in deleted/archived channels",
Fn: testSearchInDeletedOrArchivedChannels,
Tags: []string{EngineMySql, EnginePostgres},
},
{
Name: "Should be able to search terms with dashes",
Fn: testSearchTermsWithDashes,
Tags: []string{EngineAll},
Skip: true,
SkipMessage: "Not working",
},
{
Name: "Should be able to search terms with dots",
Fn: testSearchTermsWithDots,
Tags: []string{EnginePostgres, EngineElasticSearch},
},
{
Name: "Should be able to search terms with underscores",
Fn: testSearchTermsWithUnderscores,
Tags: []string{EngineMySql, EngineElasticSearch},
},
{
Name: "Should be able to search posts made by bot accounts",
Fn: testSearchBotAccountsPosts,
Tags: []string{EngineAll},
},
{
Name: "Should be able to combine stemming and wildcards",
Fn: testSupportStemmingAndWildcards,
Tags: []string{EngineElasticSearch},
},
{
Name: "Should support wildcard outside quotes",
Fn: testSupportWildcardOutsideQuotes,
Tags: []string{EngineElasticSearch},
},
{
Name: "Should support hashtags with 3 or more characters",
Fn: testHashtagSearchShouldSupportThreeOrMoreCharacters,
Tags: []string{EngineAll},
},
{
Name: "Should not support slash as character separator",
Fn: testSlashShouldNotBeCharSeparator,
Tags: []string{EngineMySql, EngineElasticSearch},
},
{
Name: "Should be able to search in comments",
Fn: testSupportSearchInComments,
Tags: []string{EngineAll},
},
{
Name: "Should be able to search terms within links",
Fn: testSupportSearchTermsWithinLinks,
Tags: []string{EngineMySql, EngineElasticSearch},
},
{
Name: "Should not return links that are embedded in markdown",
Fn: testShouldNotReturnLinksEmbeddedInMarkdown,
Tags: []string{EnginePostgres, EngineElasticSearch},
},
{
Name: "Should search across teams",
Fn: testSearchAcrossTeams,
Tags: []string{EngineAll},
},
}
func TestSearchPostStore(t *testing.T, s store.Store, testEngine *SearchTestEngine) {
th := &SearchTestHelper{
Store: s,
}
err := th.SetupBasicFixtures()
require.NoError(t, err)
defer th.CleanFixtures()
runTestSearch(t, testEngine, searchPostStoreTests, th)
}
func testSearchPostsIncludingDMs(t *testing.T, th *SearchTestHelper) {
direct, err := th.createDirectChannel(th.Team.Id, "direct", "direct", []*model.User{th.User, th.User2})
require.NoError(t, err)
defer th.deleteChannel(direct)
p1, err := th.createPost(th.User.Id, direct.Id, "dm test", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, direct.Id, "dm other", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "channel test", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{Terms: "test"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
}
func testSearchPostsWithPagination(t *testing.T, th *SearchTestHelper) {
direct, err := th.createDirectChannel(th.Team.Id, "direct", "direct", []*model.User{th.User, th.User2})
require.NoError(t, err)
defer th.deleteChannel(direct)
p1, err := th.createPost(th.User.Id, direct.Id, "dm test", "", model.PostTypeDefault, 10000, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, direct.Id, "dm other", "", model.PostTypeDefault, 20000, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "channel test", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{Terms: "test"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 1)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
results, err = th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 1, 1)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
}
func testSearchReturnPinnedAndUnpinned(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "channel test unpinned", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "channel test pinned", "", model.PostTypeDefault, 0, true)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{Terms: "test"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
}
func testSearchExactPhraseInQuotes(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "channel test 1 2 3", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "channel test 123", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "channel something test 1 2 3", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "channel 1 2 3", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{Terms: "\"channel test 1 2 3\""}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
}
func testSearchEmailAddresses(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "email test@test.com", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "email test2@test.com", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Should search email addresses enclosed by quotes", func(t *testing.T) {
params := &model.SearchParams{Terms: "\"test@test.com\""}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Should search email addresses without quotes", func(t *testing.T) {
params := &model.SearchParams{Terms: "test@test.com"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
}
func testSearchEmailAddressesWithQuotes(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "email test@test.com", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "email test2@test.com", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{Terms: "\"test@test.com\""}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
}
func testSearchMarkdownUnderscores(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "_start middle end_ _another_", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Should search the start inside the markdown underscore", func(t *testing.T) {
params := &model.SearchParams{Terms: "start"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Should search a word in the middle of the markdown underscore", func(t *testing.T) {
params := &model.SearchParams{Terms: "middle"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Should search in the end of the markdown underscore", func(t *testing.T) {
params := &model.SearchParams{Terms: "end"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Should search inside markdown underscore", func(t *testing.T) {
params := &model.SearchParams{Terms: "another"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
}
func testSearchNonLatinWords(t *testing.T, th *SearchTestHelper) {
t.Run("Should be able to search chinese words", func(t *testing.T) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "你好", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "你", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Should search one word", func(t *testing.T) {
params := &model.SearchParams{Terms: "你"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
t.Run("Should search two words", func(t *testing.T) {
params := &model.SearchParams{Terms: "你好"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Should search with wildcard", func(t *testing.T) {
params := &model.SearchParams{Terms: "你*"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
})
t.Run("Should be able to search cyrillic words", func(t *testing.T) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "слово test", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Should search one word", func(t *testing.T) {
params := &model.SearchParams{Terms: "слово"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Should search using wildcard", func(t *testing.T) {
params := &model.SearchParams{Terms: "слов*"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
})
t.Run("Should be able to search japanese words", func(t *testing.T) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "本", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "本木", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Should search one word", func(t *testing.T) {
params := &model.SearchParams{Terms: "本"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
t.Run("Should search two words", func(t *testing.T) {
params := &model.SearchParams{Terms: "本木"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
t.Run("Should search with wildcard", func(t *testing.T) {
params := &model.SearchParams{Terms: "本*"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
})
t.Run("Should be able to search korean words", func(t *testing.T) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "불", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "불다", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Should search one word", func(t *testing.T) {
params := &model.SearchParams{Terms: "불"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Should search two words", func(t *testing.T) {
params := &model.SearchParams{Terms: "불다"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
t.Run("Should search with wildcard", func(t *testing.T) {
params := &model.SearchParams{Terms: "불*"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
})
}
func testSearchAlternativeSpellings(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "Straße test", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "Strasse test", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{Terms: "Straße"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
params = &model.SearchParams{Terms: "Strasse"}
results, err = th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
}
func testSearchAlternativeSpellingsAccents(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "café", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "café", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{Terms: "café"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
params = &model.SearchParams{Terms: "café"}
results, err = th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
params = &model.SearchParams{Terms: "cafe"}
results, err = th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 0)
}
func testSearchOrExcludePostsBySpecificUser(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelPrivate.Id, "test fromuser", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User2.Id, th.ChannelPrivate.Id, "test fromuser 2", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
defer th.deleteUserPosts(th.User2.Id)
params := &model.SearchParams{
Terms: "fromuser",
FromUsers: []string{th.User.Id},
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
}
func testSearchOrExcludePostsInChannel(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "test fromuser", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User2.Id, th.ChannelPrivate.Id, "test fromuser 2", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
defer th.deleteUserPosts(th.User2.Id)
params := &model.SearchParams{
Terms: "fromuser",
InChannels: []string{th.ChannelBasic.Id},
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
}
func testSearchOrExcludePostsInDMGM(t *testing.T, th *SearchTestHelper) {
direct, err := th.createDirectChannel(th.Team.Id, "direct", "direct", []*model.User{th.User, th.User2})
require.NoError(t, err)
defer th.deleteChannel(direct)
group, err := th.createGroupChannel(th.Team.Id, "test group", []*model.User{th.User, th.User2})
require.NoError(t, err)
defer th.deleteChannel(group)
p1, err := th.createPost(th.User.Id, direct.Id, "test fromuser", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User2.Id, group.Id, "test fromuser 2", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
defer th.deleteUserPosts(th.User2.Id)
t.Run("Should be able to search in both DM and GM channels", func(t *testing.T) {
params := &model.SearchParams{
Terms: "fromuser",
InChannels: []string{direct.Id, group.Id},
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
t.Run("Should be able to search only in DM channel", func(t *testing.T) {
params := &model.SearchParams{
Terms: "fromuser",
InChannels: []string{direct.Id},
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Should be able to search only in GM channel", func(t *testing.T) {
params := &model.SearchParams{
Terms: "fromuser",
InChannels: []string{group.Id},
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
}
func testFilterMessagesInSpecificDate(t *testing.T, th *SearchTestHelper) {
creationDate := model.GetMillisForTime(time.Date(2020, 03, 22, 12, 0, 0, 0, time.UTC))
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "test in specific date", "", model.PostTypeDefault, creationDate, false)
require.NoError(t, err)
creationDate2 := model.GetMillisForTime(time.Date(2020, 03, 23, 0, 0, 0, 0, time.UTC))
p2, err := th.createPost(th.User.Id, th.ChannelPrivate.Id, "test in the present", "", model.PostTypeDefault, creationDate2, false)
require.NoError(t, err)
creationDate3 := model.GetMillisForTime(time.Date(2020, 03, 21, 23, 59, 59, 0, time.UTC))
p3, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "test in the present", "", model.PostTypeDefault, creationDate3, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Should be able to search posts on date", func(t *testing.T) {
params := &model.SearchParams{
Terms: "test",
OnDate: "2020-03-22",
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Should be able to exclude posts on date", func(t *testing.T) {
params := &model.SearchParams{
Terms: "test",
ExcludedDate: "2020-03-22",
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
th.checkPostInSearchResults(t, p3.Id, results.Posts)
})
}
func testFilterMessagesBeforeSpecificDate(t *testing.T, th *SearchTestHelper) {
creationDate := model.GetMillisForTime(time.Date(2020, 03, 01, 12, 0, 0, 0, time.UTC))
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "test in specific date", "", model.PostTypeDefault, creationDate, false)
require.NoError(t, err)
creationDate2 := model.GetMillisForTime(time.Date(2020, 03, 22, 23, 59, 59, 0, time.UTC))
p2, err := th.createPost(th.User.Id, th.ChannelPrivate.Id, "test in specific date 2", "", model.PostTypeDefault, creationDate2, false)
require.NoError(t, err)
creationDate3 := model.GetMillisForTime(time.Date(2020, 03, 26, 16, 55, 0, 0, time.UTC))
p3, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "test in the present", "", model.PostTypeDefault, creationDate3, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Should be able to search posts before a date", func(t *testing.T) {
params := &model.SearchParams{
Terms: "test",
BeforeDate: "2020-03-23",
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
t.Run("Should be able to exclude posts before a date", func(t *testing.T) {
params := &model.SearchParams{
Terms: "test",
ExcludedBeforeDate: "2020-03-23",
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p3.Id, results.Posts)
})
}
func testFilterMessagesAfterSpecificDate(t *testing.T, th *SearchTestHelper) {
creationDate := model.GetMillisForTime(time.Date(2020, 03, 01, 12, 0, 0, 0, time.UTC))
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "test in specific date", "", model.PostTypeDefault, creationDate, false)
require.NoError(t, err)
creationDate2 := model.GetMillisForTime(time.Date(2020, 03, 22, 23, 59, 59, 0, time.UTC))
p2, err := th.createPost(th.User.Id, th.ChannelPrivate.Id, "test in specific date 2", "", model.PostTypeDefault, creationDate2, false)
require.NoError(t, err)
creationDate3 := model.GetMillisForTime(time.Date(2020, 03, 26, 16, 55, 0, 0, time.UTC))
p3, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "test in the present", "", model.PostTypeDefault, creationDate3, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Should be able to search posts after a date", func(t *testing.T) {
params := &model.SearchParams{
Terms: "test",
AfterDate: "2020-03-23",
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p3.Id, results.Posts)
})
t.Run("Should be able to exclude posts after a date", func(t *testing.T) {
params := &model.SearchParams{
Terms: "test",
ExcludedAfterDate: "2020-03-23",
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
}
func testFilterMessagesWithATerm(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "one two three", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelPrivate.Id, "one four five six", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "one seven eight nine", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Should exclude terms", func(t *testing.T) {
params := &model.SearchParams{
Terms: "one",
ExcludedTerms: "five eight",
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Should exclude quoted terms", func(t *testing.T) {
params := &model.SearchParams{
Terms: "one",
ExcludedTerms: "\"eight nine\"",
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
}
func testSearchUsingBooleanOperators(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "one two three message", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "two messages", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "another message", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Should search posts using OR operator", func(t *testing.T) {
params := &model.SearchParams{
Terms: "one two",
OrTerms: true,
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
t.Run("Should search posts using AND operator", func(t *testing.T) {
params := &model.SearchParams{
Terms: "one two",
OrTerms: false,
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
}
func testSearchUsingCombinedFilters(t *testing.T, th *SearchTestHelper) {
creationDate := model.GetMillisForTime(time.Date(2020, 03, 01, 12, 0, 0, 0, time.UTC))
p1, err := th.createPost(th.User.Id, th.ChannelPrivate.Id, "one two three message", "", model.PostTypeDefault, creationDate, false)
require.NoError(t, err)
creationDate2 := model.GetMillisForTime(time.Date(2020, 03, 10, 12, 0, 0, 0, time.UTC))
p2, err := th.createPost(th.User2.Id, th.ChannelPrivate.Id, "two messages", "", model.PostTypeDefault, creationDate2, false)
require.NoError(t, err)
creationDate3 := model.GetMillisForTime(time.Date(2020, 03, 20, 12, 0, 0, 0, time.UTC))
p3, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "two another message", "", model.PostTypeDefault, creationDate3, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
defer th.deleteUserPosts(th.User2.Id)
t.Run("Should search combining from user and in channel filters", func(t *testing.T) {
params := &model.SearchParams{
Terms: "two",
FromUsers: []string{th.User2.Id},
InChannels: []string{th.ChannelPrivate.Id},
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
t.Run("Should search combining excluding users and in channel filters", func(t *testing.T) {
params := &model.SearchParams{
Terms: "two",
ExcludedUsers: []string{th.User2.Id},
InChannels: []string{th.ChannelPrivate.Id},
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Should search combining excluding dates and in channel filters", func(t *testing.T) {
params := &model.SearchParams{
Terms: "two",
ExcludedBeforeDate: "2020-03-09",
ExcludedAfterDate: "2020-03-11",
InChannels: []string{th.ChannelPrivate.Id},
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
t.Run("Should search combining excluding dates and in channel filters", func(t *testing.T) {
params := &model.SearchParams{
Terms: "two",
AfterDate: "2020-03-11",
ExcludedChannels: []string{th.ChannelPrivate.Id},
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p3.Id, results.Posts)
})
}
func testSearchIgnoringStopWords(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "the search for a bunch of stop words", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "the objective is to avoid a bunch of stop words", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p3, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "in the a on to where you", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p4, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "where is the car?", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Should avoid stop word 'the'", func(t *testing.T) {
params := &model.SearchParams{
Terms: "the search",
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Should avoid stop word 'a'", func(t *testing.T) {
params := &model.SearchParams{
Terms: "a avoid",
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
t.Run("Should avoid stop word 'in'", func(t *testing.T) {
params := &model.SearchParams{
Terms: "in where you",
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p3.Id, results.Posts)
})
t.Run("Should avoid stop words 'where', 'is' and 'the'", func(t *testing.T) {
results, err := th.Store.Post().Search(th.Team.Id, th.User.Id, &model.SearchParams{Terms: "where is the car"})
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p4.Id, results.Posts)
})
t.Run("Should remove all terms and return empty list", func(t *testing.T) {
results, err := th.Store.Post().Search(th.Team.Id, th.User.Id, &model.SearchParams{Terms: "where is the"})
require.NoError(t, err)
require.Empty(t, results.Posts)
})
}
func testSupportStemming(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "search post", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "searching post", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "another post", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{
Terms: "search",
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
}
func testSupportWildcards(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "search post", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "searching", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "another post", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Simple wildcard-only search", func(t *testing.T) {
params := &model.SearchParams{
Terms: "search*",
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
t.Run("Wildcard search with another term placed after", func(t *testing.T) {
params := &model.SearchParams{
Terms: "sear* post",
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
}
func testNotSupportPrecedingWildcards(t *testing.T, th *SearchTestHelper) {
_, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "search post", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "searching post", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "another post", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{
Terms: "*earch",
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 0)
}
func testSearchDiscardWildcardAlone(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "qwerty", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "qwertyjkl", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{
Terms: "qwerty *",
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
}
func testSupportTermsWithDash(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "search term-with-dash", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "searching term with dash", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Should search terms with dash", func(t *testing.T) {
params := &model.SearchParams{
Terms: "term-with-dash",
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Should search terms with dash using quotes", func(t *testing.T) {
params := &model.SearchParams{
Terms: "\"term-with-dash\"",
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
}
func testSupportTermsWithUnderscore(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "search term_with_underscore", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "searching term with underscore", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Should search terms with underscore", func(t *testing.T) {
params := &model.SearchParams{
Terms: "term_with_underscore",
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Should search terms with underscore using quotes", func(t *testing.T) {
params := &model.SearchParams{
Terms: "\"term_with_underscore\"",
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
}
func testSearchOrExcludePostsWithHashtags(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "search post with #hashtag", "#hashtag", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "searching term with hashtag", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p3, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "searching term with", "#hashtag", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Should search terms with hashtags", func(t *testing.T) {
params := &model.SearchParams{
Terms: "#hashtag",
IsHashtag: true,
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p3.Id, results.Posts)
})
t.Run("Should search hashtag terms without hashtag option", func(t *testing.T) {
params := &model.SearchParams{
Terms: "#hashtag",
IsHashtag: false,
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
}
func testSearchHashtagWithMarkdown(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "searching hashtag #hashtag", "#hashtag", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "searching term with `#hashtag`", "#hashtag", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p3, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "searching term with **#hashtag**", "#hashtag", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p4, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "searching term with ~~#hashtag~~", "#hashtag", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p5, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "searching term with _#hashtag_", "#hashtag", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{
Terms: "#hashtag",
IsHashtag: true,
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 5)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
th.checkPostInSearchResults(t, p3.Id, results.Posts)
th.checkPostInSearchResults(t, p4.Id, results.Posts)
th.checkPostInSearchResults(t, p5.Id, results.Posts)
}
func testSearchWithMultipleHashtags(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "searching hashtag #hashtag", "#hashtwo #hashone", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "searching term with `#hashtag`", "#hashtwo #hashthree", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Should search posts with multiple hashtags", func(t *testing.T) {
params := &model.SearchParams{
Terms: "#hashone #hashtwo",
IsHashtag: true,
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Should search posts with multiple hashtags using OR", func(t *testing.T) {
params := &model.SearchParams{
Terms: "#hashone #hashtwo",
IsHashtag: true,
OrTerms: true,
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
}
func testSearchPostsWithDotsInHashtags(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "searching hashtag #hashtag.dot", "#hashtag.dot", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{
Terms: "#hashtag.dot",
IsHashtag: true,
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
}
func testSearchHashtagCaseInsensitive(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "searching hashtag #HaShTaG", "#HaShTaG", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "searching hashtag #hashtag", "#hashtag", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p3, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "searching hashtag #HASHTAG", "#HASHTAG", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Lower case hashtag", func(t *testing.T) {
params := &model.SearchParams{
Terms: "#hashtag",
IsHashtag: true,
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 3)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
th.checkPostInSearchResults(t, p3.Id, results.Posts)
})
t.Run("Upper case hashtag", func(t *testing.T) {
params := &model.SearchParams{
Terms: "#HASHTAG",
IsHashtag: true,
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 3)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
th.checkPostInSearchResults(t, p3.Id, results.Posts)
})
t.Run("Mixed case hashtag", func(t *testing.T) {
params := &model.SearchParams{
Terms: "#HaShTaG",
IsHashtag: true,
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 3)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
th.checkPostInSearchResults(t, p3.Id, results.Posts)
})
}
func testSearchHashtagWithDash(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "searching hashtag #hashtag-test", "#hashtag-test", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "searching hashtag #hashtagtest", "#hashtagtest", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{
Terms: "#hashtag-test",
IsHashtag: true,
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
}
func testSearchHashtagWithNumbers(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "searching hashtag #h4sht4g", "#h4sht4g", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "searching hashtag #hashtag", "#hashtag", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{
Terms: "#h4sht4g",
IsHashtag: true,
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
}
func testSearchHashtagWithDots(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "searching hashtag #hashtag.test", "#hashtag.test", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "searching hashtag #hashtagtest", "#hashtagtest", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{
Terms: "#hashtag.test",
IsHashtag: true,
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
}
func testSearchHashtagWithUnderscores(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "searching hashtag #hashtag_test", "#hashtag_test", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "searching hashtag #hashtagtest", "#hashtagtest", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{
Terms: "#hashtag_test",
IsHashtag: true,
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
}
func testSearchShouldExcludeSystemMessages(t *testing.T, th *SearchTestHelper) {
_, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "test system message one", "", model.PostTypeJoinChannel, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "test system message two", "", model.PostTypeLeaveChannel, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "test system message three", "", model.PostTypeLeaveTeam, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "test system message four", "", model.PostTypeAddToChannel, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "test system message five", "", model.PostTypeAddToTeam, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "test system message six", "", model.PostTypeHeaderChange, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{Terms: "test system"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 0)
}
func testSearchShouldBeAbleToMatchByMentions(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "test system @testuser", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "test system testuser", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p3, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "test system #testuser", "#testuser", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{Terms: "@testuser"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 3)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
th.checkPostInSearchResults(t, p3.Id, results.Posts)
}
func testSearchInDeletedOrArchivedChannels(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelDeleted.Id, "message in deleted channel", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "message in regular channel", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p3, err := th.createPost(th.User.Id, th.ChannelPrivate.Id, "message in private channel", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Doesn't include posts in deleted channels", func(t *testing.T) {
params := &model.SearchParams{Terms: "message", IncludeDeletedChannels: false}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
th.checkPostInSearchResults(t, p3.Id, results.Posts)
})
t.Run("Include posts in deleted channels", func(t *testing.T) {
params := &model.SearchParams{Terms: "message", IncludeDeletedChannels: true}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 3)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
th.checkPostInSearchResults(t, p3.Id, results.Posts)
})
t.Run("Include posts in deleted channels using multiple terms", func(t *testing.T) {
params := &model.SearchParams{Terms: "message channel", IncludeDeletedChannels: true}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 3)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
th.checkPostInSearchResults(t, p3.Id, results.Posts)
})
t.Run("Include posts in deleted channels using multiple OR terms", func(t *testing.T) {
params := &model.SearchParams{
Terms: "message channel",
IncludeDeletedChannels: true,
OrTerms: true,
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 3)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
th.checkPostInSearchResults(t, p3.Id, results.Posts)
})
t.Run("All IncludeDeletedChannels params should have same value if multiple SearchParams provided", func(t *testing.T) {
params1 := &model.SearchParams{
Terms: "message channel",
IncludeDeletedChannels: true,
}
params2 := &model.SearchParams{
Terms: "#hashtag",
IncludeDeletedChannels: false,
}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params1, params2}, th.User.Id, th.Team.Id, 0, 20)
require.Nil(t, results)
require.Error(t, err)
})
}
func testSearchTermsWithDashes(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "message with-dash-term", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "message with dash term", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Search for terms with dash", func(t *testing.T) {
params := &model.SearchParams{Terms: "with-dash-term"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Search for terms with quoted dash", func(t *testing.T) {
params := &model.SearchParams{Terms: "\"with-dash-term\""}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Search for multiple terms with one having dash", func(t *testing.T) {
params := &model.SearchParams{Terms: "with-dash-term message"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Search for multiple OR terms with one having dash", func(t *testing.T) {
params := &model.SearchParams{Terms: "with-dash-term message", OrTerms: true}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
}
func testSearchTermsWithDots(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "message with.dots.term", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "message with dots term", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Search for terms with dots", func(t *testing.T) {
params := &model.SearchParams{Terms: "with.dots.term"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Search for terms with quoted dots", func(t *testing.T) {
params := &model.SearchParams{Terms: "\"with.dots.term\""}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Search for multiple terms with one having dots", func(t *testing.T) {
params := &model.SearchParams{Terms: "with.dots.term message"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Search for multiple OR terms with one having dots", func(t *testing.T) {
params := &model.SearchParams{Terms: "with.dots.term message", OrTerms: true}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
}
func testSearchTermsWithUnderscores(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "message with_underscores_term", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "message with underscores term", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Search for terms with underscores", func(t *testing.T) {
params := &model.SearchParams{Terms: "with_underscores_term"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Search for terms with quoted underscores", func(t *testing.T) {
params := &model.SearchParams{Terms: "\"with_underscores_term\""}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Search for multiple terms with one having underscores", func(t *testing.T) {
params := &model.SearchParams{Terms: "with_underscores_term message"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
})
t.Run("Search for multiple OR terms with one having underscores", func(t *testing.T) {
params := &model.SearchParams{Terms: "with_underscores_term message", OrTerms: true}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
}
func testSearchBotAccountsPosts(t *testing.T, th *SearchTestHelper) {
bot, err := th.createBot("testbot", "Test Bot", th.User.Id)
require.NoError(t, err)
defer th.deleteBotUser(bot.UserId)
err = th.addUserToTeams(model.UserFromBot(bot), []string{th.Team.Id})
require.NoError(t, err)
p1, err := th.createPost(bot.UserId, th.ChannelBasic.Id, "bot test message", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(bot.UserId, th.ChannelPrivate.Id, "bot test message in private", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(bot.UserId)
params := &model.SearchParams{Terms: "bot"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
}
func testSupportStemmingAndWildcards(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "approve", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelPrivate.Id, "approved", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p3, err := th.createPost(th.User.Id, th.ChannelPrivate.Id, "approvedz", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Should stem appr", func(t *testing.T) {
params := &model.SearchParams{Terms: "appr*"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 3)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
th.checkPostInSearchResults(t, p3.Id, results.Posts)
})
t.Run("Should stem approve", func(t *testing.T) {
params := &model.SearchParams{Terms: "approve*"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p3.Id, results.Posts)
})
}
func testSupportWildcardOutsideQuotes(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "hello world", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p2, err := th.createPost(th.User.Id, th.ChannelPrivate.Id, "hell or heaven", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
t.Run("Should return results without quotes", func(t *testing.T) {
params := &model.SearchParams{Terms: "hell*"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
t.Run("Should return just one result with quotes", func(t *testing.T) {
params := &model.SearchParams{Terms: "\"hell\"*"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p2.Id, results.Posts)
})
}
func testHashtagSearchShouldSupportThreeOrMoreCharacters(t *testing.T, th *SearchTestHelper) {
_, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "one char hashtag #1", "#1", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelPrivate.Id, "two chars hashtag #12", "#12", model.PostTypeDefault, 0, false)
require.NoError(t, err)
p3, err := th.createPost(th.User.Id, th.ChannelPrivate.Id, "three chars hashtag #123", "#123", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{Terms: "#123", IsHashtag: true}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p3.Id, results.Posts)
}
func testSlashShouldNotBeCharSeparator(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "alpha/beta gamma, theta", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{Terms: "gamma"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
params = &model.SearchParams{Terms: "beta"}
results, err = th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
params = &model.SearchParams{Terms: "alpha"}
results, err = th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
}
func testSupportSearchInComments(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "message test@test.com", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
r1, err := th.createReply(th.User.Id, "reply check", "", p1, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{Terms: "reply"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, r1.Id, results.Posts)
}
func testSupportSearchTermsWithinLinks(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "message with link http://www.wikipedia.org/dolphins", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{Terms: "wikipedia"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
}
func testShouldNotReturnLinksEmbeddedInMarkdown(t *testing.T, th *SearchTestHelper) {
_, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "message with link [here](http://www.wikipedia.org/dolphins)", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
params := &model.SearchParams{Terms: "wikipedia"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 0)
}
func testSearchAcrossTeams(t *testing.T, th *SearchTestHelper) {
err := th.addUserToChannels(th.User, []string{th.ChannelAnotherTeam.Id})
require.NoError(t, err)
defer th.Store.Channel().RemoveMember(th.ChannelAnotherTeam.Id, th.User.Id)
_, err = th.createPost(th.User.Id, th.ChannelAnotherTeam.Id, "text to search", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
defer th.deleteUserPosts(th.User.Id)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "text to search", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
params := &model.SearchParams{Terms: "search"}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, "", 0, 20)
require.NoError(t, err)
require.Len(t, results.Posts, 2)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchtest
import (
"testing"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
const (
EngineAll = "all"
EngineMySql = "mysql"
EnginePostgres = "postgres"
EngineElasticSearch = "elasticsearch"
EngineBleve = "bleve"
)
type SearchTestEngine struct {
Driver string
BeforeTest func(*testing.T, store.Store)
AfterTest func(*testing.T, store.Store)
}
type searchTest struct {
Name string
Fn func(*testing.T, *SearchTestHelper)
Tags []string
Skip bool
SkipMessage string
}
func filterTestsByTag(tests []searchTest, tags ...string) []searchTest {
filteredTests := []searchTest{}
for _, test := range tests {
if utils.StringInSlice(EngineAll, test.Tags) {
filteredTests = append(filteredTests, test)
continue
}
for _, tag := range tags {
if utils.StringInSlice(tag, test.Tags) {
filteredTests = append(filteredTests, test)
break
}
}
}
return filteredTests
}
func runTestSearch(t *testing.T, testEngine *SearchTestEngine, tests []searchTest, th *SearchTestHelper) {
if testing.Short() {
t.Skip("Skipping advanced search test")
return
}
filteredTests := filterTestsByTag(tests, testEngine.Driver)
for _, test := range filteredTests {
if test.Skip {
t.Log("SKIPPED: " + test.Name + ". Reason: " + test.SkipMessage)
continue
}
if testEngine.BeforeTest != nil {
testEngine.BeforeTest(t, th.Store)
}
testName := test.Name
testFn := test.Fn
t.Run(testName, func(t *testing.T) { testFn(t, th) })
if testEngine.AfterTest != nil {
testEngine.AfterTest(t, th.Store)
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchtest
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
var searchUserStoreTests = []searchTest{
{
Name: "Should retrieve all users in a channel if the search term is empty",
Fn: testGetAllUsersInChannelWithEmptyTerm,
Tags: []string{EngineAll},
},
{
Name: "Should honor channel restrictions when autocompleting users",
Fn: testHonorChannelRestrictionsAutocompletingUsers,
Tags: []string{EngineAll},
},
{
Name: "Should honor team restrictions when autocompleting users",
Fn: testHonorTeamRestrictionsAutocompletingUsers,
Tags: []string{EngineElasticSearch, EngineBleve},
},
{
Name: "Should return nothing if the user can't access the channels of a given search",
Fn: testShouldReturnNothingWithoutProperAccess,
Tags: []string{EngineAll},
Skip: true,
SkipMessage: "Failing when the ListOfAllowedChannels property is empty",
},
{
Name: "Should autocomplete for user using username",
Fn: testAutocompleteUserByUsername,
Tags: []string{EngineAll},
},
{
Name: "Should autocomplete user searching by first name",
Fn: testAutocompleteUserByFirstName,
Tags: []string{EngineAll},
},
{
Name: "Should autocomplete user searching by last name",
Fn: testAutocompleteUserByLastName,
Tags: []string{EngineAll},
},
{
Name: "Should autocomplete for user using nickname",
Fn: testAutocompleteUserByNickName,
Tags: []string{EngineAll},
},
{
Name: "Should autocomplete for user using email",
Fn: testAutocompleteUserByEmail,
Tags: []string{EngineAll},
Skip: true,
SkipMessage: "Failing for multiple different reasons in the engines",
},
{
Name: "Should be able not to match specific queries with mail",
Fn: testShouldNotMatchSpecificQueriesEmail,
Tags: []string{EngineAll},
},
{
Name: "Should be able to autocomplete a user by part of its username splitted by Dot",
Fn: testAutocompleteUserByUsernameWithDot,
Tags: []string{EngineElasticSearch, EngineBleve},
},
{
Name: "Should be able to autocomplete a user by part of its username splitted by underscore",
Fn: testAutocompleteUserByUsernameWithUnderscore,
Tags: []string{EngineElasticSearch, EngineBleve},
},
{
Name: "Should be able to autocomplete a user by part of its username splitted by hyphen",
Fn: testAutocompleteUserByUsernameWithHyphen,
Tags: []string{EngineElasticSearch, EngineBleve},
},
{
Name: "Should escape the percentage character",
Fn: testShouldEscapePercentageCharacter,
Tags: []string{EngineAll},
},
{
Name: "Should escape the dash character",
Fn: testShouldEscapeUnderscoreCharacter,
Tags: []string{EngineAll},
},
{
Name: "Should be able to search inactive users",
Fn: testShouldBeAbleToSearchInactiveUsers,
Tags: []string{EngineMySql, EnginePostgres, EngineElasticSearch},
},
{
Name: "Should be able to search filtering by role",
Fn: testShouldBeAbleToSearchFilteringByRole,
Tags: []string{EngineMySql, EnginePostgres, EngineElasticSearch},
},
{
Name: "Should ignore leading @ when searching users",
Fn: testShouldIgnoreLeadingAtSymbols,
Tags: []string{EngineAll},
},
{
Name: "Should search users in a case insensitive manner",
Fn: testSearchUsersShouldBeCaseInsensitive,
Tags: []string{EngineAll},
},
{
Name: "Should support one or two character usernames and first/last names in search",
Fn: testSearchOneTwoCharUsernamesAndFirstLastNames,
Tags: []string{EngineAll},
},
{
Name: "Should support Korean characters",
Fn: testShouldSupportKoreanCharacters,
Tags: []string{EngineAll},
},
{
Name: "Should support search with a hyphen at the end of the term",
Fn: testSearchWithHyphenAtTheEndOfTheTerm,
Tags: []string{EngineAll},
},
{
Name: "Should support search all users in a team",
Fn: testSearchUsersInTeam,
Tags: []string{EngineElasticSearch},
},
{
Name: "Should support search users by full name",
Fn: testSearchUsersByFullName,
Tags: []string{EngineAll},
},
{
Name: "Should support search all users in a team with username containing a dot",
Fn: testSearchUsersInTeamUsernameWithDot,
Tags: []string{EngineAll},
},
{
Name: "Should support search all users in a team with username containing a hyphen",
Fn: testSearchUsersInTeamUsernameWithHyphen,
Tags: []string{EngineAll},
},
{
Name: "Should support search all users in a team with username containing a underscore",
Fn: testSearchUsersInTeamUsernameWithUnderscore,
Tags: []string{EngineAll},
},
}
func TestSearchUserStore(t *testing.T, s store.Store, testEngine *SearchTestEngine) {
th := &SearchTestHelper{
Store: s,
}
err := th.SetupBasicFixtures()
require.NoError(t, err)
defer th.CleanFixtures()
runTestSearch(t, testEngine, searchUserStoreTests, th)
}
func testGetAllUsersInChannelWithEmptyTerm(t *testing.T, th *SearchTestHelper) {
options := &model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
}
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
t.Run("Should be able to correctly honor limit when autocompleting", func(t *testing.T) {
result, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
require.NoError(t, err)
require.Len(t, result.InChannel, 1)
require.Len(t, result.OutOfChannel, 1)
})
t.Run("Return all users in team", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
})
t.Run("Return all users in teams even though some of them don't have a team associated", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
userAlternate, err := th.createUser("user-alternate", "user-alternate", "user", "alternate")
require.NoError(t, err)
defer th.deleteUser(userAlternate)
userGuest, err := th.createGuest("user-guest", "user-guest", "user", "guest")
require.NoError(t, err)
defer th.deleteUser(userGuest)
// In case teamId and channelId are empty our current logic goes through Search
users, err := th.Store.User().Search("", "", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User, th.User2, th.UserAnotherTeam,
userAlternate, userGuest}, users)
})
}
func testHonorChannelRestrictionsAutocompletingUsers(t *testing.T, th *SearchTestHelper) {
userAlternate, err := th.createUser("user-alternate", "user-alternate", "user", "alternate")
require.NoError(t, err)
defer th.deleteUser(userAlternate)
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
guest, err := th.createGuest("guest", "guest", "guest", "one")
require.NoError(t, err)
err = th.addUserToTeams(guest, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(guest, []string{th.ChannelBasic.Id})
require.NoError(t, err)
defer th.deleteUser(guest)
t.Run("Autocomplete users with channel restrictions", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
options.ViewRestrictions = &model.ViewUsersRestrictions{Channels: []string{th.ChannelBasic.Id}}
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User, userAlternate, guest}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
t.Run("Autocomplete users with term and channel restrictions", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
options.ViewRestrictions = &model.ViewUsersRestrictions{Channels: []string{th.ChannelBasic.Id}}
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "alt", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
t.Run("Autocomplete users with all channels restricted", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
options.ViewRestrictions = &model.ViewUsersRestrictions{Teams: []string{}, Channels: []string{}}
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
t.Run("Autocomplete users with all channels restricted but with empty team", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
options.ViewRestrictions = &model.ViewUsersRestrictions{Teams: []string{}, Channels: []string{}}
users, err := th.Store.User().AutocompleteUsersInChannel("", th.ChannelBasic.Id, "", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
t.Run("Autocomplete users with empty team and channels restricted", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
options.ViewRestrictions = &model.ViewUsersRestrictions{Channels: []string{th.ChannelBasic.Id}}
// In case teamId and channelId are empty our current logic goes through Search
users, err := th.Store.User().Search("", "", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate, guest, th.User}, users)
})
}
func testHonorTeamRestrictionsAutocompletingUsers(t *testing.T, th *SearchTestHelper) {
t.Run("Should return results for users in the team", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
options.ViewRestrictions = &model.ViewUsersRestrictions{Teams: []string{th.Team.Id}}
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
})
t.Run("Should return empty because we're filtering all the teams", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
options.ViewRestrictions = &model.ViewUsersRestrictions{Teams: []string{}, Channels: []string{}}
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
t.Run("Should return empty when searching in one team and filtering by another", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
options.ViewRestrictions = &model.ViewUsersRestrictions{Teams: []string{th.AnotherTeam.Id}}
users, err := th.Store.User().Search(th.Team.Id, "", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users)
acusers, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, acusers.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, acusers.OutOfChannel)
})
}
func testShouldReturnNothingWithoutProperAccess(t *testing.T, th *SearchTestHelper) {
t.Run("Should return results users for the defined channel in the list", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
options.ListOfAllowedChannels = []string{th.ChannelBasic.Id}
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
t.Run("Should return empty because we're filtering all the channels", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
options.ListOfAllowedChannels = []string{}
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
}
func testAutocompleteUserByUsername(t *testing.T, th *SearchTestHelper) {
userAlternate, err := th.createUser("alternateusername", "alternatenick", "user", "alternate")
require.NoError(t, err)
defer th.deleteUser(userAlternate)
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "basicusername", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
}
func testAutocompleteUserByFirstName(t *testing.T, th *SearchTestHelper) {
userAlternate, err := th.createUser("user-alternate", "user-alternate", "altfirstname", "lastname")
require.NoError(t, err)
defer th.deleteUser(userAlternate)
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
t.Run("Should autocomplete users when the first name is unique", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "altfirstname", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
t.Run("Should autocomplete users for in the channel and out of the channel with the same first name", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "basicfirstname", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
})
}
func testAutocompleteUserByLastName(t *testing.T, th *SearchTestHelper) {
userAlternate, err := th.createUser("user-alternate", "user-alternate", "firstname", "altlastname")
require.NoError(t, err)
defer th.deleteUser(userAlternate)
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
t.Run("Should return results when the last name is unique", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "altlastname", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
t.Run("Should return results for in the channel and out of the channel with the same last name", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "basiclastname", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
})
}
func testAutocompleteUserByNickName(t *testing.T, th *SearchTestHelper) {
userAlternate, err := th.createUser("alternateusername", "alternatenickname", "firstname", "altlastname")
require.NoError(t, err)
defer th.deleteUser(userAlternate)
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
t.Run("Should return results when the nickname is unique", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "alternatenickname", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
t.Run("Should return users that share the same part of the nickname", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "basicnickname", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
})
}
func testAutocompleteUserByEmail(t *testing.T, th *SearchTestHelper) {
userAlternate, err := th.createUser("alternateusername", "alternatenickname", "firstname", "altlastname")
require.NoError(t, err)
userAlternate.Email = "useralt@test.email.com"
_, err = th.Store.User().Update(userAlternate, false)
require.NoError(t, err)
defer th.deleteUser(userAlternate)
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
t.Run("Should autocomplete users when the email is unique", func(t *testing.T) {
options := createDefaultOptions(false, true, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "useralt@test.email.com", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
t.Run("Should autocomplete users that share the same email user prefix", func(t *testing.T) {
options := createDefaultOptions(false, true, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "success_", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
})
t.Run("Should autocomplete users that share the same email domain", func(t *testing.T) {
options := createDefaultOptions(false, true, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "simulator.amazon.com", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
})
t.Run("Should search users when the email is unique", func(t *testing.T) {
options := createDefaultOptions(false, true, false)
users, err := th.Store.User().Search(th.Team.Id, "useralt@test.email.com", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users)
})
t.Run("Should search users that share the same email user prefix", func(t *testing.T) {
options := createDefaultOptions(false, true, false)
users, err := th.Store.User().Search(th.Team.Id, "success_", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users)
})
t.Run("Should search users that share the same email domain", func(t *testing.T) {
options := createDefaultOptions(false, true, false)
users, err := th.Store.User().Search(th.Team.Id, "simulator.amazon.com", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users)
})
}
func testShouldNotMatchSpecificQueriesEmail(t *testing.T, th *SearchTestHelper) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "success_", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
}
func testAutocompleteUserByUsernameWithDot(t *testing.T, th *SearchTestHelper) {
userAlternate, err := th.createUser("alternate.username", "alternatenickname", "firstname", "altlastname")
require.NoError(t, err)
defer th.deleteUser(userAlternate)
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
t.Run("Should return results when searching for the whole username with Dot", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "alternate.username", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
t.Run("Should return results when searching for part of the username including the Dot", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, ".username", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
t.Run("Should return results when searching for part of the username not including the Dot", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "username", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
}
func testAutocompleteUserByUsernameWithUnderscore(t *testing.T, th *SearchTestHelper) {
userAlternate, err := th.createUser("alternate_username", "alternatenickname", "firstname", "altlastname")
require.NoError(t, err)
defer th.deleteUser(userAlternate)
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
t.Run("Should return results when searching for the whole username with underscore", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "alternate_username", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
t.Run("Should return results when searching for part of the username including the underscore", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "_username", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
t.Run("Should return results when searching for part of the username not including the underscore", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "username", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
}
func testAutocompleteUserByUsernameWithHyphen(t *testing.T, th *SearchTestHelper) {
userAlternate, err := th.createUser("alternate-username", "alternatenickname", "firstname", "altlastname")
require.NoError(t, err)
defer th.deleteUser(userAlternate)
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
t.Run("Should return results when searching for the whole username with hyphen", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "alternate-username", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
t.Run("Should return results when searching for part of the username including the hyphen", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "-username", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
t.Run("Should return results when searching for part of the username not including the hyphen", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "username", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
}
func testShouldEscapePercentageCharacter(t *testing.T, th *SearchTestHelper) {
userAlternate, err := th.createUser("alternateusername", "alternate%nickname", "firstname", "altlastname")
require.NoError(t, err)
defer th.deleteUser(userAlternate)
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
t.Run("Should autocomplete users escaping percentage symbol", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "alternate%", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
t.Run("Should search users escaping percentage symbol", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().Search(th.Team.Id, "alternate%", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users)
})
}
func testShouldEscapeUnderscoreCharacter(t *testing.T, th *SearchTestHelper) {
userAlternate, err := th.createUser("alternate_username", "alternatenickname", "firstname", "altlastname")
require.NoError(t, err)
defer th.deleteUser(userAlternate)
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
t.Run("Should autocomplete users escaping underscore symbol", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "alternate_", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
t.Run("Should search users escaping underscore symbol", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().Search(th.Team.Id, "alternate_", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users)
})
}
func testShouldBeAbleToSearchInactiveUsers(t *testing.T, th *SearchTestHelper) {
userAlternate, err := th.createUser("basicusernamealternate", "alternatenickname", "firstname", "altlastname")
require.NoError(t, err)
userAlternate.DeleteAt = model.GetMillis()
_, err = th.Store.User().Update(userAlternate, true)
require.NoError(t, err)
defer th.deleteUser(userAlternate)
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
t.Run("Should autocomplete inactive users if we allow it", func(t *testing.T) {
options := createDefaultOptions(false, false, true)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "basicusername", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User, userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
})
t.Run("Should search inactive users if we allow it", func(t *testing.T) {
options := createDefaultOptions(false, false, true)
users, err := th.Store.User().Search(th.Team.Id, "basicusername", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User, th.User2, userAlternate}, users)
})
t.Run("Shouldn't autocomplete inactive users if we don't allow it", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "basicusername", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
})
t.Run("Shouldn't search inactive users if we don't allow it", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().Search(th.Team.Id, "basicusername", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User, th.User2}, users)
})
}
func testShouldBeAbleToSearchFilteringByRole(t *testing.T, th *SearchTestHelper) {
userAlternate, err := th.createUser("basicusernamealternate", "alternatenickname", "firstname", "altlastname")
require.NoError(t, err)
userAlternate.Roles = "system_admin system_user"
_, err = th.Store.User().Update(userAlternate, true)
require.NoError(t, err)
defer th.deleteUser(userAlternate)
userAlternate2, err := th.createUser("basicusernamealternate2", "alternatenickname2", "firstname2", "altlastname2")
require.NoError(t, err)
userAlternate2.Roles = "system_user"
_, err = th.Store.User().Update(userAlternate2, true)
require.NoError(t, err)
defer th.deleteUser(userAlternate2)
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToTeams(userAlternate2, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
t.Run("Should autocomplete users filtering by roles", func(t *testing.T) {
options := createDefaultOptions(false, false, true)
options.Role = "system_admin"
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
t.Run("Should search users filtering by roles", func(t *testing.T) {
options := createDefaultOptions(false, false, true)
options.Role = "system_admin"
users, err := th.Store.User().Search(th.Team.Id, "", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users)
})
}
func testShouldIgnoreLeadingAtSymbols(t *testing.T, th *SearchTestHelper) {
t.Run("Should autocomplete ignoring the @ symbol at the beginning", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "@basicusername", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
})
t.Run("Should search ignoring the @ symbol at the beginning", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().Search(th.Team.Id, "@basicusername", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User, th.User2}, users)
})
}
func testSearchUsersShouldBeCaseInsensitive(t *testing.T, th *SearchTestHelper) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "BaSiCUsErNaMe", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
}
func testSearchOneTwoCharUsernamesAndFirstLastNames(t *testing.T, th *SearchTestHelper) {
userAlternate, err := th.createUser("ho", "alternatenickname", "zi", "k")
require.NoError(t, err)
defer th.deleteUser(userAlternate)
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
t.Run("Should support two characters in the full name", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "zi", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
t.Run("Should support two characters in the username", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "ho", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
}
func testShouldSupportKoreanCharacters(t *testing.T, th *SearchTestHelper) {
userAlternate, err := th.createUser("alternate-username", "alternate-nickname", "서강준", "안신원")
require.NoError(t, err)
defer th.deleteUser(userAlternate)
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
t.Run("Should support hanja korean characters", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "서강준", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
t.Run("Should support hangul korean characters", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "안신원", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
})
}
func testSearchWithHyphenAtTheEndOfTheTerm(t *testing.T, th *SearchTestHelper) {
userAlternate, err := th.createUser("alternate-username", "alternate-nickname", "altfirst", "altlast")
require.NoError(t, err)
defer th.deleteUser(userAlternate)
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
options := createDefaultOptions(true, false, false)
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "alternate-", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
}
func testSearchUsersInTeam(t *testing.T, th *SearchTestHelper) {
t.Run("Should return all the team users", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().Search(th.Team.Id, "", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User, th.User2}, users)
})
t.Run("Should return all the team users with no team id", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().Search("", "basicusername", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User, th.User2, th.UserAnotherTeam}, users)
})
t.Run("Should return all the team users filtered by username", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().Search(th.Team.Id, "basicusername1", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users)
})
t.Run("Should not return spurious results", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().Search(th.Team.Id, "falseuser", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users)
})
t.Run("Should return all the team users filtered by username and with channel restrictions", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
options.ViewRestrictions = &model.ViewUsersRestrictions{Channels: []string{th.ChannelBasic.Id}}
users, err := th.Store.User().Search(th.Team.Id, "basicusername", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users)
})
t.Run("Should return all the team users filtered by username and with all channel restricted", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
options.ViewRestrictions = &model.ViewUsersRestrictions{Channels: []string{}}
users, err := th.Store.User().Search(th.Team.Id, "basicusername1", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users)
})
t.Run("Should honor the limit when searching users in team", func(t *testing.T) {
optionsWithLimit := &model.UserSearchOptions{
Limit: 1,
}
users, err := th.Store.User().Search(th.Team.Id, "", optionsWithLimit)
require.NoError(t, err)
require.Len(t, users, 1)
})
}
func testSearchUsersInTeamUsernameWithDot(t *testing.T, th *SearchTestHelper) {
userAlternate, err := th.createUser("alternate.username", "altnickname", "altfirst", "altlast")
require.NoError(t, err)
defer th.deleteUser(userAlternate)
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
options := createDefaultOptions(true, false, false)
users, err := th.Store.User().Search(th.Team.Id, "alternate.", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users)
}
func testSearchUsersInTeamUsernameWithHyphen(t *testing.T, th *SearchTestHelper) {
userAlternate, err := th.createUser("alternate-username", "altnickname", "altfirst", "altlast")
require.NoError(t, err)
defer th.deleteUser(userAlternate)
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
options := createDefaultOptions(true, false, false)
users, err := th.Store.User().Search(th.Team.Id, "alternate-", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users)
}
func testSearchUsersInTeamUsernameWithUnderscore(t *testing.T, th *SearchTestHelper) {
userAlternate, err := th.createUser("alternate_username", "altnickname", "altfirst", "altlast")
require.NoError(t, err)
defer th.deleteUser(userAlternate)
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
options := createDefaultOptions(true, false, false)
users, err := th.Store.User().Search(th.Team.Id, "alternate_", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users)
}
func testSearchUsersByFullName(t *testing.T, th *SearchTestHelper) {
t.Run("Should search users by full name", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
users, err := th.Store.User().Search(th.Team.Id, "basicfirstname", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User, th.User2}, users)
})
t.Run("Should search user by full name", func(t *testing.T) {
options := createDefaultOptions(true, false, false)
users, err := th.Store.User().Search(th.Team.Id, "basicfirstname1", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users)
})
t.Run("Should return empty when search by full name and is deactivated", func(t *testing.T) {
options := createDefaultOptions(false, false, false)
users, err := th.Store.User().Search(th.Team.Id, "basicfirstname1", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users)
})
}
func createDefaultOptions(allowFullName, allowEmails, allowInactive bool) *model.UserSearchOptions {
return &model.UserSearchOptions{
AllowFullNames: allowFullName,
AllowEmails: allowEmails,
AllowInactive: allowInactive,
Limit: model.UserSearchDefaultLimit,
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"bytes"
"database/sql/driver"
"fmt"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type jsonArray []string
func (a jsonArray) Value() (driver.Value, error) {
var out bytes.Buffer
if err := out.WriteByte('['); err != nil {
return nil, err
}
for i, item := range a {
if _, err := out.WriteString(strconv.Quote(item)); err != nil {
return nil, err
}
// Skip the last element.
if i < len(a)-1 {
if err := out.WriteByte(','); err != nil {
return nil, err
}
}
}
err := out.WriteByte(']')
return out.Bytes(), err
}
type jsonStringVal string
func (str jsonStringVal) Value() (driver.Value, error) {
return strconv.Quote(string(str)), nil
}
type jsonKeyPath string
func (str jsonKeyPath) Value() (driver.Value, error) {
return "{" + string(str) + "}", nil
}
type TraceOnAdapter struct{}
func (t *TraceOnAdapter) Printf(format string, v ...any) {
originalString := fmt.Sprintf(format, v...)
newString := strings.ReplaceAll(originalString, "\n", " ")
newString = strings.ReplaceAll(newString, "\t", " ")
newString = strings.ReplaceAll(newString, "\"", "")
mlog.Debug(newString)
}
type JSONSerializable interface {
ToJSON() string
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlAuditStore struct {
*SqlStore
}
func newSqlAuditStore(sqlStore *SqlStore) store.AuditStore {
return &SqlAuditStore{sqlStore}
}
func (s SqlAuditStore) Save(audit *model.Audit) error {
audit.Id = model.NewId()
audit.CreateAt = model.GetMillis()
if _, err := s.GetMasterX().NamedExec(`INSERT INTO Audits
(Id, CreateAt, UserId, Action, ExtraInfo, IpAddress, SessionId)
VALUES
(:Id, :CreateAt, :UserId, :Action, :ExtraInfo, :IpAddress, :SessionId)`, audit); err != nil {
return errors.Wrapf(err, "failed to save Audit with userId=%s and action=%s", audit.UserId, audit.Action)
}
return nil
}
func (s SqlAuditStore) Get(userId string, offset int, limit int) (model.Audits, error) {
if limit > 1000 {
return nil, store.NewErrOutOfBounds(limit)
}
query := s.getQueryBuilder().
Select("*").
From("Audits").
OrderBy("CreateAt DESC").
Limit(uint64(limit)).
Offset(uint64(offset))
if userId != "" {
query = query.Where(sq.Eq{"UserId": userId})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "audits_tosql")
}
var audits model.Audits
if err := s.GetReplicaX().Select(&audits, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to get Audit list for userId=%s", userId)
}
return audits, nil
}
func (s SqlAuditStore) PermanentDeleteByUser(userId string) error {
if _, err := s.GetMasterX().Exec("DELETE FROM Audits WHERE UserId = ?", userId); err != nil {
return errors.Wrapf(err, "failed to delete Audit with userId=%s", userId)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
// bot is a subset of the model.Bot type, omitting the model.User fields.
type bot struct {
UserId string `json:"user_id"`
Description string `json:"description"`
OwnerId string `json:"owner_id"`
LastIconUpdate int64 `json:"last_icon_update"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
}
func botFromModel(b *model.Bot) *bot {
return &bot{
UserId: b.UserId,
Description: b.Description,
OwnerId: b.OwnerId,
LastIconUpdate: b.LastIconUpdate,
CreateAt: b.CreateAt,
UpdateAt: b.UpdateAt,
DeleteAt: b.DeleteAt,
}
}
// SqlBotStore is a store for managing bots in the database.
// Bots are otherwise normal users with extra metadata record in the Bots table. The primary key
// for a bot matches the primary key value for corresponding User record.
type SqlBotStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
}
// newSqlBotStore creates an instance of SqlBotStore, registering the table schema in question.
func newSqlBotStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.BotStore {
return &SqlBotStore{
SqlStore: sqlStore,
metrics: metrics,
}
}
// Get fetches the given bot in the database.
func (us SqlBotStore) Get(botUserId string, includeDeleted bool) (*model.Bot, error) {
var excludeDeletedSql = "AND b.DeleteAt = 0"
if includeDeleted {
excludeDeletedSql = ""
}
query := `
SELECT
b.UserId,
u.Username,
u.FirstName AS DisplayName,
b.Description,
b.OwnerId,
COALESCE(b.LastIconUpdate, 0) AS LastIconUpdate,
b.CreateAt,
b.UpdateAt,
b.DeleteAt
FROM
Bots b
JOIN
Users u ON (u.Id = b.UserId)
WHERE
b.UserId = ?
` + excludeDeletedSql + `
`
var bot model.Bot
if err := us.GetReplicaX().Get(&bot, query, botUserId); err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Bot", botUserId)
} else if err != nil {
return nil, errors.Wrapf(err, "selectone: user_id=%s", botUserId)
}
return &bot, nil
}
// GetAll fetches from all bots in the database.
func (us SqlBotStore) GetAll(options *model.BotGetOptions) ([]*model.Bot, error) {
var conditions []string
var conditionsSql string
var additionalJoin string
var args []any
if !options.IncludeDeleted {
conditions = append(conditions, "b.DeleteAt = 0")
}
if options.OwnerId != "" {
conditions = append(conditions, "b.OwnerId = ?")
args = append(args, options.OwnerId)
}
if options.OnlyOrphaned {
additionalJoin = "JOIN Users o ON (o.Id = b.OwnerId)"
conditions = append(conditions, "o.DeleteAt != 0")
}
if len(conditions) > 0 {
conditionsSql = "WHERE " + strings.Join(conditions, " AND ")
}
sql := `
SELECT
b.UserId,
u.Username,
u.FirstName AS DisplayName,
b.Description,
b.OwnerId,
COALESCE(b.LastIconUpdate, 0) AS LastIconUpdate,
b.CreateAt,
b.UpdateAt,
b.DeleteAt
FROM
Bots b
JOIN
Users u ON (u.Id = b.UserId)
` + additionalJoin + `
` + conditionsSql + `
ORDER BY
b.CreateAt ASC,
u.Username ASC
LIMIT
?
OFFSET
?
`
// append limit, offset
args = append(args, options.PerPage, options.Page*options.PerPage)
bots := []*model.Bot{}
if err := us.GetReplicaX().Select(&bots, sql, args...); err != nil {
return nil, errors.Wrap(err, "error selecting all bots")
}
return bots, nil
}
// Save persists a new bot to the database.
// It assumes the corresponding user was saved via the user store.
func (us SqlBotStore) Save(bot *model.Bot) (*model.Bot, error) {
bot = bot.Clone()
bot.PreSave()
if err := bot.IsValid(); err != nil { // TODO: change to return error in v6.
return nil, err
}
if _, err := us.GetMasterX().NamedExec(`INSERT INTO Bots
(UserId, Description, OwnerId, LastIconUpdate, CreateAt, UpdateAt, DeleteAt)
VALUES
(:UserId, :Description, :OwnerId, :LastIconUpdate, :CreateAt, :UpdateAt, :DeleteAt)`, botFromModel(bot)); err != nil {
return nil, errors.Wrapf(err, "insert: user_id=%s", bot.UserId)
}
return bot, nil
}
// Update persists an updated bot to the database.
// It assumes the corresponding user was updated via the user store.
func (us SqlBotStore) Update(bot *model.Bot) (*model.Bot, error) {
bot = bot.Clone()
bot.PreUpdate()
if err := bot.IsValid(); err != nil { // TODO: needs to return error in v6
return nil, err
}
oldBot, err := us.Get(bot.UserId, true)
if err != nil {
return nil, err
}
oldBot.Description = bot.Description
oldBot.OwnerId = bot.OwnerId
oldBot.LastIconUpdate = bot.LastIconUpdate
oldBot.UpdateAt = bot.UpdateAt
oldBot.DeleteAt = bot.DeleteAt
bot = oldBot
res, err := us.GetMasterX().NamedExec(`UPDATE Bots
SET Description=:Description, OwnerId=:OwnerId, LastIconUpdate=:LastIconUpdate,
UpdateAt=:UpdateAt, DeleteAt=:DeleteAt
WHERE UserId=:UserId`, botFromModel(bot))
if err != nil {
return nil, errors.Wrapf(err, "update: user_id=%s", bot.UserId)
}
count, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "error while getting rows_affected")
}
if count > 1 {
return nil, fmt.Errorf("unexpected count while updating bot: count=%d, userId=%s", count, bot.UserId)
}
return bot, nil
}
// PermanentDelete removes the bot from the database altogether.
// If the corresponding user is to be deleted, it must be done via the user store.
func (us SqlBotStore) PermanentDelete(botUserId string) error {
query := "DELETE FROM Bots WHERE UserId = ?"
if _, err := us.GetMasterX().Exec(query, botUserId); err != nil {
return store.NewErrInvalidInput("Bot", "UserId", botUserId).Wrap(err)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SqlChannelMemberHistoryStore struct {
*SqlStore
}
func newSqlChannelMemberHistoryStore(sqlStore *SqlStore) store.ChannelMemberHistoryStore {
return &SqlChannelMemberHistoryStore{
SqlStore: sqlStore,
}
}
func (s SqlChannelMemberHistoryStore) LogJoinEvent(userId string, channelId string, joinTime int64) error {
channelMemberHistory := &model.ChannelMemberHistory{
UserId: userId,
ChannelId: channelId,
JoinTime: joinTime,
}
if _, err := s.GetMasterX().NamedExec(`INSERT INTO ChannelMemberHistory
(UserId, ChannelId, JoinTime)
VALUES
(:UserId, :ChannelId, :JoinTime)`, channelMemberHistory); err != nil {
return errors.Wrapf(err, "LogJoinEvent userId=%s channelId=%s joinTime=%d", userId, channelId, joinTime)
}
return nil
}
func (s SqlChannelMemberHistoryStore) LogLeaveEvent(userId string, channelId string, leaveTime int64) error {
query, params, err := s.getQueryBuilder().
Update("ChannelMemberHistory").
Set("LeaveTime", leaveTime).
Where(sq.And{
sq.Eq{"UserId": userId},
sq.Eq{"ChannelId": channelId},
sq.Eq{"LeaveTime": nil},
}).ToSql()
if err != nil {
return errors.Wrap(err, "channel_member_history_to_sql")
}
sqlResult, err := s.GetMasterX().Exec(query, params...)
if err != nil {
return errors.Wrapf(err, "LogLeaveEvent userId=%s channelId=%s leaveTime=%d", userId, channelId, leaveTime)
}
if rows, err := sqlResult.RowsAffected(); err == nil && rows != 1 {
// there was no join event to update - this is best effort, so no need to raise an error
mlog.Warn("Channel join event for user and channel not found", mlog.String("user", userId), mlog.String("channel", channelId))
}
return nil
}
func (s SqlChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelId string) ([]*model.ChannelMemberHistoryResult, error) {
useChannelMemberHistory, err := s.hasDataAtOrBefore(startTime)
if err != nil {
return nil, errors.Wrapf(err, "hasDataAtOrBefore startTime=%d endTime=%d channelId=%s", startTime, endTime, channelId)
}
if useChannelMemberHistory {
// the export period starts after the ChannelMemberHistory table was first introduced, so we can use the
// data from it for our export
channelMemberHistories, err2 := s.getFromChannelMemberHistoryTable(startTime, endTime, channelId)
if err2 != nil {
return nil, errors.Wrapf(err2, "getFromChannelMemberHistoryTable startTime=%d endTime=%d channelId=%s", startTime, endTime, channelId)
}
return channelMemberHistories, nil
}
// the export period starts before the ChannelMemberHistory table was introduced, so we need to fake the
// data by assuming that anybody who has ever joined the channel in question was present during the export period.
// this may not always be true, but it's better than saying that somebody wasn't there when they were
channelMemberHistories, err := s.getFromChannelMembersTable(startTime, endTime, channelId)
if err != nil {
return nil, errors.Wrapf(err, "getFromChannelMembersTable startTime=%d endTime=%d channelId=%s", startTime, endTime, channelId)
}
return channelMemberHistories, nil
}
func (s SqlChannelMemberHistoryStore) hasDataAtOrBefore(time int64) (bool, error) {
type NullableCountResult struct {
Min sql.NullInt64
}
query, _, err := s.getQueryBuilder().Select("MIN(JoinTime) as Min").From("ChannelMemberHistory").ToSql()
if err != nil {
return false, errors.Wrap(err, "channel_member_history_to_sql")
}
var result NullableCountResult
if err := s.GetReplicaX().Get(&result, query); err != nil {
return false, err
} else if result.Min.Valid {
return result.Min.Int64 <= time, nil
} else {
// if the result was null, there are no rows in the table, so there is no data from before
return false, nil
}
}
func (s SqlChannelMemberHistoryStore) getFromChannelMemberHistoryTable(startTime int64, endTime int64, channelId string) ([]*model.ChannelMemberHistoryResult, error) {
query, args, err := s.getQueryBuilder().
Select(`cmh.*, u.Email AS "Email", u.Username, Bots.UserId IS NOT NULL AS IsBot, u.DeleteAt AS UserDeleteAt`).
From("ChannelMemberHistory cmh").
Join("Users u ON cmh.UserId = u.Id").
LeftJoin("Bots ON Bots.UserId = u.Id").
Where(sq.And{
sq.Eq{"cmh.ChannelId": channelId},
sq.LtOrEq{"cmh.JoinTime": endTime},
sq.Or{
sq.Eq{"cmh.LeaveTime": nil},
sq.GtOrEq{"cmh.LeaveTime": startTime},
},
}).
OrderBy("cmh.JoinTime ASC").ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_member_history_to_sql")
}
histories := []*model.ChannelMemberHistoryResult{}
if err := s.GetReplicaX().Select(&histories, query, args...); err != nil {
return nil, err
}
return histories, nil
}
func (s SqlChannelMemberHistoryStore) getFromChannelMembersTable(startTime int64, endTime int64, channelId string) ([]*model.ChannelMemberHistoryResult, error) {
query, args, err := s.getQueryBuilder().
Select(`ch.ChannelId, ch.UserId, u.Email AS "Email", u.Username, Bots.UserId IS NOT NULL AS IsBot, u.DeleteAt AS UserDeleteAt`).
Distinct().
From("ChannelMembers ch").
Join("Users u ON ch.UserId = u.id").
LeftJoin("Bots ON Bots.UserId = u.id").
Where(sq.Eq{"ch.ChannelId": channelId}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_member_history_to_sql")
}
histories := []*model.ChannelMemberHistoryResult{}
if err := s.GetReplicaX().Select(&histories, query, args...); err != nil {
return nil, err
}
// we have to fill in the join/leave times, because that data doesn't exist in the channel members table
for _, channelMemberHistory := range histories {
channelMemberHistory.JoinTime = startTime
channelMemberHistory.LeaveTime = model.NewInt64(endTime)
}
return histories, nil
}
// PermanentDeleteBatchForRetentionPolicies deletes a batch of records which are affected by
// the global or a granular retention policy.
// See `genericPermanentDeleteBatchForRetentionPolicies` for details.
func (s SqlChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
builder := s.getQueryBuilder().
Select("ChannelMemberHistory.ChannelId, ChannelMemberHistory.UserId, ChannelMemberHistory.JoinTime").
From("ChannelMemberHistory")
return genericPermanentDeleteBatchForRetentionPolicies(RetentionPolicyBatchDeletionInfo{
BaseBuilder: builder,
Table: "ChannelMemberHistory",
TimeColumn: "LeaveTime",
PrimaryKeys: []string{"ChannelId", "UserId", "JoinTime"},
ChannelIDTable: "ChannelMemberHistory",
NowMillis: now,
GlobalPolicyEndTime: globalPolicyEndTime,
Limit: limit,
}, s.SqlStore, cursor)
}
// DeleteOrphanedRows removes entries from ChannelMemberHistory when a corresponding channel no longer exists.
func (s SqlChannelMemberHistoryStore) DeleteOrphanedRows(limit int) (deleted int64, err error) {
// We need the extra level of nesting to deal with MySQL's locking
const query = `
DELETE FROM ChannelMemberHistory WHERE (ChannelId, UserId, JoinTime) IN (
SELECT * FROM (
SELECT ChannelId, UserId, JoinTime FROM ChannelMemberHistory
LEFT JOIN Channels ON ChannelMemberHistory.ChannelId = Channels.Id
WHERE Channels.Id IS NULL
LIMIT ?
) AS A
)`
result, err := s.GetMasterX().Exec(query, limit)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
func (s SqlChannelMemberHistoryStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
var (
query string
args []any
err error
)
if s.DriverName() == model.DatabaseDriverPostgres {
var innerSelect string
innerSelect, args, err = s.getQueryBuilder().
Select("ctid").
From("ChannelMemberHistory").
Where(sq.And{
sq.NotEq{"LeaveTime": nil},
sq.LtOrEq{"LeaveTime": endTime},
}).Limit(uint64(limit)).
ToSql()
if err != nil {
return 0, errors.Wrap(err, "channel_member_history_to_sql")
}
query, _, err = s.getQueryBuilder().
Delete("ChannelMemberHistory").
Where(fmt.Sprintf(
"ctid IN (%s)", innerSelect,
)).ToSql()
} else {
query, args, err = s.getQueryBuilder().
Delete("ChannelMemberHistory").
Where(sq.And{
sq.NotEq{"LeaveTime": nil},
sq.LtOrEq{"LeaveTime": endTime},
}).
Limit(uint64(limit)).ToSql()
}
if err != nil {
return 0, errors.Wrap(err, "channel_member_history_to_sql")
}
sqlResult, err := s.GetMasterX().Exec(query, args...)
if err != nil {
return 0, errors.Wrapf(err, "PermanentDeleteBatch endTime=%d limit=%d", endTime, limit)
}
rowsAffected, err := sqlResult.RowsAffected()
if err != nil {
return 0, errors.Wrapf(err, "PermanentDeleteBatch endTime=%d limit=%d", endTime, limit)
}
return rowsAffected, nil
}
// GetChannelsLeftSince returns list of channels that the user has left after a given time,
// but has not rejoined again.
func (s SqlChannelMemberHistoryStore) GetChannelsLeftSince(userID string, since int64) ([]string, error) {
query, params, err := s.getQueryBuilder().
Select("ChannelId").
From("ChannelMemberHistory").
GroupBy("ChannelId").
Where(sq.Eq{"UserId": userID}).
Having("MAX(LeaveTime) > MAX(JoinTime) AND MAX(LeaveTime) IS NOT NULL AND MAX(LeaveTime) >= ?", since).ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_member_history_to_sql")
}
channelIds := []string{}
err = s.GetReplicaX().Select(&channelIds, query, params...)
if err != nil {
return nil, errors.Wrapf(err, "GetChannelsLeftSince userId=%s since=%d", userID, since)
}
return channelIds, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
"fmt"
"sort"
"strconv"
"strings"
"time"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/cache"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
AllChannelMembersForUserCacheSize = model.SessionCacheSize
AllChannelMembersForUserCacheDuration = 15 * time.Minute // 15 mins
AllChannelMembersNotifyPropsForChannelCacheSize = model.SessionCacheSize
AllChannelMembersNotifyPropsForChannelCacheDuration = 30 * time.Minute // 30 mins
ChannelCacheDuration = 15 * time.Minute // 15 mins
)
type SqlChannelStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
// prepared query builders for use in multiple methods
channelMembersForTeamWithSchemeSelectQuery sq.SelectBuilder
}
type channelMember struct {
ChannelId string
UserId string
Roles string
LastViewedAt int64
MsgCount int64
MentionCount int64
UrgentMentionCount int64
NotifyProps model.StringMap
LastUpdateAt int64
SchemeUser sql.NullBool
SchemeAdmin sql.NullBool
SchemeGuest sql.NullBool
MentionCountRoot int64
MsgCountRoot int64
}
func NewMapFromChannelMemberModel(cm *model.ChannelMember) map[string]any {
return map[string]any{
"ChannelId": cm.ChannelId,
"UserId": cm.UserId,
"Roles": cm.ExplicitRoles,
"LastViewedAt": cm.LastViewedAt,
"MsgCount": cm.MsgCount,
"MentionCount": cm.MentionCount,
"MentionCountRoot": cm.MentionCountRoot,
"UrgentMentionCount": cm.UrgentMentionCount,
"MsgCountRoot": cm.MsgCountRoot,
"NotifyProps": cm.NotifyProps,
"LastUpdateAt": cm.LastUpdateAt,
"SchemeGuest": sql.NullBool{Valid: true, Bool: cm.SchemeGuest},
"SchemeUser": sql.NullBool{Valid: true, Bool: cm.SchemeUser},
"SchemeAdmin": sql.NullBool{Valid: true, Bool: cm.SchemeAdmin},
}
}
type channelMemberWithSchemeRoles struct {
ChannelId string
UserId string
Roles string
LastViewedAt int64
MsgCount int64
MentionCount int64
MentionCountRoot int64
UrgentMentionCount int64
NotifyProps model.StringMap
LastUpdateAt int64
SchemeGuest sql.NullBool
SchemeUser sql.NullBool
SchemeAdmin sql.NullBool
TeamSchemeDefaultGuestRole sql.NullString
TeamSchemeDefaultUserRole sql.NullString
TeamSchemeDefaultAdminRole sql.NullString
ChannelSchemeDefaultGuestRole sql.NullString
ChannelSchemeDefaultUserRole sql.NullString
ChannelSchemeDefaultAdminRole sql.NullString
MsgCountRoot int64
}
type channelMemberWithTeamWithSchemeRoles struct {
channelMemberWithSchemeRoles
TeamDisplayName string
TeamName string
TeamUpdateAt int64
}
type channelMemberWithTeamWithSchemeRolesList []channelMemberWithTeamWithSchemeRoles
func channelMemberSliceColumns() []string {
return []string{"ChannelId", "UserId", "Roles", "LastViewedAt", "MsgCount", "MsgCountRoot", "MentionCount", "MentionCountRoot", "UrgentMentionCount", "NotifyProps", "LastUpdateAt", "SchemeUser", "SchemeAdmin", "SchemeGuest"}
}
func channelMemberToSlice(member *model.ChannelMember) []any {
resultSlice := []any{}
resultSlice = append(resultSlice, member.ChannelId)
resultSlice = append(resultSlice, member.UserId)
resultSlice = append(resultSlice, member.ExplicitRoles)
resultSlice = append(resultSlice, member.LastViewedAt)
resultSlice = append(resultSlice, member.MsgCount)
resultSlice = append(resultSlice, member.MsgCountRoot)
resultSlice = append(resultSlice, member.MentionCount)
resultSlice = append(resultSlice, member.MentionCountRoot)
resultSlice = append(resultSlice, member.UrgentMentionCount)
resultSlice = append(resultSlice, model.MapToJSON(member.NotifyProps))
resultSlice = append(resultSlice, member.LastUpdateAt)
resultSlice = append(resultSlice, member.SchemeUser)
resultSlice = append(resultSlice, member.SchemeAdmin)
resultSlice = append(resultSlice, member.SchemeGuest)
return resultSlice
}
type channelMemberWithSchemeRolesList []channelMemberWithSchemeRoles
func getChannelRoles(schemeGuest, schemeUser, schemeAdmin bool, defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole, defaultChannelGuestRole, defaultChannelUserRole, defaultChannelAdminRole string,
roles []string) rolesInfo {
result := rolesInfo{
roles: []string{},
explicitRoles: []string{},
schemeGuest: schemeGuest,
schemeUser: schemeUser,
schemeAdmin: schemeAdmin,
}
// Identify any scheme derived roles that are in "Roles" field due to not yet being migrated, and exclude
// them from ExplicitRoles field.
for _, role := range roles {
switch role {
case model.ChannelGuestRoleId:
result.schemeGuest = true
case model.ChannelUserRoleId:
result.schemeUser = true
case model.ChannelAdminRoleId:
result.schemeAdmin = true
default:
result.explicitRoles = append(result.explicitRoles, role)
result.roles = append(result.roles, role)
}
}
// Add any scheme derived roles that are not in the Roles field due to being Implicit from the Scheme, and add
// them to the Roles field for backwards compatibility reasons.
var schemeImpliedRoles []string
if result.schemeGuest {
if defaultChannelGuestRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultChannelGuestRole)
} else if defaultTeamGuestRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultTeamGuestRole)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.ChannelGuestRoleId)
}
}
if result.schemeUser {
if defaultChannelUserRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultChannelUserRole)
} else if defaultTeamUserRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultTeamUserRole)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.ChannelUserRoleId)
}
}
if result.schemeAdmin {
if defaultChannelAdminRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultChannelAdminRole)
} else if defaultTeamAdminRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultTeamAdminRole)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.ChannelAdminRoleId)
}
}
for _, impliedRole := range schemeImpliedRoles {
alreadyThere := false
for _, role := range result.roles {
if role == impliedRole {
alreadyThere = true
break
}
}
if !alreadyThere {
result.roles = append(result.roles, impliedRole)
}
}
return result
}
func (db channelMemberWithSchemeRoles) ToModel() *model.ChannelMember {
// Identify any system-wide scheme derived roles that are in "Roles" field due to not yet being migrated,
// and exclude them from ExplicitRoles field.
schemeGuest := db.SchemeGuest.Valid && db.SchemeGuest.Bool
schemeUser := db.SchemeUser.Valid && db.SchemeUser.Bool
schemeAdmin := db.SchemeAdmin.Valid && db.SchemeAdmin.Bool
defaultTeamGuestRole := ""
if db.TeamSchemeDefaultGuestRole.Valid {
defaultTeamGuestRole = db.TeamSchemeDefaultGuestRole.String
}
defaultTeamUserRole := ""
if db.TeamSchemeDefaultUserRole.Valid {
defaultTeamUserRole = db.TeamSchemeDefaultUserRole.String
}
defaultTeamAdminRole := ""
if db.TeamSchemeDefaultAdminRole.Valid {
defaultTeamAdminRole = db.TeamSchemeDefaultAdminRole.String
}
defaultChannelGuestRole := ""
if db.ChannelSchemeDefaultGuestRole.Valid {
defaultChannelGuestRole = db.ChannelSchemeDefaultGuestRole.String
}
defaultChannelUserRole := ""
if db.ChannelSchemeDefaultUserRole.Valid {
defaultChannelUserRole = db.ChannelSchemeDefaultUserRole.String
}
defaultChannelAdminRole := ""
if db.ChannelSchemeDefaultAdminRole.Valid {
defaultChannelAdminRole = db.ChannelSchemeDefaultAdminRole.String
}
rolesResult := getChannelRoles(
schemeGuest, schemeUser, schemeAdmin,
defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole,
defaultChannelGuestRole, defaultChannelUserRole, defaultChannelAdminRole,
strings.Fields(db.Roles),
)
return &model.ChannelMember{
ChannelId: db.ChannelId,
UserId: db.UserId,
Roles: strings.Join(rolesResult.roles, " "),
LastViewedAt: db.LastViewedAt,
MsgCount: db.MsgCount,
MsgCountRoot: db.MsgCountRoot,
MentionCount: db.MentionCount,
MentionCountRoot: db.MentionCountRoot,
UrgentMentionCount: db.UrgentMentionCount,
NotifyProps: db.NotifyProps,
LastUpdateAt: db.LastUpdateAt,
SchemeAdmin: rolesResult.schemeAdmin,
SchemeUser: rolesResult.schemeUser,
SchemeGuest: rolesResult.schemeGuest,
ExplicitRoles: strings.Join(rolesResult.explicitRoles, " "),
}
}
// This is almost an entire copy of the above method with team information added.
func (db channelMemberWithTeamWithSchemeRoles) ToModel() *model.ChannelMemberWithTeamData {
// Identify any system-wide scheme derived roles that are in "Roles" field due to not yet being migrated,
// and exclude them from ExplicitRoles field.
schemeGuest := db.SchemeGuest.Valid && db.SchemeGuest.Bool
schemeUser := db.SchemeUser.Valid && db.SchemeUser.Bool
schemeAdmin := db.SchemeAdmin.Valid && db.SchemeAdmin.Bool
defaultTeamGuestRole := ""
if db.TeamSchemeDefaultGuestRole.Valid {
defaultTeamGuestRole = db.TeamSchemeDefaultGuestRole.String
}
defaultTeamUserRole := ""
if db.TeamSchemeDefaultUserRole.Valid {
defaultTeamUserRole = db.TeamSchemeDefaultUserRole.String
}
defaultTeamAdminRole := ""
if db.TeamSchemeDefaultAdminRole.Valid {
defaultTeamAdminRole = db.TeamSchemeDefaultAdminRole.String
}
defaultChannelGuestRole := ""
if db.ChannelSchemeDefaultGuestRole.Valid {
defaultChannelGuestRole = db.ChannelSchemeDefaultGuestRole.String
}
defaultChannelUserRole := ""
if db.ChannelSchemeDefaultUserRole.Valid {
defaultChannelUserRole = db.ChannelSchemeDefaultUserRole.String
}
defaultChannelAdminRole := ""
if db.ChannelSchemeDefaultAdminRole.Valid {
defaultChannelAdminRole = db.ChannelSchemeDefaultAdminRole.String
}
rolesResult := getChannelRoles(
schemeGuest, schemeUser, schemeAdmin,
defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole,
defaultChannelGuestRole, defaultChannelUserRole, defaultChannelAdminRole,
strings.Fields(db.Roles),
)
return &model.ChannelMemberWithTeamData{
ChannelMember: model.ChannelMember{
ChannelId: db.ChannelId,
UserId: db.UserId,
Roles: strings.Join(rolesResult.roles, " "),
LastViewedAt: db.LastViewedAt,
MsgCount: db.MsgCount,
MsgCountRoot: db.MsgCountRoot,
MentionCount: db.MentionCount,
MentionCountRoot: db.MentionCountRoot,
UrgentMentionCount: db.UrgentMentionCount,
NotifyProps: db.NotifyProps,
LastUpdateAt: db.LastUpdateAt,
SchemeAdmin: rolesResult.schemeAdmin,
SchemeUser: rolesResult.schemeUser,
SchemeGuest: rolesResult.schemeGuest,
ExplicitRoles: strings.Join(rolesResult.explicitRoles, " "),
},
TeamName: db.TeamName,
TeamDisplayName: db.TeamDisplayName,
TeamUpdateAt: db.TeamUpdateAt,
}
}
func (db channelMemberWithSchemeRolesList) ToModel() model.ChannelMembers {
cms := model.ChannelMembers{}
for _, cm := range db {
cms = append(cms, *cm.ToModel())
}
return cms
}
func (db channelMemberWithTeamWithSchemeRolesList) ToModel() model.ChannelMembersWithTeamData {
cms := model.ChannelMembersWithTeamData{}
for _, cm := range db {
cms = append(cms, *cm.ToModel())
}
return cms
}
type allChannelMember struct {
ChannelId string
Roles string
SchemeGuest sql.NullBool
SchemeUser sql.NullBool
SchemeAdmin sql.NullBool
TeamSchemeDefaultGuestRole sql.NullString
TeamSchemeDefaultUserRole sql.NullString
TeamSchemeDefaultAdminRole sql.NullString
ChannelSchemeDefaultGuestRole sql.NullString
ChannelSchemeDefaultUserRole sql.NullString
ChannelSchemeDefaultAdminRole sql.NullString
}
type allChannelMembers []allChannelMember
func (db allChannelMember) Process() (string, string) {
roles := strings.Fields(db.Roles)
// Add any scheme derived roles that are not in the Roles field due to being Implicit from the Scheme, and add
// them to the Roles field for backwards compatibility reasons.
var schemeImpliedRoles []string
if db.SchemeGuest.Valid && db.SchemeGuest.Bool {
if db.ChannelSchemeDefaultGuestRole.Valid && db.ChannelSchemeDefaultGuestRole.String != "" {
schemeImpliedRoles = append(schemeImpliedRoles, db.ChannelSchemeDefaultGuestRole.String)
} else if db.TeamSchemeDefaultGuestRole.Valid && db.TeamSchemeDefaultGuestRole.String != "" {
schemeImpliedRoles = append(schemeImpliedRoles, db.TeamSchemeDefaultGuestRole.String)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.ChannelGuestRoleId)
}
}
if db.SchemeUser.Valid && db.SchemeUser.Bool {
if db.ChannelSchemeDefaultUserRole.Valid && db.ChannelSchemeDefaultUserRole.String != "" {
schemeImpliedRoles = append(schemeImpliedRoles, db.ChannelSchemeDefaultUserRole.String)
} else if db.TeamSchemeDefaultUserRole.Valid && db.TeamSchemeDefaultUserRole.String != "" {
schemeImpliedRoles = append(schemeImpliedRoles, db.TeamSchemeDefaultUserRole.String)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.ChannelUserRoleId)
}
}
if db.SchemeAdmin.Valid && db.SchemeAdmin.Bool {
if db.ChannelSchemeDefaultAdminRole.Valid && db.ChannelSchemeDefaultAdminRole.String != "" {
schemeImpliedRoles = append(schemeImpliedRoles, db.ChannelSchemeDefaultAdminRole.String)
} else if db.TeamSchemeDefaultAdminRole.Valid && db.TeamSchemeDefaultAdminRole.String != "" {
schemeImpliedRoles = append(schemeImpliedRoles, db.TeamSchemeDefaultAdminRole.String)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.ChannelAdminRoleId)
}
}
for _, impliedRole := range schemeImpliedRoles {
alreadyThere := false
for _, role := range roles {
if role == impliedRole {
alreadyThere = true
break
}
}
if !alreadyThere {
roles = append(roles, impliedRole)
}
}
return db.ChannelId, strings.Join(roles, " ")
}
func (db allChannelMembers) ToMapStringString() map[string]string {
result := make(map[string]string)
for _, item := range db {
key, value := item.Process()
result[key] = value
}
return result
}
// publicChannel is a subset of the metadata corresponding to public channels only.
type publicChannel struct {
Id string `json:"id"`
DeleteAt int64 `json:"delete_at"`
TeamId string `json:"team_id"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
Header string `json:"header"`
Purpose string `json:"purpose"`
}
var allChannelMembersForUserCache = cache.NewLRU(cache.LRUOptions{
Size: AllChannelMembersForUserCacheSize,
})
var allChannelMembersNotifyPropsForChannelCache = cache.NewLRU(cache.LRUOptions{
Size: AllChannelMembersNotifyPropsForChannelCacheSize,
})
var channelByNameCache = cache.NewLRU(cache.LRUOptions{
Size: model.ChannelCacheSize,
})
func (s SqlChannelStore) ClearMembersForUserCache() {
allChannelMembersForUserCache.Purge()
}
func (s SqlChannelStore) ClearCaches() {
allChannelMembersForUserCache.Purge()
allChannelMembersNotifyPropsForChannelCache.Purge()
channelByNameCache.Purge()
if s.metrics != nil {
s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members for User - Purge")
s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members Notify Props for Channel - Purge")
s.metrics.IncrementMemCacheInvalidationCounter("Channel By Name - Purge")
}
}
func newSqlChannelStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.ChannelStore {
s := &SqlChannelStore{
SqlStore: sqlStore,
metrics: metrics,
}
s.initializeQueries()
return s
}
func (s *SqlChannelStore) initializeQueries() {
s.channelMembersForTeamWithSchemeSelectQuery = s.getQueryBuilder().
Select(
"ChannelMembers.ChannelId",
"ChannelMembers.UserId",
"ChannelMembers.Roles",
"ChannelMembers.LastViewedAt",
"ChannelMembers.MsgCount",
"ChannelMembers.MentionCount",
"ChannelMembers.MentionCountRoot",
"COALESCE(ChannelMembers.UrgentMentionCount, 0) AS UrgentMentionCount",
"ChannelMembers.MsgCountRoot",
"ChannelMembers.NotifyProps",
"ChannelMembers.LastUpdateAt",
"ChannelMembers.SchemeUser",
"ChannelMembers.SchemeAdmin",
"ChannelMembers.SchemeGuest",
"TeamScheme.DefaultChannelGuestRole TeamSchemeDefaultGuestRole",
"TeamScheme.DefaultChannelUserRole TeamSchemeDefaultUserRole",
"TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole",
"ChannelScheme.DefaultChannelGuestRole ChannelSchemeDefaultGuestRole",
"ChannelScheme.DefaultChannelUserRole ChannelSchemeDefaultUserRole",
"ChannelScheme.DefaultChannelAdminRole ChannelSchemeDefaultAdminRole",
).
From("ChannelMembers").
InnerJoin("Channels ON ChannelMembers.ChannelId = Channels.Id").
LeftJoin("Schemes ChannelScheme ON Channels.SchemeId = ChannelScheme.Id").
LeftJoin("Teams ON Channels.TeamId = Teams.Id").
LeftJoin("Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id")
}
func (s SqlChannelStore) upsertPublicChannelT(transaction *sqlxTxWrapper, channel *model.Channel) error {
publicChannel := &publicChannel{
Id: channel.Id,
DeleteAt: channel.DeleteAt,
TeamId: channel.TeamId,
DisplayName: channel.DisplayName,
Name: channel.Name,
Header: channel.Header,
Purpose: channel.Purpose,
}
if channel.Type != model.ChannelTypeOpen {
if _, err := transaction.Exec(`DELETE FROM PublicChannels WHERE Id=?`, publicChannel.Id); err != nil {
return errors.Wrap(err, "failed to delete public channel")
}
return nil
}
vals := map[string]any{
"id": publicChannel.Id,
"deleteat": publicChannel.DeleteAt,
"teamid": publicChannel.TeamId,
"displayname": publicChannel.DisplayName,
"name": publicChannel.Name,
"header": publicChannel.Header,
"purpose": publicChannel.Purpose,
}
var err error
if s.DriverName() == model.DatabaseDriverMysql {
_, err = transaction.NamedExec(`
INSERT INTO
PublicChannels(Id, DeleteAt, TeamId, DisplayName, Name, Header, Purpose)
VALUES
(:id, :deleteat, :teamid, :displayname, :name, :header, :purpose)
`, vals)
if err != nil && IsUniqueConstraintError(err, []string{"PRIMARY"}) {
_, err = transaction.NamedExec(`UPDATE PublicChannels
SET deleteAt = :deleteat,
TeamId = :teamid,
DisplayName = :displayname,
Name = :name,
Header = :header,
Purpose = :purpose
WHERE Id=:id`, vals)
}
} else {
_, err = transaction.NamedExec(`
INSERT INTO
PublicChannels(Id, DeleteAt, TeamId, DisplayName, Name, Header, Purpose)
VALUES
(:id, :deleteat, :teamid, :displayname, :name, :header, :purpose)
ON CONFLICT (id) DO UPDATE
SET DeleteAt = :deleteat,
TeamId = :teamid,
DisplayName = :displayname,
Name = :name,
Header = :header,
Purpose = :purpose;
`, vals)
}
if err != nil {
return errors.Wrap(err, "failed to insert public channel")
}
return nil
}
// Save writes the (non-direct) channel to the database.
func (s SqlChannelStore) Save(channel *model.Channel, maxChannelsPerTeam int64) (_ *model.Channel, err error) {
if channel.DeleteAt != 0 {
return nil, store.NewErrInvalidInput("Channel", "DeleteAt", channel.DeleteAt)
}
if channel.Type == model.ChannelTypeDirect {
return nil, store.NewErrInvalidInput("Channel", "Type", channel.Type)
}
var newChannel *model.Channel
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
newChannel, err = s.saveChannelT(transaction, channel, maxChannelsPerTeam)
if err != nil {
return newChannel, err
}
// Additionally propagate the write to the PublicChannels table.
if err = s.upsertPublicChannelT(transaction, newChannel); err != nil {
return nil, errors.Wrap(err, "upsert_public_channel")
}
if err = transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
// There are cases when in case of conflict, the original channel value is returned.
// So we return both and let the caller do the checks.
return newChannel, err
}
func (s SqlChannelStore) CreateDirectChannel(user *model.User, otherUser *model.User, channelOptions ...model.ChannelOption) (*model.Channel, error) {
channel := new(model.Channel)
for _, option := range channelOptions {
option(channel)
}
channel.DisplayName = ""
channel.Name = model.GetDMNameFromIds(otherUser.Id, user.Id)
channel.Header = ""
channel.Type = model.ChannelTypeDirect
channel.Shared = model.NewBool(user.IsRemote() || otherUser.IsRemote())
channel.CreatorId = user.Id
cm1 := &model.ChannelMember{
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: user.IsGuest(),
SchemeUser: !user.IsGuest(),
}
cm2 := &model.ChannelMember{
UserId: otherUser.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: otherUser.IsGuest(),
SchemeUser: !otherUser.IsGuest(),
}
return s.SaveDirectChannel(channel, cm1, cm2)
}
func (s SqlChannelStore) SaveDirectChannel(directChannel *model.Channel, member1 *model.ChannelMember, member2 *model.ChannelMember) (_ *model.Channel, err error) {
if directChannel.DeleteAt != 0 {
return nil, store.NewErrInvalidInput("Channel", "DeleteAt", directChannel.DeleteAt)
}
if directChannel.Type != model.ChannelTypeDirect {
return nil, store.NewErrInvalidInput("Channel", "Type", directChannel.Type)
}
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
directChannel.TeamId = ""
newChannel, err := s.saveChannelT(transaction, directChannel, 0)
if err != nil {
return newChannel, err
}
// Members need new channel ID
member1.ChannelId = newChannel.Id
member2.ChannelId = newChannel.Id
if member1.UserId != member2.UserId {
_, err = s.saveMultipleMembers([]*model.ChannelMember{member1, member2})
} else {
_, err = s.saveMemberT(member2)
}
if err != nil {
return nil, err
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return newChannel, nil
}
func (s SqlChannelStore) saveChannelT(transaction *sqlxTxWrapper, channel *model.Channel, maxChannelsPerTeam int64) (*model.Channel, error) {
if channel.Id != "" && !channel.IsShared() {
return nil, store.NewErrInvalidInput("Channel", "Id", channel.Id)
}
channel.PreSave()
if err := channel.IsValid(); err != nil { // TODO: this needs to return plain error in v6.
return nil, err // we just pass through the error as-is for now.
}
if channel.Type != model.ChannelTypeDirect && channel.Type != model.ChannelTypeGroup && maxChannelsPerTeam >= 0 {
var count int64
if err := transaction.Get(&count, "SELECT COUNT(0) FROM Channels WHERE TeamId = ? AND DeleteAt = 0 AND (Type = ? OR Type = ?)", channel.TeamId, model.ChannelTypeOpen, model.ChannelTypePrivate); err != nil {
return nil, errors.Wrapf(err, "save_channel_count: teamId=%s", channel.TeamId)
} else if count >= maxChannelsPerTeam {
return nil, store.NewErrLimitExceeded("channels_per_team", int(count), "teamId="+channel.TeamId)
}
}
if _, err := transaction.NamedExec(`INSERT INTO Channels
(Id, CreateAt, UpdateAt, DeleteAt, TeamId, Type, DisplayName, Name, Header, Purpose, LastPostAt, TotalMsgCount, ExtraUpdateAt, CreatorId, SchemeId, GroupConstrained, Shared, TotalMsgCountRoot, LastRootPostAt)
VALUES
(:Id, :CreateAt, :UpdateAt, :DeleteAt, :TeamId, :Type, :DisplayName, :Name, :Header, :Purpose, :LastPostAt, :TotalMsgCount, :ExtraUpdateAt, :CreatorId, :SchemeId, :GroupConstrained, :Shared, :TotalMsgCountRoot, :LastRootPostAt)`, channel); err != nil {
if IsUniqueConstraintError(err, []string{"Name", "channels_name_teamid_key"}) {
dupChannel := model.Channel{}
if serr := s.GetMasterX().Get(&dupChannel, "SELECT * FROM Channels WHERE TeamId = ? AND Name = ?", channel.TeamId, channel.Name); serr != nil {
return nil, errors.Wrapf(serr, "error while retrieving existing channel %s", channel.Name) // do not return this as a *store.ErrConflict as it would be treated as a recoverable error
}
return &dupChannel, store.NewErrConflict("Channel", err, "id="+channel.Id)
}
return nil, errors.Wrapf(err, "save_channel: id=%s", channel.Id)
}
return channel, nil
}
// Update writes the updated channel to the database.
func (s SqlChannelStore) Update(channel *model.Channel) (_ *model.Channel, err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
updatedChannel, err := s.updateChannelT(transaction, channel)
if err != nil {
return nil, err
}
// Additionally propagate the write to the PublicChannels table.
if err := s.upsertPublicChannelT(transaction, updatedChannel); err != nil {
return nil, errors.Wrap(err, "upsertPublicChannelT: failed to upsert channel")
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return updatedChannel, nil
}
func (s SqlChannelStore) updateChannelT(transaction *sqlxTxWrapper, channel *model.Channel) (*model.Channel, error) {
channel.PreUpdate()
if channel.DeleteAt != 0 {
return nil, store.NewErrInvalidInput("Channel", "DeleteAt", channel.DeleteAt)
}
if err := channel.IsValid(); err != nil {
return nil, err
}
res, err := transaction.NamedExec(`UPDATE Channels
SET CreateAt=:CreateAt,
UpdateAt=:UpdateAt,
DeleteAt=:DeleteAt,
TeamId=:TeamId,
Type=:Type,
DisplayName=:DisplayName,
Name=:Name,
Header=:Header,
Purpose=:Purpose,
LastPostAt=:LastPostAt,
TotalMsgCount=:TotalMsgCount,
ExtraUpdateAt=:ExtraUpdateAt,
CreatorId=:CreatorId,
SchemeId=:SchemeId,
GroupConstrained=:GroupConstrained,
Shared=:Shared,
TotalMsgCountRoot=:TotalMsgCountRoot,
LastRootPostAt=:LastRootPostAt
WHERE Id=:Id`, channel)
if err != nil {
if IsUniqueConstraintError(err, []string{"Name", "channels_name_teamid_key"}) {
dupChannel := model.Channel{}
s.GetReplicaX().Get(&dupChannel, "SELECT * FROM Channels WHERE TeamId = :TeamId AND Name= :Name AND DeleteAt > 0", map[string]any{"TeamId": channel.TeamId, "Name": channel.Name})
if dupChannel.DeleteAt > 0 {
return nil, store.NewErrInvalidInput("Channel", "Id", channel.Id)
}
return nil, store.NewErrInvalidInput("Channel", "Id", channel.Id)
}
return nil, errors.Wrapf(err, "failed to update channel with id=%s", channel.Id)
}
count, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "error while getting rowsAffected in updateChannelT")
}
if count > 1 {
return nil, fmt.Errorf("the expected number of channels to be updated is <=1 but was %d", count)
}
return channel, nil
}
func (s SqlChannelStore) GetChannelUnread(channelId, userId string) (*model.ChannelUnread, error) {
var unreadChannel model.ChannelUnread
err := s.GetReplicaX().Get(&unreadChannel,
`SELECT
Channels.TeamId TeamId, Channels.Id ChannelId, (Channels.TotalMsgCount - ChannelMembers.MsgCount) MsgCount, (Channels.TotalMsgCountRoot - ChannelMembers.MsgCountRoot) MsgCountRoot, ChannelMembers.MentionCount MentionCount, ChannelMembers.MentionCountRoot MentionCountRoot, COALESCE(ChannelMembers.UrgentMentionCount, 0) UrgentMentionCount, ChannelMembers.NotifyProps NotifyProps
FROM
Channels, ChannelMembers
WHERE
Id = ChannelId
AND Id = ?
AND UserId = ?
AND DeleteAt = 0`,
channelId, userId)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Channel", fmt.Sprintf("channelId=%s,userId=%s", channelId, userId))
}
return nil, errors.Wrapf(err, "failed to get Channel with channelId=%s and userId=%s", channelId, userId)
}
return &unreadChannel, nil
}
//nolint:unparam
func (s SqlChannelStore) InvalidateChannel(id string) {
}
func (s SqlChannelStore) InvalidateChannelByName(teamId, name string) {
channelByNameCache.Remove(teamId + name)
if s.metrics != nil {
s.metrics.IncrementMemCacheInvalidationCounter("Channel by Name - Remove by TeamId and Name")
}
}
func (s SqlChannelStore) GetPinnedPosts(channelId string) (*model.PostList, error) {
pl := model.NewPostList()
posts := []*model.Post{}
if err := s.GetReplicaX().Select(&posts, "SELECT *, (SELECT count(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount FROM Posts p WHERE IsPinned = true AND ChannelId = ? AND DeleteAt = 0 ORDER BY CreateAt ASC", channelId); err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
for _, post := range posts {
pl.AddPost(post)
pl.AddOrder(post.Id)
}
return pl, nil
}
//nolint:unparam
func (s SqlChannelStore) Get(id string, allowFromCache bool) (*model.Channel, error) {
ch := model.Channel{}
err := s.GetReplicaX().Get(&ch, `SELECT * FROM Channels WHERE Id=?`, id)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Channel", id)
}
return nil, errors.Wrapf(err, "failed to find channel with id = %s", id)
}
return &ch, nil
}
//nolint:unparam
func (s SqlChannelStore) GetMany(ids []string, allowFromCache bool) (model.ChannelList, error) {
query := s.getQueryBuilder().
Select("*").
From("Channels").
Where(sq.Eq{"Id": ids})
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getmany_tosql")
}
channels := model.ChannelList{}
err = s.GetReplicaX().Select(&channels, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get channels with ids %v", ids)
}
if len(channels) == 0 {
return nil, store.NewErrNotFound("Channel", fmt.Sprintf("ids=%v", ids))
}
return channels, nil
}
// Delete records the given deleted timestamp to the channel in question.
func (s SqlChannelStore) Delete(channelId string, time int64) error {
return s.SetDeleteAt(channelId, time, time)
}
// Restore reverts a previous deleted timestamp from the channel in question.
func (s SqlChannelStore) Restore(channelId string, time int64) error {
return s.SetDeleteAt(channelId, 0, time)
}
// SetDeleteAt records the given deleted and updated timestamp to the channel in question.
func (s SqlChannelStore) SetDeleteAt(channelId string, deleteAt, updateAt int64) (err error) {
defer s.InvalidateChannel(channelId)
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "SetDeleteAt: begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
err = s.setDeleteAtT(transaction, channelId, deleteAt, updateAt)
if err != nil {
return errors.Wrap(err, "setDeleteAtT")
}
// Additionally propagate the write to the PublicChannels table.
if _, err := transaction.Exec(`
UPDATE
PublicChannels
SET
DeleteAt = ?
WHERE
Id = ?
`, deleteAt, channelId); err != nil {
return errors.Wrapf(err, "failed to delete public channels with id=%s", channelId)
}
if err := transaction.Commit(); err != nil {
return errors.Wrapf(err, "SetDeleteAt: commit_transaction")
}
return nil
}
func (s SqlChannelStore) setDeleteAtT(transaction *sqlxTxWrapper, channelId string, deleteAt, updateAt int64) error {
_, err := transaction.Exec(`UPDATE Channels
SET DeleteAt = ?,
UpdateAt = ?
WHERE Id = ?`, deleteAt, updateAt, channelId)
if err != nil {
return errors.Wrapf(err, "failed to delete channel with id=%s", channelId)
}
return nil
}
// PermanentDeleteByTeam removes all channels for the given team from the database.
func (s SqlChannelStore) PermanentDeleteByTeam(teamId string) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "PermanentDeleteByTeam: begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
if err := s.permanentDeleteByTeamtT(transaction, teamId); err != nil {
return errors.Wrap(err, "permanentDeleteByTeamtT")
}
// Additionally propagate the deletions to the PublicChannels table.
if _, err := transaction.Exec(`
DELETE FROM
PublicChannels
WHERE
TeamId = ?
`, teamId); err != nil {
return errors.Wrapf(err, "failed to delete public channels by team with teamId=%s", teamId)
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "PermanentDeleteByTeam: commit_transaction")
}
return nil
}
func (s SqlChannelStore) permanentDeleteByTeamtT(transaction *sqlxTxWrapper, teamId string) error {
if _, err := transaction.Exec("DELETE FROM Channels WHERE TeamId = ?", teamId); err != nil {
return errors.Wrapf(err, "failed to delete channel by team with teamId=%s", teamId)
}
return nil
}
// PermanentDelete removes the given channel from the database.
func (s SqlChannelStore) PermanentDelete(channelId string) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "PermanentDelete: begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
if err := s.permanentDeleteT(transaction, channelId); err != nil {
return errors.Wrap(err, "permanentDeleteT")
}
// Additionally propagate the deletion to the PublicChannels table.
if _, err := transaction.Exec(`
DELETE FROM
PublicChannels
WHERE
Id = ?
`, channelId); err != nil {
return errors.Wrapf(err, "failed to delete public channels with id=%s", channelId)
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "PermanentDelete: commit_transaction")
}
return nil
}
func (s SqlChannelStore) permanentDeleteT(transaction *sqlxTxWrapper, channelId string) error {
if _, err := transaction.Exec("DELETE FROM Channels WHERE Id = ?", channelId); err != nil {
return errors.Wrapf(err, "failed to delete channel with id=%s", channelId)
}
return nil
}
func (s SqlChannelStore) PermanentDeleteMembersByChannel(channelId string) error {
_, err := s.GetMasterX().Exec("DELETE FROM ChannelMembers WHERE ChannelId = ?", channelId)
if err != nil {
return errors.Wrapf(err, "failed to delete Channel with channelId=%s", channelId)
}
return nil
}
func (s SqlChannelStore) GetChannels(teamId string, userId string, opts *model.ChannelSearchOpts) (model.ChannelList, error) {
query := s.getQueryBuilder().
Select("ch.*").
From("Channels ch, ChannelMembers cm").
Where(
sq.And{
sq.Expr("ch.Id = cm.ChannelId"),
sq.Eq{"cm.UserId": userId},
},
).
OrderBy("ch.DisplayName")
if teamId != "" {
query = query.Where(sq.Or{
sq.Eq{"ch.TeamId": teamId},
sq.Eq{"ch.TeamId": ""},
})
}
if opts.IncludeDeleted {
if opts.LastDeleteAt != 0 {
// We filter by non-archived, and archived >= a timestamp.
query = query.Where(sq.Or{
sq.Eq{"ch.DeleteAt": 0},
sq.GtOrEq{"ch.DeleteAt": opts.LastDeleteAt},
})
}
// If opts.LastDeleteAt is not set, we include everything. That means no filter is needed.
} else {
// Don't include archived channels.
query = query.Where(sq.Eq{"ch.DeleteAt": 0})
}
if opts.LastUpdateAt > 0 {
query = query.Where(sq.GtOrEq{"ch.UpdateAt": opts.LastUpdateAt})
}
channels := model.ChannelList{}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getchannels_tosql")
}
err = s.GetReplicaX().Select(&channels, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get channels with TeamId=%s and UserId=%s", teamId, userId)
}
if len(channels) == 0 {
return nil, store.NewErrNotFound("Channel", "userId="+userId)
}
return channels, nil
}
func (s SqlChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
query := s.getQueryBuilder().
Select("ch.*").
From("Channels ch, ChannelMembers cm").
Where(
sq.And{
sq.Expr("ch.Id = cm.ChannelId"),
sq.Eq{"cm.UserId": userId},
},
).
OrderBy("ch.Id")
if opts.PerPage != nil {
// The limit is verified at the GraphQL layer.
query = query.Limit(uint64(*opts.PerPage))
}
if afterChannelID != "" {
query = query.Where(sq.Gt{"ch.Id": afterChannelID})
}
if teamId != "" {
query = query.Where(sq.Or{
sq.Eq{"ch.TeamId": teamId},
sq.Eq{"ch.TeamId": ""},
})
}
if opts.IncludeDeleted {
if opts.LastDeleteAt != 0 {
// We filter by non-archived, and archived >= a timestamp.
query = query.Where(sq.Or{
sq.Eq{"ch.DeleteAt": 0},
sq.GtOrEq{"ch.DeleteAt": opts.LastDeleteAt},
})
}
// If opts.LastDeleteAt is not set, we include everything. That means no filter is needed.
} else {
// Don't include archived channels.
query = query.Where(sq.Eq{"ch.DeleteAt": 0})
}
if opts.LastUpdateAt > 0 {
query = query.Where(sq.GtOrEq{"ch.UpdateAt": opts.LastUpdateAt})
}
channels := model.ChannelList{}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getchannels_tosql")
}
err = s.GetReplicaX().Select(&channels, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get channels with TeamId=%s and UserId=%s", teamId, userId)
}
return channels, nil
}
func (s SqlChannelStore) GetChannelsByUser(userId string, includeDeleted bool, lastDeleteAt, pageSize int, fromChannelID string) (model.ChannelList, error) {
query := s.getQueryBuilder().
Select("Channels.*").
From("Channels, ChannelMembers").
Where(
sq.And{
sq.Expr("Id = ChannelId"),
sq.Eq{"UserId": userId},
},
).
OrderBy("Id ASC")
if fromChannelID != "" {
query = query.Where(sq.Gt{"Id": fromChannelID})
}
if pageSize != -1 {
query = query.Limit(uint64(pageSize))
}
if includeDeleted {
if lastDeleteAt != 0 {
// We filter by non-archived, and archived >= a timestamp.
query = query.Where(sq.Or{
sq.Eq{"DeleteAt": 0},
sq.GtOrEq{"DeleteAt": lastDeleteAt},
})
}
// If lastDeleteAt is not set, we include everything. That means no filter is needed.
} else {
// Don't include archived channels.
query = query.Where(sq.Eq{"DeleteAt": 0})
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getchannels_tosql")
}
channels := model.ChannelList{}
err = s.GetReplicaX().Select(&channels, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get channels with UserId=%s", userId)
}
if len(channels) == 0 {
return nil, store.NewErrNotFound("Channel", "userId="+userId)
}
return channels, nil
}
func (s SqlChannelStore) GetAllChannelMembersById(channelID string) ([]string, error) {
sql, args, err := s.channelMembersForTeamWithSchemeSelectQuery.Where(sq.Eq{
"ChannelId": channelID,
}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetAllChannelMembersById_ToSql")
}
dbMembers := channelMemberWithSchemeRolesList{}
err = s.GetReplicaX().Select(&dbMembers, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get ChannelMembers with channelID=%s", channelID)
}
res := make([]string, len(dbMembers))
for i, member := range dbMembers.ToModel() {
res[i] = member.UserId
}
return res, nil
}
func (s SqlChannelStore) GetAllChannels(offset, limit int, opts store.ChannelSearchOpts) (model.ChannelListWithTeamData, error) {
query := s.getAllChannelsQuery(opts, false)
query = query.
OrderBy("c.DisplayName, Teams.DisplayName").
Limit(uint64(limit)).
Offset(uint64(offset))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "failed to create query")
}
data := model.ChannelListWithTeamData{}
err = s.GetReplicaX().Select(&data, queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to get all channels")
}
return data, nil
}
func (s SqlChannelStore) GetAllChannelsCount(opts store.ChannelSearchOpts) (int64, error) {
query := s.getAllChannelsQuery(opts, true)
queryString, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "failed to create query")
}
var count int64
err = s.GetReplicaX().Get(&count, queryString, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to count all channels")
}
return count, nil
}
func (s SqlChannelStore) getAllChannelsQuery(opts store.ChannelSearchOpts, forCount bool) sq.SelectBuilder {
var selectStr string
if forCount {
selectStr = "count(c.Id)"
} else {
selectStr = "c.*, Teams.DisplayName AS TeamDisplayName, Teams.Name AS TeamName, Teams.UpdateAt AS TeamUpdateAt"
if opts.IncludePolicyID {
selectStr += ", RetentionPoliciesChannels.PolicyId AS PolicyID"
}
}
query := s.getQueryBuilder().
Select(selectStr).
From("Channels AS c").
Where(sq.Eq{"c.Type": []model.ChannelType{model.ChannelTypePrivate, model.ChannelTypeOpen}})
if !forCount {
query = query.Join("Teams ON Teams.Id = c.TeamId")
}
if !opts.IncludeDeleted {
query = query.Where(sq.Eq{"c.DeleteAt": int(0)})
}
if opts.NotAssociatedToGroup != "" {
query = query.Where("c.Id NOT IN (SELECT ChannelId FROM GroupChannels WHERE GroupChannels.GroupId = ? AND GroupChannels.DeleteAt = 0)", opts.NotAssociatedToGroup)
}
if len(opts.ExcludeChannelNames) > 0 {
query = query.Where(sq.NotEq{"c.Name": opts.ExcludeChannelNames})
}
if opts.ExcludePolicyConstrained || opts.IncludePolicyID {
query = query.LeftJoin("RetentionPoliciesChannels ON c.Id = RetentionPoliciesChannels.ChannelId")
}
if opts.ExcludePolicyConstrained {
query = query.Where("RetentionPoliciesChannels.ChannelId IS NULL")
}
return query
}
func (s SqlChannelStore) GetMoreChannels(teamId string, userId string, offset int, limit int) (model.ChannelList, error) {
channels := model.ChannelList{}
err := s.GetReplicaX().Select(&channels, `
SELECT
Channels.*
FROM
Channels
JOIN
PublicChannels c ON (c.Id = Channels.Id)
WHERE
c.TeamId = ?
AND c.DeleteAt = 0
AND c.Id NOT IN (
SELECT
c.Id
FROM
PublicChannels c
JOIN
ChannelMembers cm ON (cm.ChannelId = c.Id)
WHERE
c.TeamId = ?
AND cm.UserId = ?
AND c.DeleteAt = 0
)
ORDER BY
c.DisplayName
LIMIT ?
OFFSET ?
`, teamId, teamId, userId, limit, offset)
if err != nil {
return nil, errors.Wrapf(err, "failed getting channels with teamId=%s and userId=%s", teamId, userId)
}
return channels, nil
}
func (s SqlChannelStore) GetPrivateChannelsForTeam(teamId string, offset int, limit int) (model.ChannelList, error) {
channels := model.ChannelList{}
builder := s.getQueryBuilder().
Select("*").
From("Channels").
Where(sq.Eq{"Type": model.ChannelTypePrivate, "TeamId": teamId, "DeleteAt": 0}).
OrderBy("DisplayName").
Limit(uint64(limit)).
Offset(uint64(offset))
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channels_tosql")
}
err = s.GetReplicaX().Select(&channels, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find channel with teamId=%s", teamId)
}
return channels, nil
}
func (s SqlChannelStore) GetPublicChannelsForTeam(teamId string, offset int, limit int) (model.ChannelList, error) {
channels := model.ChannelList{}
err := s.GetReplicaX().Select(&channels, `
SELECT
Channels.*
FROM
Channels
JOIN
PublicChannels pc ON (pc.Id = Channels.Id)
WHERE
pc.TeamId = ?
AND pc.DeleteAt = 0
ORDER BY pc.DisplayName
LIMIT ?
OFFSET ?
`, teamId, limit, offset)
if err != nil {
return nil, errors.Wrapf(err, "failed to find channel with teamId=%s", teamId)
}
return channels, nil
}
func (s SqlChannelStore) GetPublicChannelsByIdsForTeam(teamId string, channelIds []string) (model.ChannelList, error) {
props := make(map[string]any)
props["teamId"] = teamId
idQuery := ""
for index, channelId := range channelIds {
if idQuery != "" {
idQuery += ", "
}
props["channelId"+strconv.Itoa(index)] = channelId
idQuery += ":channelId" + strconv.Itoa(index)
}
var data model.ChannelList
builder := s.getQueryBuilder().
Select("Channels.*").
From("Channels").
Join("PublicChannels pc ON (pc.Id = Channels.Id)").
Where(sq.And{
sq.Eq{"pc.TeamId": teamId},
sq.Eq{"pc.DeleteAt": 0},
sq.Eq{"pc.Id": channelIds},
}).
OrderBy("pc.DisplayName")
queryString, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetPublicChannelsByIdsForTeam to_sql")
}
err = s.GetReplicaX().Select(&data, queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Channels")
}
if len(data) == 0 {
return nil, store.NewErrNotFound("Channel", fmt.Sprintf("teamId=%s, channelIds=%v", teamId, channelIds))
}
return data, nil
}
func (s SqlChannelStore) GetChannelCounts(teamId string, userId string) (*model.ChannelCounts, error) {
data := []struct {
Id string
TotalMsgCount int64
TotalMsgCountRoot int64
UpdateAt int64
}{}
err := s.GetReplicaX().Select(&data, `SELECT Id, TotalMsgCount, TotalMsgCountRoot, UpdateAt
FROM Channels
WHERE Id IN (SELECT ChannelId FROM ChannelMembers WHERE UserId = ?)
AND (TeamId = ? OR TeamId = '')
AND DeleteAt = 0
ORDER BY DisplayName`, userId, teamId)
if err != nil {
return nil, errors.Wrapf(err, "failed to get channels count with teamId=%s and userId=%s", teamId, userId)
}
counts := &model.ChannelCounts{
Counts: make(map[string]int64),
CountsRoot: make(map[string]int64),
UpdateTimes: make(map[string]int64),
}
for i := range data {
v := data[i]
counts.Counts[v.Id] = v.TotalMsgCount
counts.CountsRoot[v.Id] = v.TotalMsgCountRoot
counts.UpdateTimes[v.Id] = v.UpdateAt
}
return counts, nil
}
func (s SqlChannelStore) GetTeamChannels(teamId string) (model.ChannelList, error) {
data := model.ChannelList{}
err := s.GetReplicaX().Select(&data, "SELECT * FROM Channels WHERE TeamId = ? And Type != ? ORDER BY DisplayName", teamId, model.ChannelTypeDirect)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Channels with teamId=%s", teamId)
}
if len(data) == 0 {
return nil, store.NewErrNotFound("Channel", fmt.Sprintf("teamId=%s", teamId))
}
return data, nil
}
func (s SqlChannelStore) GetByName(teamId string, name string, allowFromCache bool) (*model.Channel, error) {
return s.getByName(teamId, name, false, allowFromCache)
}
func (s SqlChannelStore) GetByNames(teamId string, names []string, allowFromCache bool) ([]*model.Channel, error) {
var channels []*model.Channel
if allowFromCache {
var misses []string
visited := make(map[string]struct{})
for _, name := range names {
if _, ok := visited[name]; ok {
continue
}
visited[name] = struct{}{}
var cacheItem *model.Channel
if err := channelByNameCache.Get(teamId+name, &cacheItem); err == nil {
channels = append(channels, cacheItem)
} else {
misses = append(misses, name)
}
}
names = misses
}
if len(names) > 0 {
builder := s.getQueryBuilder().
Select("*").
From("Channels").
Where(
sq.And{
sq.Eq{"Name": names},
sq.Eq{"DeleteAt": 0},
},
)
if teamId != "" {
builder = builder.Where(sq.Eq{"TeamId": teamId})
}
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetByNames_tosql")
}
dbChannels := []*model.Channel{}
if err := s.GetReplicaX().Select(&dbChannels, query, args...); err != nil && err != sql.ErrNoRows {
msg := fmt.Sprintf("failed to get channels with names=%v", names)
if teamId != "" {
msg += fmt.Sprintf(" teamId=%s", teamId)
}
return nil, errors.Wrap(err, msg)
}
for _, channel := range dbChannels {
channelByNameCache.SetWithExpiry(teamId+channel.Name, channel, ChannelCacheDuration)
channels = append(channels, channel)
}
// Not all channels are in cache. Increment aggregate miss counter.
if s.metrics != nil {
s.metrics.IncrementMemCacheMissCounter("Channel By Name - Aggregate")
}
} else {
// All of the channel names are in cache. Increment aggregate hit counter.
if s.metrics != nil {
s.metrics.IncrementMemCacheHitCounter("Channel By Name - Aggregate")
}
}
return channels, nil
}
func (s SqlChannelStore) GetByNameIncludeDeleted(teamId string, name string, allowFromCache bool) (*model.Channel, error) {
return s.getByName(teamId, name, true, allowFromCache)
}
func (s SqlChannelStore) getByName(teamId string, name string, includeDeleted bool, allowFromCache bool) (*model.Channel, error) {
query := s.getQueryBuilder().
Select("*").
From("Channels").
Where(sq.Eq{"Name": name})
if !includeDeleted {
query = query.Where(sq.Eq{"DeleteAt": 0})
}
if teamId != "" {
query = query.Where(sq.Or{
sq.Eq{"TeamId": teamId},
sq.Eq{"TeamId": ""},
})
}
channel := model.Channel{}
if allowFromCache {
var cacheItem *model.Channel
if err := channelByNameCache.Get(teamId+name, &cacheItem); err == nil {
if s.metrics != nil {
s.metrics.IncrementMemCacheHitCounter("Channel By Name")
}
return cacheItem, nil
}
if s.metrics != nil {
s.metrics.IncrementMemCacheMissCounter("Channel By Name")
}
}
queryStr, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getByName_tosql")
}
if err = s.GetReplicaX().Get(&channel, queryStr, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Channel", fmt.Sprintf("TeamId=%s&Name=%s", teamId, name))
}
return nil, errors.Wrapf(err, "failed to find channel with TeamId=%s and Name=%s", teamId, name)
}
err = channelByNameCache.SetWithExpiry(teamId+name, &channel, ChannelCacheDuration)
return &channel, err
}
func (s SqlChannelStore) GetDeletedByName(teamId string, name string) (*model.Channel, error) {
channel := model.Channel{}
if err := s.GetReplicaX().Get(&channel, `SELECT *
FROM Channels
WHERE (TeamId = ? OR TeamId = '')
AND Name = ?
AND DeleteAt != 0`, teamId, name); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Channel", fmt.Sprintf("name=%s", name))
}
return nil, errors.Wrapf(err, "failed to get channel by teamId=%s and name=%s", teamId, name)
}
return &channel, nil
}
func (s SqlChannelStore) GetDeleted(teamId string, offset int, limit int, userId string) (model.ChannelList, error) {
channels := model.ChannelList{}
query := `
SELECT * FROM Channels
WHERE (TeamId = ? OR TeamId = '')
AND DeleteAt != 0
AND Type != ?
UNION
SELECT * FROM Channels
WHERE (TeamId = ? OR TeamId = '')
AND DeleteAt != 0
AND Type = ?
AND Id IN (SELECT ChannelId FROM ChannelMembers WHERE UserId = ?)
ORDER BY DisplayName LIMIT ? OFFSET ?
`
if err := s.GetReplicaX().Select(&channels, query, teamId, model.ChannelTypePrivate, teamId, model.ChannelTypePrivate, userId, limit, offset); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Channel", fmt.Sprintf("TeamId=%s,UserId=%s", teamId, userId))
}
return nil, errors.Wrapf(err, "failed to get deleted channels with TeamId=%s and UserId=%s", teamId, userId)
}
return channels, nil
}
var channelMembersWithSchemeSelectQuery = `
SELECT
ChannelMembers.ChannelId,
ChannelMembers.UserId,
ChannelMembers.Roles,
ChannelMembers.LastViewedAt,
ChannelMembers.MsgCount,
ChannelMembers.MentionCount,
ChannelMembers.MentionCountRoot,
COALESCE(ChannelMembers.UrgentMentionCount, 0) AS UrgentMentionCount,
ChannelMembers.MsgCountRoot,
ChannelMembers.NotifyProps,
ChannelMembers.LastUpdateAt,
ChannelMembers.SchemeUser,
ChannelMembers.SchemeAdmin,
ChannelMembers.SchemeGuest,
COALESCE(Teams.DisplayName, '') TeamDisplayName,
COALESCE(Teams.Name, '') TeamName,
COALESCE(Teams.UpdateAt, 0) TeamUpdateAt,
TeamScheme.DefaultChannelGuestRole TeamSchemeDefaultGuestRole,
TeamScheme.DefaultChannelUserRole TeamSchemeDefaultUserRole,
TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole,
ChannelScheme.DefaultChannelGuestRole ChannelSchemeDefaultGuestRole,
ChannelScheme.DefaultChannelUserRole ChannelSchemeDefaultUserRole,
ChannelScheme.DefaultChannelAdminRole ChannelSchemeDefaultAdminRole
FROM
ChannelMembers
INNER JOIN
Channels ON ChannelMembers.ChannelId = Channels.Id
LEFT JOIN
Schemes ChannelScheme ON Channels.SchemeId = ChannelScheme.Id
LEFT JOIN
Teams ON Channels.TeamId = Teams.Id
LEFT JOIN
Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id
`
func (s SqlChannelStore) SaveMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
for _, member := range members {
defer s.InvalidateAllChannelMembersForUser(member.UserId)
}
newMembers, err := s.saveMultipleMembers(members)
if err != nil {
return nil, err
}
return newMembers, nil
}
func (s SqlChannelStore) SaveMember(member *model.ChannelMember) (*model.ChannelMember, error) {
newMembers, err := s.SaveMultipleMembers([]*model.ChannelMember{member})
if err != nil {
return nil, err
}
return newMembers[0], nil
}
func (s SqlChannelStore) saveMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
newChannelMembers := map[string]int{}
users := map[string]bool{}
for _, member := range members {
if val, ok := newChannelMembers[member.ChannelId]; val < 1 || !ok {
newChannelMembers[member.ChannelId] = 1
} else {
newChannelMembers[member.ChannelId]++
}
users[member.UserId] = true
member.PreSave()
if err := member.IsValid(); err != nil { // TODO: this needs to return plain error in v6.
return nil, err
}
}
channels := []string{}
for channel := range newChannelMembers {
channels = append(channels, channel)
}
defaultChannelRolesByChannel := map[string]struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
channelRolesQuery := s.getQueryBuilder().
Select(
"Channels.Id as Id",
"ChannelScheme.DefaultChannelGuestRole as Guest",
"ChannelScheme.DefaultChannelUserRole as User",
"ChannelScheme.DefaultChannelAdminRole as Admin",
).
From("Channels").
LeftJoin("Schemes ChannelScheme ON Channels.SchemeId = ChannelScheme.Id").
Where(sq.Eq{"Channels.Id": channels})
channelRolesSql, channelRolesArgs, err := channelRolesQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_roles_tosql")
}
defaultChannelsRoles := []struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
err = s.GetMasterX().Select(&defaultChannelsRoles, channelRolesSql, channelRolesArgs...)
if err != nil {
return nil, errors.Wrap(err, "default_channel_roles_select")
}
for _, defaultRoles := range defaultChannelsRoles {
defaultChannelRolesByChannel[defaultRoles.Id] = defaultRoles
}
defaultTeamRolesByChannel := map[string]struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
teamRolesQuery := s.getQueryBuilder().
Select(
"Channels.Id as Id",
"TeamScheme.DefaultChannelGuestRole as Guest",
"TeamScheme.DefaultChannelUserRole as User",
"TeamScheme.DefaultChannelAdminRole as Admin",
).
From("Channels").
LeftJoin("Teams ON Teams.Id = Channels.TeamId").
LeftJoin("Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id").
Where(sq.Eq{"Channels.Id": channels})
teamRolesSql, teamRolesArgs, err := teamRolesQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_roles_tosql")
}
defaultTeamsRoles := []struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
err = s.GetMasterX().Select(&defaultTeamsRoles, teamRolesSql, teamRolesArgs...)
if err != nil {
return nil, errors.Wrap(err, "default_team_roles_select")
}
for _, defaultRoles := range defaultTeamsRoles {
defaultTeamRolesByChannel[defaultRoles.Id] = defaultRoles
}
query := s.getQueryBuilder().Insert("ChannelMembers").Columns(channelMemberSliceColumns()...)
for _, member := range members {
query = query.Values(channelMemberToSlice(member)...)
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_members_tosql")
}
if _, err := s.GetMasterX().Exec(sql, args...); err != nil {
if IsUniqueConstraintError(err, []string{"ChannelId", "channelmembers_pkey", "PRIMARY"}) {
return nil, store.NewErrConflict("ChannelMembers", err, "")
}
return nil, errors.Wrap(err, "channel_members_save")
}
newMembers := []*model.ChannelMember{}
for _, member := range members {
defaultTeamGuestRole := defaultTeamRolesByChannel[member.ChannelId].Guest.String
defaultTeamUserRole := defaultTeamRolesByChannel[member.ChannelId].User.String
defaultTeamAdminRole := defaultTeamRolesByChannel[member.ChannelId].Admin.String
defaultChannelGuestRole := defaultChannelRolesByChannel[member.ChannelId].Guest.String
defaultChannelUserRole := defaultChannelRolesByChannel[member.ChannelId].User.String
defaultChannelAdminRole := defaultChannelRolesByChannel[member.ChannelId].Admin.String
rolesResult := getChannelRoles(
member.SchemeGuest, member.SchemeUser, member.SchemeAdmin,
defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole,
defaultChannelGuestRole, defaultChannelUserRole, defaultChannelAdminRole,
strings.Fields(member.ExplicitRoles),
)
newMember := *member
newMember.SchemeGuest = rolesResult.schemeGuest
newMember.SchemeUser = rolesResult.schemeUser
newMember.SchemeAdmin = rolesResult.schemeAdmin
newMember.Roles = strings.Join(rolesResult.roles, " ")
newMember.ExplicitRoles = strings.Join(rolesResult.explicitRoles, " ")
newMembers = append(newMembers, &newMember)
}
return newMembers, nil
}
func (s SqlChannelStore) saveMemberT(member *model.ChannelMember) (*model.ChannelMember, error) {
members, err := s.saveMultipleMembers([]*model.ChannelMember{member})
if err != nil {
return nil, err
}
return members[0], nil
}
func (s SqlChannelStore) UpdateMultipleMembers(members []*model.ChannelMember) (_ []*model.ChannelMember, err error) {
for _, member := range members {
member.PreUpdate()
if err := member.IsValid(); err != nil {
return nil, err
}
}
var transaction *sqlxTxWrapper
if transaction, err = s.GetMasterX().Beginx(); err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
updatedMembers := []*model.ChannelMember{}
for _, member := range members {
update := s.getQueryBuilder().
Update("ChannelMembers").
SetMap(NewMapFromChannelMemberModel(member)).
Where(sq.Eq{
"ChannelId": member.ChannelId,
"UserId": member.UserId,
})
sqlUpdate, args, err := update.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "UpdateMultipleMembers_Update_ToSql ChannelID=%s UserID=%s", member.ChannelId, member.UserId)
}
if _, err = transaction.Exec(sqlUpdate, args...); err != nil {
return nil, errors.Wrap(err, "failed to update ChannelMember")
}
sqlSelect, args, err := s.channelMembersForTeamWithSchemeSelectQuery.
Where(sq.Eq{
"ChannelMembers.ChannelId": member.ChannelId,
"ChannelMembers.UserId": member.UserId,
}).ToSql()
if err != nil {
return nil, errors.Wrapf(err, "UpdateMultipleMembers_Select_ToSql ChannelID=%s UserID=%s", member.ChannelId, member.UserId)
}
// TODO: Get this out of the transaction when is possible
var dbMember channelMemberWithSchemeRoles
if err := transaction.Get(&dbMember, sqlSelect, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("ChannelMember", fmt.Sprintf("channelId=%s, userId=%s", member.ChannelId, member.UserId))
}
return nil, errors.Wrapf(err, "failed to get ChannelMember with channelId=%s and userId=%s", member.ChannelId, member.UserId)
}
updatedMembers = append(updatedMembers, dbMember.ToModel())
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return updatedMembers, nil
}
func (s SqlChannelStore) UpdateMember(member *model.ChannelMember) (*model.ChannelMember, error) {
updatedMembers, err := s.UpdateMultipleMembers([]*model.ChannelMember{member})
if err != nil {
return nil, err
}
return updatedMembers[0], nil
}
func (s SqlChannelStore) UpdateMemberNotifyProps(channelID, userID string, props map[string]string) (_ *model.ChannelMember, err error) {
tx, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(tx, &err)
if s.DriverName() == model.DatabaseDriverPostgres {
sql, args, err2 := s.getQueryBuilder().
Update("channelmembers").
Set("notifyprops", sq.Expr("notifyprops || ?::jsonb", model.MapToJSON(props))).
Where(sq.Eq{
"userid": userID,
"channelid": channelID,
}).ToSql()
if err2 != nil {
return nil, errors.Wrapf(err2, "UpdateMemberNotifyProps_Update_Postgres_ToSql channelID=%s and userID=%s", channelID, userID)
}
_, err = tx.Exec(sql, args...)
} else if len(props) > 0 {
// It's difficult to construct a SQL query for MySQL
// to handle a case of empty map. So we just ignore it.
// unpack the keys and values to pass to MySQL.
jsonArgs, jsonSQL := constructMySQLJSONArgs(props)
jsonExpr := sq.Expr(fmt.Sprintf("JSON_SET(NotifyProps, %s)", jsonSQL), jsonArgs...)
// Example: UPDATE ChannelMembers
// SET NotifyProps = JSON_SET(NotifyProps, '$.mark_unread', '"yes"' [, ...])
// WHERE ...
sql, args, err2 := s.getQueryBuilder().
Update("ChannelMembers").
Set("NotifyProps", jsonExpr).
Where(sq.Eq{
"UserId": userID,
"ChannelId": channelID,
}).ToSql()
if err2 != nil {
return nil, errors.Wrapf(err2, "UpdateMemberNotifyProps_Update_MySQL_ToSql channelID=%s and userID=%s", channelID, userID)
}
_, err = tx.Exec(sql, args...)
}
if err != nil {
return nil, errors.Wrapf(err, "failed to update ChannelMember with channelID=%s and userID=%s", channelID, userID)
}
selectSQL, args, err := s.channelMembersForTeamWithSchemeSelectQuery.
Where(sq.Eq{
"ChannelMembers.ChannelId": channelID,
"ChannelMembers.UserId": userID,
}).ToSql()
if err != nil {
return nil, errors.Wrapf(err, "UpdateMemberNotifyProps_Select_ToSql channelID=%s and userID=%s", channelID, userID)
}
var dbMember channelMemberWithSchemeRoles
if err2 := tx.Get(&dbMember, selectSQL, args...); err2 != nil {
if err2 == sql.ErrNoRows {
return nil, store.NewErrNotFound("ChannelMember", fmt.Sprintf("channelId=%s, userId=%s", channelID, userID))
}
return nil, errors.Wrapf(err2, "failed to get ChannelMember with channelId=%s and userId=%s", channelID, userID)
}
if err2 := tx.Commit(); err2 != nil {
return nil, errors.Wrap(err2, "commit_transaction")
}
return dbMember.ToModel(), err
}
func (s SqlChannelStore) GetMembers(channelID string, offset, limit int) (model.ChannelMembers, error) {
sql, args, err := s.channelMembersForTeamWithSchemeSelectQuery.
Where(sq.Eq{
"ChannelId": channelID,
}).
Limit(uint64(limit)).
Offset(uint64(offset)).
ToSql()
if err != nil {
return nil, errors.Wrapf(err, "GetMember_ToSql ChannelID=%s", channelID)
}
dbMembers := channelMemberWithSchemeRolesList{}
err = s.GetReplicaX().Select(&dbMembers, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get ChannelMembers with channelId=%s", channelID)
}
return dbMembers.ToModel(), nil
}
func (s SqlChannelStore) GetChannelMembersTimezones(channelId string) ([]model.StringMap, error) {
dbMembersTimezone := []model.StringMap{}
err := s.GetReplicaX().Select(&dbMembersTimezone, `
SELECT
Users.Timezone
FROM
ChannelMembers
LEFT JOIN
Users ON ChannelMembers.UserId = Id
WHERE ChannelId = ?
`, channelId)
if err != nil {
return nil, errors.Wrapf(err, "failed to find user timezones for users in channels with channelId=%s", channelId)
}
return dbMembersTimezone, nil
}
func (s SqlChannelStore) GetMember(ctx context.Context, channelID string, userID string) (*model.ChannelMember, error) {
selectSQL, args, err := s.channelMembersForTeamWithSchemeSelectQuery.
Where(sq.Eq{
"ChannelMembers.ChannelId": channelID,
"ChannelMembers.UserId": userID,
}).ToSql()
if err != nil {
return nil, errors.Wrapf(err, "GetMember_ToSql ChannelID=%s UserID=%s", channelID, userID)
}
var dbMember channelMemberWithSchemeRoles
if err := s.DBXFromContext(ctx).Get(&dbMember, selectSQL, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("ChannelMember", fmt.Sprintf("channelId=%s, userId=%s", channelID, userID))
}
return nil, errors.Wrapf(err, "failed to get ChannelMember with channelId=%s and userId=%s", channelID, userID)
}
return dbMember.ToModel(), nil
}
func (s SqlChannelStore) InvalidateAllChannelMembersForUser(userId string) {
allChannelMembersForUserCache.Remove(userId)
allChannelMembersForUserCache.Remove(userId + "_deleted")
if s.metrics != nil {
s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members for User - Remove by UserId")
}
}
func (s SqlChannelStore) IsUserInChannelUseCache(userId string, channelId string) bool {
var ids map[string]string
if err := allChannelMembersForUserCache.Get(userId, &ids); err == nil {
if s.metrics != nil {
s.metrics.IncrementMemCacheHitCounter("All Channel Members for User")
}
if _, ok := ids[channelId]; ok {
return true
}
return false
}
if s.metrics != nil {
s.metrics.IncrementMemCacheMissCounter("All Channel Members for User")
}
ids, err := s.GetAllChannelMembersForUser(userId, true, false)
if err != nil {
mlog.Error("Error getting all channel members for user", mlog.Err(err))
return false
}
if _, ok := ids[channelId]; ok {
return true
}
return false
}
func (s SqlChannelStore) GetMemberForPost(postId string, userId string) (*model.ChannelMember, error) {
var dbMember channelMemberWithSchemeRoles
query := `
SELECT
ChannelMembers.ChannelId,
ChannelMembers.UserId,
ChannelMembers.Roles,
ChannelMembers.LastViewedAt,
ChannelMembers.MsgCount,
ChannelMembers.MentionCount,
ChannelMembers.MentionCountRoot,
COALESCE(ChannelMembers.UrgentMentionCount, 0) AS UrgentMentionCount,
ChannelMembers.MsgCountRoot,
ChannelMembers.NotifyProps,
ChannelMembers.LastUpdateAt,
ChannelMembers.SchemeUser,
ChannelMembers.SchemeAdmin,
ChannelMembers.SchemeGuest,
TeamScheme.DefaultChannelGuestRole TeamSchemeDefaultGuestRole,
TeamScheme.DefaultChannelUserRole TeamSchemeDefaultUserRole,
TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole,
ChannelScheme.DefaultChannelGuestRole ChannelSchemeDefaultGuestRole,
ChannelScheme.DefaultChannelUserRole ChannelSchemeDefaultUserRole,
ChannelScheme.DefaultChannelAdminRole ChannelSchemeDefaultAdminRole
FROM
ChannelMembers
INNER JOIN
Posts ON ChannelMembers.ChannelId = Posts.ChannelId
INNER JOIN
Channels ON ChannelMembers.ChannelId = Channels.Id
LEFT JOIN
Schemes ChannelScheme ON Channels.SchemeId = ChannelScheme.Id
LEFT JOIN
Teams ON Channels.TeamId = Teams.Id
LEFT JOIN
Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id
WHERE
ChannelMembers.UserId = ?
AND
Posts.Id = ?`
if err := s.GetReplicaX().Get(&dbMember, query, userId, postId); err != nil {
return nil, errors.Wrapf(err, "failed to get ChannelMember with postId=%s and userId=%s", postId, userId)
}
return dbMember.ToModel(), nil
}
func (s SqlChannelStore) GetAllChannelMembersForUser(userId string, allowFromCache bool, includeDeleted bool) (_ map[string]string, err error) {
cache_key := userId
if includeDeleted {
cache_key += "_deleted"
}
if allowFromCache {
ids := make(map[string]string)
if err = allChannelMembersForUserCache.Get(cache_key, &ids); err == nil {
if s.metrics != nil {
s.metrics.IncrementMemCacheHitCounter("All Channel Members for User")
}
return ids, nil
}
}
if s.metrics != nil {
s.metrics.IncrementMemCacheMissCounter("All Channel Members for User")
}
query := s.getQueryBuilder().
Select(`
ChannelMembers.ChannelId, ChannelMembers.Roles, ChannelMembers.SchemeGuest,
ChannelMembers.SchemeUser, ChannelMembers.SchemeAdmin,
TeamScheme.DefaultChannelGuestRole TeamSchemeDefaultGuestRole,
TeamScheme.DefaultChannelUserRole TeamSchemeDefaultUserRole,
TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole,
ChannelScheme.DefaultChannelGuestRole ChannelSchemeDefaultGuestRole,
ChannelScheme.DefaultChannelUserRole ChannelSchemeDefaultUserRole,
ChannelScheme.DefaultChannelAdminRole ChannelSchemeDefaultAdminRole
`).
From("ChannelMembers").
Join("Channels ON ChannelMembers.ChannelId = Channels.Id").
LeftJoin("Schemes ChannelScheme ON Channels.SchemeId = ChannelScheme.Id").
LeftJoin("Teams ON Channels.TeamId = Teams.Id").
LeftJoin("Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id").
Where(sq.Eq{"ChannelMembers.UserId": userId})
if !includeDeleted {
query = query.Where(sq.Eq{"Channels.DeleteAt": 0})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_tosql")
}
rows, err := s.GetReplicaX().DB.Query(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find ChannelMembers, TeamScheme and ChannelScheme data")
}
defer deferClose(rows, &err)
var data allChannelMembers
for rows.Next() {
var cm allChannelMember
err = rows.Scan(
&cm.ChannelId, &cm.Roles, &cm.SchemeGuest, &cm.SchemeUser,
&cm.SchemeAdmin, &cm.TeamSchemeDefaultGuestRole, &cm.TeamSchemeDefaultUserRole,
&cm.TeamSchemeDefaultAdminRole, &cm.ChannelSchemeDefaultGuestRole,
&cm.ChannelSchemeDefaultUserRole, &cm.ChannelSchemeDefaultAdminRole,
)
if err != nil {
return nil, errors.Wrap(err, "unable to scan columns")
}
data = append(data, cm)
}
if err = rows.Err(); err != nil {
return nil, errors.Wrap(err, "error while iterating over rows")
}
ids := data.ToMapStringString()
if allowFromCache {
allChannelMembersForUserCache.SetWithExpiry(cache_key, ids, AllChannelMembersForUserCacheDuration)
}
return ids, nil
}
func (s SqlChannelStore) InvalidateCacheForChannelMembersNotifyProps(channelId string) {
allChannelMembersNotifyPropsForChannelCache.Remove(channelId)
if s.metrics != nil {
s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members Notify Props for Channel - Remove by ChannelId")
}
}
type allChannelMemberNotifyProps struct {
UserId string
NotifyProps model.StringMap
}
func (s SqlChannelStore) GetAllChannelMembersNotifyPropsForChannel(channelId string, allowFromCache bool) (map[string]model.StringMap, error) {
if allowFromCache {
var cacheItem map[string]model.StringMap
if err := allChannelMembersNotifyPropsForChannelCache.Get(channelId, &cacheItem); err == nil {
if s.metrics != nil {
s.metrics.IncrementMemCacheHitCounter("All Channel Members Notify Props for Channel")
}
return cacheItem, nil
}
}
if s.metrics != nil {
s.metrics.IncrementMemCacheMissCounter("All Channel Members Notify Props for Channel")
}
data := []allChannelMemberNotifyProps{}
err := s.GetReplicaX().Select(&data, `
SELECT UserId, NotifyProps
FROM ChannelMembers
WHERE ChannelId = ?`, channelId)
if err != nil {
return nil, errors.Wrapf(err, "failed to find data from ChannelMembers with channelId=%s", channelId)
}
props := make(map[string]model.StringMap)
for i := range data {
props[data[i].UserId] = data[i].NotifyProps
}
allChannelMembersNotifyPropsForChannelCache.SetWithExpiry(channelId, props, AllChannelMembersNotifyPropsForChannelCacheDuration)
return props, nil
}
//nolint:unparam
func (s SqlChannelStore) InvalidateMemberCount(channelId string) {
}
func (s SqlChannelStore) GetMemberCountFromCache(channelId string) int64 {
count, _ := s.GetMemberCount(channelId, true)
return count
}
func (s SqlChannelStore) GetFileCount(channelId string) (int64, error) {
var count int64
err := s.GetReplicaX().Get(&count, `
SELECT
COUNT(*)
FROM
FileInfo
WHERE
FileInfo.DeleteAt = 0
AND FileInfo.ChannelId = ?`,
channelId)
if err != nil {
return 0, errors.Wrapf(err, "failed to count files with channelId=%s", channelId)
}
return count, nil
}
//nolint:unparam
func (s SqlChannelStore) GetMemberCount(channelId string, allowFromCache bool) (int64, error) {
var count int64
err := s.GetReplicaX().Get(&count, `
SELECT
count(*)
FROM
ChannelMembers,
Users
WHERE
ChannelMembers.UserId = Users.Id
AND ChannelMembers.ChannelId = ?
AND Users.DeleteAt = 0`, channelId)
if err != nil {
return 0, errors.Wrapf(err, "failed to count ChannelMembers with channelId=%s", channelId)
}
return count, nil
}
// GetMemberCountsByGroup returns a slice of ChannelMemberCountByGroup for a given channel
// which contains the number of channel members for each group and optionally the number of unique timezones present for each group in the channel
func (s SqlChannelStore) GetMemberCountsByGroup(ctx context.Context, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, error) {
selectStr := "GroupMembers.GroupId, COUNT(ChannelMembers.UserId) AS ChannelMemberCount"
if includeTimezones {
if s.DriverName() == model.DatabaseDriverMysql {
selectStr += `,
COUNT(DISTINCT
(
CASE WHEN JSON_EXTRACT(Timezone, '$.useAutomaticTimezone') = 'true' AND LENGTH(JSON_UNQUOTE(JSON_EXTRACT(Timezone, '$.automaticTimezone'))) > 0
THEN JSON_EXTRACT(Timezone, '$.automaticTimezone')
WHEN JSON_EXTRACT(Timezone, '$.useAutomaticTimezone') = 'false' AND LENGTH(JSON_UNQUOTE(JSON_EXTRACT(Timezone, '$.manualTimezone'))) > 0
THEN JSON_EXTRACT(Timezone, '$.manualTimezone')
END
)) AS ChannelMemberTimezonesCount`
} else if s.DriverName() == model.DatabaseDriverPostgres {
selectStr += `,
COUNT(DISTINCT
(
CASE WHEN Timezone->>'useAutomaticTimezone' = 'true' AND length(Timezone->>'automaticTimezone') > 0
THEN Timezone->>'automaticTimezone'
WHEN Timezone->>'useAutomaticTimezone' = 'false' AND length(Timezone->>'manualTimezone') > 0
THEN Timezone->>'manualTimezone'
END
)) AS ChannelMemberTimezonesCount`
}
}
query := s.getQueryBuilder().
Select(selectStr).
From("ChannelMembers").
Join("GroupMembers ON GroupMembers.UserId = ChannelMembers.UserId AND GroupMembers.DeleteAt = 0")
if includeTimezones {
query = query.Join("Users ON Users.Id = GroupMembers.UserId")
}
query = query.Where(sq.Eq{"ChannelMembers.ChannelId": channelID}).GroupBy("GroupMembers.GroupId")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_tosql")
}
data := []*model.ChannelMemberCountByGroup{}
if err := s.DBXFromContext(ctx).Select(&data, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to count ChannelMembers with channelId=%s", channelID)
}
return data, nil
}
//nolint:unparam
func (s SqlChannelStore) InvalidatePinnedPostCount(channelId string) {
}
//nolint:unparam
func (s SqlChannelStore) GetPinnedPostCount(channelId string, allowFromCache bool) (int64, error) {
var count int64
err := s.GetReplicaX().Get(&count, `
SELECT count(*)
FROM Posts
WHERE
IsPinned = true
AND ChannelId = ?
AND DeleteAt = 0`, channelId)
if err != nil {
return 0, errors.Wrapf(err, "failed to count pinned Posts with channelId=%s", channelId)
}
return count, nil
}
//nolint:unparam
func (s SqlChannelStore) InvalidateGuestCount(channelId string) {
}
//nolint:unparam
func (s SqlChannelStore) GetGuestCount(channelId string, allowFromCache bool) (int64, error) {
var indexHint string
if s.DriverName() == model.DatabaseDriverMysql {
indexHint = `USE INDEX(idx_channelmembers_channel_id_scheme_guest_user_id)`
}
var count int64
err := s.GetReplicaX().Get(&count, `
SELECT
count(*)
FROM
ChannelMembers `+indexHint+`,
Users
WHERE
ChannelMembers.UserId = Users.Id
AND ChannelMembers.ChannelId = ?
AND ChannelMembers.SchemeGuest = TRUE
AND Users.DeleteAt = 0`, channelId)
if err != nil {
return 0, errors.Wrapf(err, "failed to count Guests with channelId=%s", channelId)
}
return count, nil
}
func (s SqlChannelStore) RemoveMembers(channelId string, userIds []string) error {
builder := s.getQueryBuilder().
Delete("ChannelMembers").
Where(sq.Eq{"ChannelId": channelId}).
Where(sq.Eq{"UserId": userIds})
query, args, err := builder.ToSql()
if err != nil {
return errors.Wrap(err, "channel_tosql")
}
_, err = s.GetMasterX().Exec(query, args...)
if err != nil {
return errors.Wrap(err, "failed to delete ChannelMembers")
}
// cleanup sidebarchannels table if the user is no longer a member of that channel
query, args, err = s.getQueryBuilder().
Delete("SidebarChannels").
Where(sq.And{
sq.Eq{"ChannelId": channelId},
sq.Eq{"UserId": userIds},
}).ToSql()
if err != nil {
return errors.Wrap(err, "channel_tosql")
}
_, err = s.GetMasterX().Exec(query, args...)
if err != nil {
return errors.Wrap(err, "failed to delete SidebarChannels")
}
return nil
}
func (s SqlChannelStore) RemoveMember(channelId string, userId string) error {
return s.RemoveMembers(channelId, []string{userId})
}
func (s SqlChannelStore) RemoveAllDeactivatedMembers(channelId string) error {
query := `
DELETE
FROM
ChannelMembers
WHERE
UserId IN (
SELECT
Id
FROM
Users
WHERE
Users.DeleteAt != 0
)
AND
ChannelMembers.ChannelId = ?
`
_, err := s.GetMasterX().Exec(query, channelId)
if err != nil {
return errors.Wrapf(err, "failed to delete ChannelMembers with channelId=%s", channelId)
}
return nil
}
func (s SqlChannelStore) PermanentDeleteMembersByUser(userId string) error {
if _, err := s.GetMasterX().Exec("DELETE FROM ChannelMembers WHERE UserId = ?", userId); err != nil {
return errors.Wrapf(err, "failed to permanent delete ChannelMembers with userId=%s", userId)
}
return nil
}
func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string) (map[string]int64, error) {
lastPostAtTimes := []struct {
Id string
LastPostAt int64
TotalMsgCount int64
TotalMsgCountRoot int64
}{}
// We use the question placeholder format for both databases, because
// we replace that with the dollar format later on.
// It's needed to support the prefix CTE query. See: https://github.com/Masterminds/squirrel/issues/285.
query := sq.StatementBuilder.PlaceholderFormat(sq.Question).
Select("Id, LastPostAt, TotalMsgCount, TotalMsgCountRoot").
From("Channels").
Where(sq.Eq{"Id": channelIds})
// TODO: use a CTE for mysql too when version 8 becomes the minimum supported version.
if s.DriverName() == model.DatabaseDriverPostgres {
with := query.Prefix("WITH c AS (").Suffix(") ,")
update := sq.StatementBuilder.PlaceholderFormat(sq.Question).
Update("ChannelMembers cm").
Set("MentionCount", 0).
Set("MentionCountRoot", 0).
Set("UrgentMentionCount", 0).
Set("MsgCount", sq.Expr("greatest(cm.MsgCount, c.TotalMsgCount)")).
Set("MsgCountRoot", sq.Expr("greatest(cm.MsgCountRoot, c.TotalMsgCountRoot)")).
Set("LastViewedAt", sq.Expr("greatest(cm.LastViewedAt, c.LastPostAt)")).
Set("LastUpdateAt", sq.Expr("greatest(cm.LastViewedAt, c.LastPostAt)")).
SuffixExpr(sq.Expr("FROM c WHERE cm.UserId = ? AND c.Id = cm.ChannelId", userId))
updateWrap := update.Prefix("updated AS (").Suffix(")")
query = with.SuffixExpr(updateWrap).Suffix("SELECT Id, LastPostAt FROM c")
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "UpdateLastViewedAt_CTE_Tosql")
}
if s.DriverName() == model.DatabaseDriverPostgres {
sql, err = sq.Dollar.ReplacePlaceholders(sql)
if err != nil {
return nil, errors.Wrap(err, "UpdateLastViewedAt_ReplacePlaceholders")
}
}
err = s.GetMasterX().Select(&lastPostAtTimes, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find ChannelMembers data with userId=%s and channelId in %v", userId, channelIds)
}
if len(lastPostAtTimes) == 0 {
return nil, store.NewErrInvalidInput("Channel", "Id", fmt.Sprintf("%v", channelIds))
}
times := map[string]int64{}
if s.DriverName() == model.DatabaseDriverPostgres {
for _, t := range lastPostAtTimes {
times[t.Id] = t.LastPostAt
}
return times, nil
}
var msgCountQuery, msgCountQueryRoot, lastViewedQuery = sq.Case("ChannelId"), sq.Case("ChannelId"), sq.Case("ChannelId")
for _, t := range lastPostAtTimes {
times[t.Id] = t.LastPostAt
msgCountQuery = msgCountQuery.When(
sq.Expr("?", t.Id),
sq.Expr("GREATEST(MsgCount, ?)", t.TotalMsgCount))
msgCountQueryRoot = msgCountQueryRoot.When(
sq.Expr("?", t.Id),
sq.Expr("GREATEST(MsgCountRoot, ?)", t.TotalMsgCountRoot))
lastViewedQuery = lastViewedQuery.When(
sq.Expr("?", t.Id),
sq.Expr("GREATEST(LastViewedAt, ?)", t.LastPostAt))
}
updateQuery := s.getQueryBuilder().Update("ChannelMembers").
Set("MentionCount", 0).
Set("MentionCountRoot", 0).
Set("UrgentMentionCount", 0).
Set("MsgCount", msgCountQuery).
Set("MsgCountRoot", msgCountQueryRoot).
Set("LastViewedAt", lastViewedQuery).
Set("LastUpdateAt", sq.Expr("LastViewedAt")).
Where(sq.Eq{
"UserId": userId,
"ChannelId": channelIds,
})
sql, args, err = updateQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "UpdateLastViewedAt_Update_Tosql")
}
if _, err := s.GetMasterX().Exec(sql, args...); err != nil {
return nil, errors.Wrapf(err, "failed to update ChannelMembers with userId=%s and channelId in %v", userId, channelIds)
}
return times, nil
}
func (s SqlChannelStore) CountUrgentPostsAfter(channelId string, timestamp int64, userId string) (int, error) {
query := s.getQueryBuilder().
Select("count(*)").
From("PostsPriority").
Join("Posts ON Posts.Id = PostsPriority.PostId").
Where(sq.And{
sq.Eq{"PostsPriority.Priority": model.PostPriorityUrgent},
sq.Eq{"Posts.ChannelId": channelId},
sq.Gt{"Posts.CreateAt": timestamp},
sq.Eq{"Posts.DeleteAt": 0},
})
if userId != "" {
query = query.Where(sq.Eq{"Posts.UserId": userId})
}
var urgent int64
err := s.GetReplicaX().GetBuilder(&urgent, query)
if err != nil {
return 0, errors.Wrap(err, "failed to count urgent Posts")
}
return int(urgent), nil
}
// CountPostsAfter returns the number of posts in the given channel created after but not including the given timestamp. If given a non-empty user ID, only counts posts made by that user.
func (s SqlChannelStore) CountPostsAfter(channelId string, timestamp int64, userId string) (int, int, error) {
joinLeavePostTypes := []string{
// These types correspond to the ones checked by Post.IsJoinLeaveMessage
model.PostTypeJoinLeave,
model.PostTypeAddRemove,
model.PostTypeJoinChannel,
model.PostTypeLeaveChannel,
model.PostTypeJoinTeam,
model.PostTypeLeaveTeam,
model.PostTypeAddToChannel,
model.PostTypeRemoveFromChannel,
model.PostTypeAddToTeam,
model.PostTypeRemoveFromTeam,
}
query := s.getQueryBuilder().
Select("count(*)").
From("Posts").
Where(sq.And{
sq.Eq{"ChannelId": channelId},
sq.Gt{"CreateAt": timestamp},
sq.NotEq{"Type": joinLeavePostTypes},
sq.Eq{"DeleteAt": 0},
})
if userId != "" {
query = query.Where(sq.Eq{"UserId": userId})
}
sql, args, err := query.ToSql()
if err != nil {
return 0, 0, errors.Wrap(err, "CountPostsAfter_ToSql1")
}
var unread int64
err = s.GetReplicaX().Get(&unread, sql, args...)
if err != nil {
return 0, 0, errors.Wrap(err, "failed to count Posts")
}
sql2, args2, err := query.Where(sq.Eq{"RootId": ""}).ToSql()
if err != nil {
return 0, 0, errors.Wrap(err, "CountPostsAfter_ToSql2")
}
var unreadRoot int64
err = s.GetReplicaX().Get(&unreadRoot, sql2, args2...)
if err != nil {
return 0, 0, errors.Wrap(err, "failed to count root Posts")
}
return int(unread), int(unreadRoot), nil
}
// UpdateLastViewedAtPost updates a ChannelMember as if the user last read the channel at the time of the given post.
// If the provided mentionCount is -1, the given post and all posts after it are considered to be mentions. Returns
// an updated model.ChannelUnreadAt that can be returned to the client.
func (s SqlChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount, mentionCountRoot, urgentMentionCount int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) {
unreadDate := unreadPost.CreateAt - 1
unread, unreadRoot, err := s.CountPostsAfter(unreadPost.ChannelId, unreadDate, "")
if err != nil {
return nil, err
}
if !setUnreadCountRoot {
unreadRoot = 0
}
params := map[string]any{
"mentions": mentionCount,
"mentionsroot": mentionCountRoot,
"urgentmentions": urgentMentionCount,
"unreadcount": unread,
"unreadcountroot": unreadRoot,
"lastviewedat": unreadDate,
"userid": userID,
"channelid": unreadPost.ChannelId,
"updatedat": model.GetMillis(),
}
// msg count uses the value from channels to prevent counting on older channels where no. of messages can be high.
// we only count the unread which will be a lot less in 99% cases
setUnreadQuery := `
UPDATE
ChannelMembers
SET
MentionCount = :mentions,
MentionCountRoot = :mentionsroot,
UrgentMentionCount = :urgentmentions,
MsgCount = (SELECT TotalMsgCount FROM Channels WHERE ID = :channelid) - :unreadcount,
MsgCountRoot = (SELECT TotalMsgCountRoot FROM Channels WHERE ID = :channelid) - :unreadcountroot,
LastViewedAt = :lastviewedat,
LastUpdateAt = :updatedat
WHERE
UserId = :userid
AND ChannelId = :channelid
`
_, err = s.GetMasterX().NamedExec(setUnreadQuery, params)
if err != nil {
return nil, errors.Wrap(err, "failed to update ChannelMembers")
}
chanUnreadQuery := `
SELECT
c.TeamId TeamId,
cm.UserId UserId,
cm.ChannelId ChannelId,
cm.MsgCount MsgCount,
cm.MsgCountRoot MsgCountRoot,
cm.MentionCount MentionCount,
cm.MentionCountRoot MentionCountRoot,
COALESCE(cm.UrgentMentionCount, 0) UrgentMentionCount,
cm.LastViewedAt LastViewedAt,
cm.NotifyProps NotifyProps
FROM
ChannelMembers cm
LEFT JOIN Channels c ON c.Id=cm.ChannelId
WHERE
cm.UserId = ?
AND cm.channelId = ?
AND c.DeleteAt = 0
`
result := &model.ChannelUnreadAt{}
if err = s.GetMasterX().Get(result, chanUnreadQuery, userID, unreadPost.ChannelId); err != nil {
return nil, errors.Wrapf(err, "failed to get ChannelMember with channelId=%s", unreadPost.ChannelId)
}
return result, nil
}
func (s SqlChannelStore) IncrementMentionCount(channelId string, userIDs []string, isRoot bool, isUrgent bool) error {
now := model.GetMillis()
rootInc := 0
if isRoot {
rootInc = 1
}
urgentInc := 0
if isUrgent {
urgentInc = 1
}
sql, args, err := s.getQueryBuilder().
Update("ChannelMembers").
Set("MentionCount", sq.Expr("MentionCount + 1")).
Set("MentionCountRoot", sq.Expr("MentionCountRoot + ?", rootInc)).
Set("UrgentMentionCount", sq.Expr("UrgentMentionCount + ?", urgentInc)).
Set("LastUpdateAt", now).
Where(sq.Eq{
"UserId": userIDs,
"ChannelId": channelId,
}).
ToSql()
if err != nil {
return errors.Wrap(err, "IncrementMentionCount_Tosql")
}
_, err = s.GetMasterX().Exec(sql, args...)
if err != nil {
return errors.Wrapf(err, "failed to Update ChannelMembers with channelId=%s and userId=%v", channelId, userIDs)
}
return nil
}
func (s SqlChannelStore) GetAll(teamId string) ([]*model.Channel, error) {
data := []*model.Channel{}
err := s.GetReplicaX().Select(&data, "SELECT * FROM Channels WHERE TeamId = ? AND Type != ? ORDER BY Name", teamId, model.ChannelTypeDirect)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Channels with teamId=%s", teamId)
}
return data, nil
}
func (s SqlChannelStore) GetChannelsByIds(channelIds []string, includeDeleted bool) ([]*model.Channel, error) {
query := s.getQueryBuilder().
Select("*").
From("Channels").
Where(sq.Eq{"Id": channelIds}).
OrderBy("Name")
if !includeDeleted {
query = query.Where(sq.Eq{"DeleteAt": 0})
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetChannelsByIds_tosql")
}
channels := []*model.Channel{}
err = s.GetReplicaX().Select(&channels, sql, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Channels")
}
return channels, nil
}
func (s SqlChannelStore) GetChannelsWithTeamDataByIds(channelIDs []string, includeDeleted bool) ([]*model.ChannelWithTeamData, error) {
query := s.getQueryBuilder().
Select("c.*",
"COALESCE(t.DisplayName, '') As TeamDisplayName",
"COALESCE(t.Name, '') AS TeamName",
"COALESCE(t.UpdateAt, 0) AS TeamUpdateAt").
From("Channels c").
LeftJoin("Teams t ON c.TeamId = t.Id").
Where(sq.Eq{"c.Id": channelIDs}).
OrderBy("c.Name")
if !includeDeleted {
query = query.Where(sq.Eq{"c.DeleteAt": 0})
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getChannelsWithTeamData_tosql")
}
channels := []*model.ChannelWithTeamData{}
err = s.GetReplicaX().Select(&channels, sql, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Channels")
}
return channels, nil
}
func (s SqlChannelStore) GetForPost(postId string) (*model.Channel, error) {
channel := model.Channel{}
if err := s.GetReplicaX().Get(
&channel,
`SELECT
Channels.*
FROM
Channels,
Posts
WHERE
Channels.Id = Posts.ChannelId
AND Posts.Id = ?`, postId); err != nil {
return nil, errors.Wrapf(err, "failed to get Channel with postId=%s", postId)
}
return &channel, nil
}
func (s SqlChannelStore) AnalyticsTypeCount(teamId string, channelType model.ChannelType) (int64, error) {
query := s.getQueryBuilder().
Select("COUNT(*) AS Value").
From("Channels")
if channelType != "" {
query = query.Where(sq.Eq{"Type": channelType})
}
if teamId != "" {
query = query.Where(sq.Eq{"TeamId": teamId})
}
sql, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "AnalyticsTypeCount_ToSql")
}
var value int64
err = s.GetReplicaX().Get(&value, sql, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to count Channels")
}
return value, nil
}
func (s SqlChannelStore) AnalyticsDeletedTypeCount(teamId string, channelType model.ChannelType) (int64, error) {
query := s.getQueryBuilder().
Select("COUNT(Id) AS Value").
From("Channels").
Where(sq.And{
sq.Eq{"Type": channelType},
sq.Gt{"DeleteAt": 0},
})
if teamId != "" {
query = query.Where(sq.Eq{"TeamId": teamId})
}
sql, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "AnalyticsDeletedTypeCount_ToSql")
}
var v int64
err = s.GetReplicaX().Get(&v, sql, args...)
if err != nil {
return 0, errors.Wrapf(err, "failed to count Channels with teamId=%s and channelType=%s", teamId, channelType)
}
return v, nil
}
func (s SqlChannelStore) GetMembersForUser(teamID string, userID string) (model.ChannelMembers, error) {
sql, args, err := s.channelMembersForTeamWithSchemeSelectQuery.
Where(sq.And{
sq.Eq{"ChannelMembers.UserId": userID},
sq.Or{
sq.Eq{"Teams.Id": teamID},
sq.Eq{"Teams.Id": ""},
sq.Eq{"Teams.Id": nil},
},
}).ToSql()
if err != nil {
return nil, errors.Wrapf(err, "GetMembersForUser_ToSql teamID=%s userID=%s", teamID, userID)
}
dbMembers := channelMemberWithSchemeRolesList{}
err = s.GetReplicaX().Select(&dbMembers, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find ChannelMembers data with teamId=%s and userId=%s", teamID, userID)
}
return dbMembers.ToModel(), nil
}
func (s SqlChannelStore) GetMembersForUserWithCursor(userID, teamID string, opts *store.ChannelMemberGraphQLSearchOpts) (model.ChannelMembers, error) {
query := s.getQueryBuilder().
Select(
"ChannelMembers.ChannelId",
"ChannelMembers.UserId",
"ChannelMembers.Roles",
"ChannelMembers.LastViewedAt",
"ChannelMembers.MsgCount",
"ChannelMembers.MentionCount",
"ChannelMembers.MentionCountRoot",
"COALESCE(ChannelMembers.UrgentMentionCount, 0) AS UrgentMentionCount",
"ChannelMembers.MsgCountRoot",
"ChannelMembers.NotifyProps",
"ChannelMembers.LastUpdateAt",
"ChannelMembers.SchemeUser",
"ChannelMembers.SchemeAdmin",
"ChannelMembers.SchemeGuest",
"TeamScheme.DefaultChannelGuestRole TeamSchemeDefaultGuestRole",
"TeamScheme.DefaultChannelUserRole TeamSchemeDefaultUserRole",
"TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole",
"ChannelScheme.DefaultChannelGuestRole ChannelSchemeDefaultGuestRole",
"ChannelScheme.DefaultChannelUserRole ChannelSchemeDefaultUserRole",
"ChannelScheme.DefaultChannelAdminRole ChannelSchemeDefaultAdminRole").
From("ChannelMembers").
InnerJoin("Channels ON ChannelMembers.ChannelId = Channels.Id").
LeftJoin("Schemes ChannelScheme ON Channels.SchemeId = ChannelScheme.Id").
LeftJoin("Teams ON Channels.TeamId = Teams.Id").
LeftJoin("Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id").
Where(sq.Eq{
"ChannelMembers.UserId": userID,
"Channels.DeleteAt": 0,
}).
OrderBy("ChannelId, UserId ASC").
// The limit is verified at the GraphQL layer.
Limit(uint64(opts.Limit))
if teamID != "" {
if opts.ExcludeTeam {
// Exclude this team and DM/GMs
query = query.Where(sq.And{
sq.NotEq{"Channels.TeamId": teamID},
sq.NotEq{"Channels.TeamId": ""},
})
} else {
// Include this team and DM/GMs
query = query.Where(sq.Or{
sq.Eq{"Channels.TeamId": teamID},
sq.Eq{"Channels.TeamId": ""},
})
}
}
if opts.AfterChannel != "" && opts.AfterUser != "" {
query = query.Where(sq.Or{
sq.Gt{"ChannelMembers.ChannelId": opts.AfterChannel},
sq.And{
sq.Eq{"ChannelMembers.ChannelId": opts.AfterChannel},
sq.Gt{"ChannelMembers.UserId": opts.AfterUser},
},
})
}
if opts.LastUpdateAt != 0 {
query = query.Where(sq.GtOrEq{"ChannelMembers.LastUpdateAt": opts.LastUpdateAt})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "getMembersForUserWithCursor_tosql")
}
dbMembers := channelMemberWithSchemeRolesList{}
err = s.GetReplicaX().Select(&dbMembers, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find ChannelMembers data with userId=%s", userID)
}
return dbMembers.ToModel(), nil
}
func (s SqlChannelStore) GetMembersForUserWithPagination(userId string, page, perPage int) (model.ChannelMembersWithTeamData, error) {
dbMembers := channelMemberWithTeamWithSchemeRolesList{}
offset := page * perPage
err := s.GetReplicaX().Select(&dbMembers, channelMembersWithSchemeSelectQuery+"WHERE ChannelMembers.UserId = ? ORDER BY ChannelId ASC Limit ? Offset ?", userId, perPage, offset)
if err != nil {
return nil, errors.Wrapf(err, "failed to find ChannelMembers data with and userId=%s", userId)
}
return dbMembers.ToModel(), nil
}
func (s SqlChannelStore) GetTeamMembersForChannel(channelID string) ([]string, error) {
teamMemberIDs := []string{}
if err := s.GetReplicaX().Select(&teamMemberIDs, `SELECT tm.UserId
FROM Channels c, Teams t, TeamMembers tm
WHERE
c.TeamId=t.Id
AND
t.Id=tm.TeamId
AND
c.Id = ?`,
channelID); err != nil {
return nil, errors.Wrapf(err, "error while getting team members for a channel")
}
return teamMemberIDs, nil
}
func (s SqlChannelStore) Autocomplete(userID, term string, includeDeleted, isGuest bool) (model.ChannelListWithTeamData, error) {
query := s.getQueryBuilder().Select("c.*",
"t.DisplayName AS TeamDisplayName",
"t.Name AS TeamName",
"t.UpdateAt AS TeamUpdateAt").
From("Channels c, Teams t, TeamMembers tm").
Where(sq.And{
sq.Expr("c.TeamId = t.id"),
sq.Expr("t.id = tm.TeamId"),
sq.Eq{"tm.UserId": userID},
}).
OrderBy("c.DisplayName")
if !includeDeleted {
query = query.Where(sq.And{
sq.Eq{"c.DeleteAt": 0},
sq.Eq{"tm.DeleteAt": 0},
})
}
if isGuest {
query = query.Where(sq.Expr("c.Id IN (?)", sq.Select("ChannelId").
From("ChannelMembers").
Where(sq.Eq{"UserId": userID})))
} else {
query = query.Where(sq.Or{
sq.NotEq{"c.Type": model.ChannelTypePrivate},
sq.And{
sq.Eq{"c.Type": model.ChannelTypePrivate},
sq.Expr("c.Id IN (?)", sq.Select("ChannelId").
From("ChannelMembers").
Where(sq.Eq{"UserId": userID})),
},
})
}
searchClause := s.searchClause(term)
if searchClause != nil {
query = query.Where(searchClause)
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Autocomplete_Tosql")
}
channels := model.ChannelListWithTeamData{}
err = s.GetReplicaX().Select(&channels, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "could not find channel with term=%s", term)
}
return channels, nil
}
func (s SqlChannelStore) AutocompleteInTeam(teamID, userID, term string, includeDeleted, isGuest bool) (model.ChannelList, error) {
query := s.getQueryBuilder().Select("*").
From("Channels c").
Where(sq.Eq{"c.TeamId": teamID}).
OrderBy("c.DisplayName").
Limit(model.ChannelSearchDefaultLimit)
if !includeDeleted {
query = query.Where(sq.Eq{"c.DeleteAt": 0})
}
if isGuest {
query = query.Where(sq.Expr("c.Id IN (?)", sq.Select("ChannelId").
From("ChannelMembers").
Where(sq.Eq{"UserId": userID})))
} else {
query = query.Where(sq.Or{
sq.NotEq{"c.Type": model.ChannelTypePrivate},
sq.And{
sq.Eq{"c.Type": model.ChannelTypePrivate},
sq.Expr("c.Id IN (?)", sq.Select("ChannelId").
From("ChannelMembers").
Where(sq.Eq{"UserId": userID})),
},
})
}
searchClause := s.searchClause(term)
if searchClause != nil {
query = query.Where(searchClause)
}
return s.performSearch(query, term)
}
func (s SqlChannelStore) AutocompleteInTeamForSearch(teamID string, userID string, term string, includeDeleted bool) (model.ChannelList, error) {
// shared query
query := s.getSubQueryBuilder().Select("C.*").
From("Channels AS C").
Join("ChannelMembers AS CM ON CM.ChannelId = C.Id").
Limit(50).
Where(sq.And{
sq.Or{
sq.Eq{"C.TeamId": teamID},
sq.Eq{
"C.TeamId": "",
"C.Type": model.ChannelTypeGroup,
},
},
sq.Eq{"CM.UserId": userID},
})
if !includeDeleted {
// include the DeleteAt = 0 condition
query.Where(sq.Eq{"DeleteAt": 0})
}
var (
channels = model.ChannelList{}
sql string
args []any
)
// build the like clause
like := s.buildLIKEClauseX(term, "Name", "DisplayName", "Purpose")
if like == nil {
var err error
// generate the SQL query
sql, args, err = query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "AutocompleteInTeamForSearch_Tosql")
}
} else {
// build the full text search clause
full := s.buildFulltextClauseX(term, "Name", "DisplayName", "Purpose")
// build the LIKE query
likeSQL, likeArgs, err := query.Where(like).ToSql()
if err != nil {
return nil, errors.Wrap(err, "AutocompleteInTeamForSearch_Like_Tosql")
}
// build the full text query
fullSQL, fullArgs, err := query.Where(full).ToSql()
if err != nil {
return nil, errors.Wrap(err, "AutocompleteInTeamForSearch_Full_Tosql")
}
// Using a UNION results in index_merge and fulltext queries and is much faster than the ref
// query you would get using an OR of the LIKE and full-text clauses.
sql = fmt.Sprintf("(%s) UNION (%s) LIMIT 50", likeSQL, fullSQL)
args = append(likeArgs, fullArgs...)
}
var err error
// since the UNION is not part of squirrel, we need to assemble it and then update
// the placeholders manually
if s.DriverName() == model.DatabaseDriverPostgres {
sql, err = sq.Dollar.ReplacePlaceholders(sql)
if err != nil {
return nil, errors.Wrap(err, "AutocompleteInTeamForSearch_Placeholder")
}
}
// query the database
err = s.GetReplicaX().Select(&channels, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Channels with term='%s'", term)
}
directChannels, err := s.autocompleteInTeamForSearchDirectMessages(userID, term)
if err != nil {
return nil, err
}
channels = append(channels, directChannels...)
sort.Slice(channels, func(a, b int) bool {
return strings.ToLower(channels[a].DisplayName) < strings.ToLower(channels[b].DisplayName)
})
return channels, nil
}
func (s SqlChannelStore) autocompleteInTeamForSearchDirectMessages(userID string, term string) ([]*model.Channel, error) {
// create the main query
query := s.getQueryBuilder().Select("C.*", "OtherUsers.Username as DisplayName").
From("Channels AS C").
Join("ChannelMembers AS CM ON CM.ChannelId = C.Id").
Where(sq.Eq{
"C.Type": model.ChannelTypeDirect,
"CM.UserId": userID,
}).
Limit(50)
// create the subquery
subQuery := s.getSubQueryBuilder().Select("ICM.ChannelId AS ChannelId", "IU.Username AS Username").
From("Users AS IU").
Join("ChannelMembers AS ICM ON ICM.UserId = IU.Id").
Where(sq.NotEq{"IU.Id": userID})
// try to create a LIKE clause from the search term
if like := s.buildLIKEClauseX(term, "IU.Username", "IU.Nickname"); like != nil {
subQuery = subQuery.Where(like)
}
// put the subquery into an INNER JOIN
innerJoin := subQuery.
Prefix("INNER JOIN (").
Suffix(") AS OtherUsers ON OtherUsers.ChannelId = C.Id")
// add the subquery to the main query
query = query.JoinClause(innerJoin)
// create the SQL query and argument list
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "autocompleteInTeamForSearchDirectMessages_InnerJoin_Tosql")
}
// query the channel list from the database using SQLX
channels := model.ChannelList{}
if err := s.GetReplicaX().Select(&channels, sql, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Channels with term='%s' (%s %% %v)", term, sql, args)
}
return channels, nil
}
func (s SqlChannelStore) SearchInTeam(teamId string, term string, includeDeleted bool) (model.ChannelList, error) {
query := s.getQueryBuilder().Select("Channels.*").
From("Channels").
Join("PublicChannels c ON (c.Id = Channels.Id)").
Where(sq.Eq{"c.TeamId": teamId}).
OrderBy("c.DisplayName").
Limit(100)
if !includeDeleted {
query = query.Where(sq.Eq{"c.DeleteAt": 0})
}
if term != "" {
searchClause := s.searchClause(term)
if searchClause != nil {
query = query.Where(searchClause)
}
}
return s.performSearch(query, term)
}
func (s SqlChannelStore) SearchArchivedInTeam(teamId string, term string, userId string) (model.ChannelList, error) {
queryBase := s.getQueryBuilder().Select("Channels.*").
From("Channels").
Join("Channels c ON (c.Id = Channels.Id)").
Where(sq.And{
sq.Eq{"c.TeamId": teamId},
sq.NotEq{"c.DeleteAt": 0},
}).
OrderBy("c.DisplayName").
Limit(100)
searchClause := s.searchClause(term)
if searchClause != nil {
queryBase = queryBase.Where(searchClause)
}
publicQuery := queryBase.
Where(sq.NotEq{"c.Type": model.ChannelTypePrivate})
privateQuery := queryBase.
Where(
sq.And{
sq.Eq{"c.Type": model.ChannelTypePrivate},
sq.Expr("c.Id IN (?)", sq.Select("ChannelId").
From("ChannelMembers").
Where(sq.Eq{"UserId": userId})),
})
publicChannels, err := s.performSearch(publicQuery, term)
if err != nil {
return nil, err
}
privateChannels, err := s.performSearch(privateQuery, term)
if err != nil {
return nil, err
}
output := publicChannels
output = append(output, privateChannels...)
return output, nil
}
func (s SqlChannelStore) SearchForUserInTeam(userId string, teamId string, term string, includeDeleted bool) (model.ChannelList, error) {
query := s.getQueryBuilder().Select("Channels.*").
From("Channels").
Join("PublicChannels c ON (c.Id = Channels.Id)").
Join("ChannelMembers cm ON (c.Id = cm.ChannelId)").
Where(sq.Eq{
"c.TeamId": teamId,
"cm.UserId": userId,
}).
OrderBy("c.DisplayName").
Limit(100)
if !includeDeleted {
query = query.Where(sq.Eq{"c.DeleteAt": 0})
}
searchClause := s.searchClause(term)
if searchClause != nil {
query = query.Where(searchClause)
}
return s.performSearch(query, term)
}
func (s SqlChannelStore) channelSearchQuery(opts *store.ChannelSearchOpts) sq.SelectBuilder {
var limit int
if opts.PerPage != nil {
limit = *opts.PerPage
} else {
limit = 100
}
var selectStr string
if opts.CountOnly {
selectStr = "count(*)"
} else {
selectStr = "c.*"
if opts.IncludeTeamInfo {
selectStr += ", t.DisplayName AS TeamDisplayName, t.Name AS TeamName, t.UpdateAt as TeamUpdateAt"
}
if opts.IncludePolicyID {
selectStr += ", RetentionPoliciesChannels.PolicyId AS PolicyID"
}
}
query := s.getQueryBuilder().
Select(selectStr).
From("Channels AS c").
Join("Teams AS t ON t.Id = c.TeamId")
// don't bother ordering or limiting if we're just getting the count
if !opts.CountOnly {
query = query.
OrderBy("c.DisplayName, t.DisplayName").
Limit(uint64(limit))
}
if opts.Deleted {
query = query.Where(sq.NotEq{"c.DeleteAt": int(0)})
} else if !opts.IncludeDeleted {
query = query.Where(sq.Eq{"c.DeleteAt": int(0)})
}
if opts.IsPaginated() && !opts.CountOnly {
query = query.Offset(uint64(*opts.Page * *opts.PerPage))
}
if opts.PolicyID != "" {
query = query.
InnerJoin("RetentionPoliciesChannels ON c.Id = RetentionPoliciesChannels.ChannelId").
Where(sq.Eq{"RetentionPoliciesChannels.PolicyId": opts.PolicyID})
} else if opts.ExcludePolicyConstrained {
query = query.
LeftJoin("RetentionPoliciesChannels ON c.Id = RetentionPoliciesChannels.ChannelId").
Where("RetentionPoliciesChannels.ChannelId IS NULL")
} else if opts.IncludePolicyID {
query = query.
LeftJoin("RetentionPoliciesChannels ON c.Id = RetentionPoliciesChannels.ChannelId")
}
likeFields := "c.Name, c.DisplayName, c.Purpose"
if opts.IncludeSearchById {
likeFields = likeFields + ", c.Id"
}
likeClause, likeTerm := s.buildLIKEClause(opts.Term, likeFields)
if likeTerm != "" {
// Keep the number of likeTerms same as the number of columns
// (c.Name, c.DisplayName, c.Purpose, c.Id?)
likeTerms := make([]any, len(strings.Split(likeFields, ",")))
for i := 0; i < len(likeTerms); i++ {
likeTerms[i] = likeTerm
}
likeClause = strings.ReplaceAll(likeClause, ":LikeTerm", "?")
fulltextClause, fulltextTerm := s.buildFulltextClause(opts.Term, "c.Name, c.DisplayName, c.Purpose")
fulltextClause = strings.ReplaceAll(fulltextClause, ":FulltextTerm", "?")
query = query.Where(sq.Or{
sq.Expr(likeClause, likeTerms...),
sq.Expr(fulltextClause, fulltextTerm),
})
}
if len(opts.ExcludeChannelNames) > 0 {
query = query.Where(sq.NotEq{"c.Name": opts.ExcludeChannelNames})
}
if opts.NotAssociatedToGroup != "" {
query = query.Where("c.Id NOT IN (SELECT ChannelId FROM GroupChannels WHERE GroupChannels.GroupId = ? AND GroupChannels.DeleteAt = 0)", opts.NotAssociatedToGroup)
}
if len(opts.TeamIds) > 0 {
query = query.Where(sq.Eq{"c.TeamId": opts.TeamIds})
}
if opts.GroupConstrained {
query = query.Where(sq.Eq{"c.GroupConstrained": true})
} else if opts.ExcludeGroupConstrained {
query = query.Where(sq.Or{
sq.NotEq{"c.GroupConstrained": true},
sq.Eq{"c.GroupConstrained": nil},
})
}
if opts.Public && !opts.Private {
query = query.InnerJoin("PublicChannels ON c.Id = PublicChannels.Id")
} else if opts.Private && !opts.Public {
query = query.Where(sq.Eq{"c.Type": model.ChannelTypePrivate})
} else {
query = query.Where(sq.Or{
sq.Eq{"c.Type": model.ChannelTypeOpen},
sq.Eq{"c.Type": model.ChannelTypePrivate},
})
}
return query
}
func (s SqlChannelStore) SearchAllChannels(term string, opts store.ChannelSearchOpts) (model.ChannelListWithTeamData, int64, error) {
opts.Term = term
opts.IncludeTeamInfo = true
queryString, args, err := s.channelSearchQuery(&opts).ToSql()
if err != nil {
return nil, 0, errors.Wrap(err, "channel_tosql")
}
channels := model.ChannelListWithTeamData{}
if err2 := s.GetReplicaX().Select(&channels, queryString, args...); err2 != nil {
return nil, 0, errors.Wrapf(err2, "failed to find Channels with term='%s'", term)
}
var totalCount int64
// only query a 2nd time for the count if the results are being requested paginated.
if opts.IsPaginated() {
opts.CountOnly = true
queryString, args, err = s.channelSearchQuery(&opts).ToSql()
if err != nil {
return nil, 0, errors.Wrap(err, "channel_tosql")
}
if err2 := s.GetReplicaX().Get(&totalCount, queryString, args...); err2 != nil {
return nil, 0, errors.Wrapf(err2, "failed to find Channels with term='%s'", term)
}
} else {
totalCount = int64(len(channels))
}
return channels, totalCount, nil
}
func (s SqlChannelStore) SearchMore(userId string, teamId string, term string) (model.ChannelList, error) {
teamQuery := s.getSubQueryBuilder().Select("c.Id").
From("PublicChannels c").
Join("ChannelMembers cm ON (cm.ChannelId = c.Id)").
Where(sq.Eq{
"c.TeamId": teamId,
"cm.UserId": userId,
"c.DeleteAt": 0,
})
query := s.getQueryBuilder().Select("Channels.*").
From("Channels").
Join("PublicChannels c ON (c.Id=Channels.Id)").
Where(sq.And{
sq.Eq{"c.TeamId": teamId},
sq.Eq{"c.DeleteAt": 0},
sq.Expr("c.Id NOT IN (?)", teamQuery),
}).
OrderBy("c.DisplayName").
Limit(100)
searchClause := s.searchClause(term)
if searchClause != nil {
query = query.Where(searchClause)
}
return s.performSearch(query, term)
}
func (s SqlChannelStore) buildLIKEClause(term string, searchColumns string) (likeClause, likeTerm string) {
likeTerm = sanitizeSearchTerm(term, "*")
if likeTerm == "" {
return
}
// Prepare the LIKE portion of the query.
var searchFields []string
for _, field := range strings.Split(searchColumns, ", ") {
if s.DriverName() == model.DatabaseDriverPostgres {
searchFields = append(searchFields, fmt.Sprintf("lower(%s) LIKE lower(%s) escape '*'", field, ":LikeTerm"))
} else {
searchFields = append(searchFields, fmt.Sprintf("%s LIKE %s escape '*'", field, ":LikeTerm"))
}
}
likeClause = fmt.Sprintf("(%s)", strings.Join(searchFields, " OR "))
likeTerm = wildcardSearchTerm(likeTerm)
return
}
func (s SqlChannelStore) buildLIKEClauseX(term string, searchColumns ...string) sq.Sqlizer {
// escape the special characters with *
likeTerm := sanitizeSearchTerm(term, "*")
if likeTerm == "" {
return nil
}
// add a placeholder at the beginning and end
likeTerm = wildcardSearchTerm(likeTerm)
// Prepare the LIKE portion of the query.
var searchFields sq.Or
for _, field := range searchColumns {
if s.DriverName() == model.DatabaseDriverPostgres {
expr := fmt.Sprintf("LOWER(%s) LIKE LOWER(?) ESCAPE '*'", field)
searchFields = append(searchFields, sq.Expr(expr, likeTerm))
} else {
expr := fmt.Sprintf("%s LIKE ? ESCAPE '*'", field)
searchFields = append(searchFields, sq.Expr(expr, likeTerm))
}
}
return searchFields
}
const spaceFulltextSearchChars = "<>+-()~:*\"!@"
func (s SqlChannelStore) buildFulltextClause(term string, searchColumns string) (fulltextClause, fulltextTerm string) {
// Copy the terms as we will need to prepare them differently for each search type.
fulltextTerm = term
// These chars must be treated as spaces in the fulltext query.
fulltextTerm = strings.Map(func(r rune) rune {
if strings.ContainsRune(spaceFulltextSearchChars, r) {
return ' '
}
return r
}, fulltextTerm)
// Prepare the FULLTEXT portion of the query.
if s.DriverName() == model.DatabaseDriverPostgres {
fulltextTerm = strings.ReplaceAll(fulltextTerm, "|", "")
splitTerm := strings.Fields(fulltextTerm)
for i, t := range strings.Fields(fulltextTerm) {
splitTerm[i] = t + ":*"
}
fulltextTerm = strings.Join(splitTerm, " & ")
fulltextClause = fmt.Sprintf("((to_tsvector('%[1]s', %[2]s)) @@ to_tsquery('%[1]s', :FulltextTerm))", s.pgDefaultTextSearchConfig, convertMySQLFullTextColumnsToPostgres(searchColumns))
} else if s.DriverName() == model.DatabaseDriverMysql {
splitTerm := strings.Fields(fulltextTerm)
for i, t := range strings.Fields(fulltextTerm) {
splitTerm[i] = "+" + t + "*"
}
fulltextTerm = strings.Join(splitTerm, " ")
fulltextClause = fmt.Sprintf("MATCH(%s) AGAINST (:FulltextTerm IN BOOLEAN MODE)", searchColumns)
}
return
}
func (s SqlChannelStore) buildFulltextClauseX(term string, searchColumns ...string) sq.Sqlizer {
// Copy the terms as we will need to prepare them differently for each search type.
fulltextTerm := term
// These chars must be treated as spaces in the fulltext query.
fulltextTerm = strings.Map(func(r rune) rune {
if strings.ContainsRune(spaceFulltextSearchChars, r) {
return ' '
}
return r
}, fulltextTerm)
// Prepare the FULLTEXT portion of the query.
if s.DriverName() == model.DatabaseDriverPostgres {
// remove all pipes |
fulltextTerm = strings.ReplaceAll(fulltextTerm, "|", "")
// split the search term and append :* to each part
splitTerm := strings.Fields(fulltextTerm)
for i, t := range splitTerm {
splitTerm[i] = t + ":*"
}
// join the search term with &
fulltextTerm = strings.Join(splitTerm, " & ")
expr := fmt.Sprintf("((to_tsvector('%[1]s', %[2]s)) @@ to_tsquery('%[1]s', ?))", s.pgDefaultTextSearchConfig, strings.Join(searchColumns, " || ' ' || "))
return sq.Expr(expr, fulltextTerm)
}
splitTerm := strings.Fields(fulltextTerm)
for i, t := range splitTerm {
splitTerm[i] = "+" + t + "*"
}
fulltextTerm = strings.Join(splitTerm, " ")
expr := fmt.Sprintf("MATCH(%s) AGAINST (? IN BOOLEAN MODE)", strings.Join(searchColumns, ", "))
return sq.Expr(expr, fulltextTerm)
}
func (s SqlChannelStore) performSearch(searchQuery sq.SelectBuilder, term string) (model.ChannelList, error) {
sql, args, err := searchQuery.ToSql()
if err != nil {
return model.ChannelList{}, errors.Wrapf(err, "performSearch_ToSql")
}
channels := model.ChannelList{}
err = s.GetReplicaX().Select(&channels, sql, args...)
if err != nil {
return channels, errors.Wrapf(err, "failed to find Channels with term='%s'", term)
}
return channels, nil
}
func (s SqlChannelStore) searchClause(term string) sq.Sqlizer {
likeClause := s.buildLIKEClauseX(term, "c.Name", "c.DisplayName", "c.Purpose")
if likeClause == nil {
return nil
}
fulltextClause := s.buildFulltextClauseX(term, "c.Name", "c.DisplayName", "c.Purpose")
return sq.Or{
likeClause,
fulltextClause,
}
}
func (s SqlChannelStore) searchGroupChannelsQuery(userId, term string, isPostgreSQL bool) sq.SelectBuilder {
var baseLikeTerm string
terms := strings.Fields((strings.ToLower(term)))
having := sq.And{}
if isPostgreSQL {
baseLikeTerm = "ARRAY_TO_STRING(ARRAY_AGG(u.Username), ', ') LIKE ?"
cc := s.getSubQueryBuilder().Select("c.Id").
From("Channels c").
Join("ChannelMembers cm ON c.Id=cm.ChannelId").
Join("Users u on u.Id = cm.UserId").
Where(sq.Eq{
"c.Type": model.ChannelTypeGroup,
"u.id": userId,
}).
GroupBy("c.Id")
for _, term := range terms {
term = sanitizeSearchTerm(term, "\\")
having = append(having, sq.Expr(baseLikeTerm, "%"+term+"%"))
}
subq := s.getSubQueryBuilder().Select("cc.id").
FromSelect(cc, "cc").
Join("ChannelMembers cm On cc.Id = cm.ChannelId").
Join("Users u On u.Id = cm.UserId").
GroupBy("cc.Id").
Having(having).
Limit(model.ChannelSearchDefaultLimit)
return s.getQueryBuilder().Select("*").
From("Channels").
Where(sq.Expr("Id IN (?)", subq))
}
baseLikeTerm = "GROUP_CONCAT(u.Username SEPARATOR ', ') LIKE ?"
for _, term := range terms {
term = sanitizeSearchTerm(term, "\\")
having = append(having, sq.Expr(baseLikeTerm, "%"+term+"%"))
}
cc := s.getSubQueryBuilder().Select("c.*").
From("Channels c").
Join("ChannelMembers cm ON c.Id=cm.ChannelId").
Join("Users u on u.Id = cm.UserId").
Where(sq.Eq{
"c.Type": model.ChannelTypeGroup,
"u.Id": userId,
}).
GroupBy("c.Id")
return s.getQueryBuilder().Select("cc.*").
FromSelect(cc, "cc").
Join("ChannelMembers cm on cc.Id = cm.ChannelId").
Join("Users u on u.Id = cm.UserId").
GroupBy("cc.Id").
Having(having).
Limit(model.ChannelSearchDefaultLimit)
}
func (s SqlChannelStore) SearchGroupChannels(userId, term string) (model.ChannelList, error) {
isPostgreSQL := s.DriverName() == model.DatabaseDriverPostgres
query := s.searchGroupChannelsQuery(userId, term, isPostgreSQL)
sql, params, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "SearchGroupChannels_Tosql")
}
groupChannels := model.ChannelList{}
if err := s.GetReplicaX().Select(&groupChannels, sql, params...); err != nil {
return nil, errors.Wrapf(err, "failed to find Channels with term='%s' and userId=%s", term, userId)
}
return groupChannels, nil
}
func (s SqlChannelStore) GetMembersByIds(channelID string, userIDs []string) (model.ChannelMembers, error) {
query := s.channelMembersForTeamWithSchemeSelectQuery.Where(
sq.Eq{
"ChannelMembers.ChannelId": channelID,
"ChannelMembers.UserId": userIDs,
},
)
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetMembersByIds_ToSql")
}
dbMembers := channelMemberWithSchemeRolesList{}
if err := s.GetReplicaX().Select(&dbMembers, sql, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find ChannelMembers with channelId=%s and userId in %v", channelID, userIDs)
}
return dbMembers.ToModel(), nil
}
func (s SqlChannelStore) GetMembersByChannelIds(channelIDs []string, userID string) (model.ChannelMembers, error) {
query := s.channelMembersForTeamWithSchemeSelectQuery.Where(
sq.Eq{
"ChannelMembers.ChannelId": channelIDs,
"ChannelMembers.UserId": userID,
},
)
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetMembersByChannelIds_ToSql")
}
dbMembers := channelMemberWithSchemeRolesList{}
if err := s.GetReplicaX().Select(&dbMembers, sql, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find ChannelMembers with userId=%s and channelId in %v", userID, channelIDs)
}
return dbMembers.ToModel(), nil
}
func (s SqlChannelStore) GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error) {
query := s.getQueryBuilder().
Select("Channels.Id as ChannelId, Users.Id, Users.FirstName, Users.LastName, Users.Nickname, Users.Username").
From("ChannelMembers as cm").
Join("Channels ON cm.ChannelId = Channels.Id").
Join("Users ON cm.UserId = Users.Id").
Where(sq.Eq{
"Channels.Id": channelIDs,
"Channels.DeleteAt": 0,
})
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "dm_gm_names_tosql")
}
res := []*struct {
model.User
ChannelId string
}{}
if err := s.GetReplicaX().Select(&res, sql, args...); err != nil {
return nil, errors.Wrap(err, "failed to find channels display name")
}
if len(res) == 0 {
return nil, store.NewErrNotFound("User", fmt.Sprintf("%v", channelIDs))
}
userInfo := make(map[string][]*model.User)
for _, item := range res {
userInfo[item.ChannelId] = append(userInfo[item.ChannelId], &item.User)
}
return userInfo, nil
}
func (s SqlChannelStore) GetChannelsByScheme(schemeId string, offset int, limit int) (model.ChannelList, error) {
channels := model.ChannelList{}
err := s.GetReplicaX().Select(&channels, "SELECT * FROM Channels WHERE SchemeId = ? ORDER BY DisplayName LIMIT ? OFFSET ?", schemeId, limit, offset)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Channels with schemeId=%s", schemeId)
}
return channels, nil
}
// This function does the Advanced Permissions Phase 2 migration for ChannelMember objects. It performs the migration
// in batches as a single transaction per batch to ensure consistency but to also minimise execution time to avoid
// causing unnecessary table locks. **THIS FUNCTION SHOULD NOT BE USED FOR ANY OTHER PURPOSE.** Executing this function
// *after* the new Schemes functionality has been used on an installation will have unintended consequences.
func (s SqlChannelStore) MigrateChannelMembers(fromChannelId string, fromUserId string) (_ map[string]string, err error) {
var transaction *sqlxTxWrapper
if transaction, err = s.GetMasterX().Beginx(); err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
channelMembers := []channelMember{}
query := `
SELECT
ChannelId,
UserId,
Roles,
LastViewedAt,
MsgCount,
MentionCount,
MentionCountRoot,
COALESCE(UrgentMentionCount, 0) AS UrgentMentionCount,
MsgCountRoot,
NotifyProps,
LastUpdateAt,
SchemeUser,
SchemeAdmin,
SchemeGuest
FROM
ChannelMembers
WHERE
(ChannelId, UserId) > (?, ?)
ORDER BY ChannelId, UserId
LIMIT 100
`
if err := transaction.Select(&channelMembers, query, fromChannelId, fromUserId); err != nil {
return nil, errors.Wrap(err, "failed to find ChannelMembers")
}
if len(channelMembers) == 0 {
// No more channel members in query result means that the migration has finished.
return nil, nil
}
for i := range channelMembers {
member := channelMembers[i]
roles := strings.Fields(member.Roles)
var newRoles []string
if !member.SchemeAdmin.Valid {
member.SchemeAdmin = sql.NullBool{Bool: false, Valid: true}
}
if !member.SchemeUser.Valid {
member.SchemeUser = sql.NullBool{Bool: false, Valid: true}
}
if !member.SchemeGuest.Valid {
member.SchemeGuest = sql.NullBool{Bool: false, Valid: true}
}
for _, role := range roles {
if role == model.ChannelAdminRoleId {
member.SchemeAdmin = sql.NullBool{Bool: true, Valid: true}
} else if role == model.ChannelUserRoleId {
member.SchemeUser = sql.NullBool{Bool: true, Valid: true}
} else if role == model.ChannelGuestRoleId {
member.SchemeGuest = sql.NullBool{Bool: true, Valid: true}
} else {
newRoles = append(newRoles, role)
}
}
member.Roles = strings.Join(newRoles, " ")
if _, err := transaction.NamedExec(`UPDATE ChannelMembers
SET Roles=:Roles,
LastViewedAt=:LastViewedAt,
MsgCount=:MsgCount,
MentionCount=:MentionCount,
UrgentMentionCount=:UrgentMentionCount,
NotifyProps=:NotifyProps,
LastUpdateAt=:LastUpdateAt,
SchemeUser=:SchemeUser,
SchemeAdmin=:SchemeAdmin,
SchemeGuest=:SchemeGuest,
MentionCountRoot=:MentionCountRoot,
MsgCountRoot=:MsgCountRoot
WHERE ChannelId=:ChannelId AND UserId=:UserId`, &member); err != nil {
return nil, errors.Wrap(err, "failed to update ChannelMember")
}
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
data := make(map[string]string)
data["ChannelId"] = channelMembers[len(channelMembers)-1].ChannelId
data["UserId"] = channelMembers[len(channelMembers)-1].UserId
return data, nil
}
func (s SqlChannelStore) ResetAllChannelSchemes() (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
err = s.resetAllChannelSchemesT(transaction)
if err != nil {
return err
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (s SqlChannelStore) resetAllChannelSchemesT(transaction *sqlxTxWrapper) error {
if _, err := transaction.Exec("UPDATE Channels SET SchemeId=''"); err != nil {
return errors.Wrap(err, "failed to update Channels")
}
return nil
}
func (s SqlChannelStore) ClearAllCustomRoleAssignments() (err error) {
builtInRoles := model.MakeDefaultRoles()
lastUserId := strings.Repeat("0", 26)
lastChannelId := strings.Repeat("0", 26)
for {
var transaction *sqlxTxWrapper
if transaction, err = s.GetMasterX().Beginx(); err != nil {
return errors.Wrap(err, "begin_transaction")
}
channelMembers := []*channelMember{}
query := `
SELECT
ChannelId,
UserId,
Roles,
LastViewedAt,
MsgCount,
MentionCount,
MentionCountRoot,
COALESCE(UrgentMentionCount, 0) AS UrgentMentionCount,
MsgCountRoot,
NotifyProps,
LastUpdateAt,
SchemeUser,
SchemeAdmin,
SchemeGuest
FROM
ChannelMembers
WHERE
(ChannelId, UserId) > (?, ?)
ORDER BY ChannelId, UserId
LIMIT 1000
`
if err = transaction.Select(&channelMembers, query, lastChannelId, lastUserId); err != nil {
finalizeTransactionX(transaction, &err)
return errors.Wrap(err, "failed to find ChannelMembers")
}
if len(channelMembers) == 0 {
finalizeTransactionX(transaction, &err)
break
}
for _, member := range channelMembers {
lastUserId = member.UserId
lastChannelId = member.ChannelId
var newRoles []string
for _, role := range strings.Fields(member.Roles) {
for name := range builtInRoles {
if name == role {
newRoles = append(newRoles, role)
break
}
}
}
newRolesString := strings.Join(newRoles, " ")
if newRolesString != member.Roles {
if _, err = transaction.Exec("UPDATE ChannelMembers SET Roles = ? WHERE UserId = ? AND ChannelId = ?", newRolesString, member.UserId, member.ChannelId); err != nil {
finalizeTransactionX(transaction, &err)
return errors.Wrap(err, "failed to update ChannelMembers")
}
}
}
if err = transaction.Commit(); err != nil {
finalizeTransactionX(transaction, &err)
return errors.Wrap(err, "commit_transaction")
}
}
return nil
}
func (s SqlChannelStore) GetAllChannelsForExportAfter(limit int, afterId string) ([]*model.ChannelForExport, error) {
channels := []*model.ChannelForExport{}
if err := s.GetReplicaX().Select(&channels, `
SELECT
Channels.*,
Teams.Name as TeamName,
Schemes.Name as SchemeName
FROM Channels
INNER JOIN
Teams ON Channels.TeamId = Teams.Id
LEFT JOIN
Schemes ON Channels.SchemeId = Schemes.Id
WHERE
Channels.Id > ?
AND Channels.Type IN (?, ?)
ORDER BY
Id
LIMIT ?`,
afterId, model.ChannelTypeOpen, model.ChannelTypePrivate, limit); err != nil {
return nil, errors.Wrap(err, "failed to find Channels for export")
}
return channels, nil
}
func (s SqlChannelStore) GetChannelMembersForExport(userId string, teamId string) ([]*model.ChannelMemberForExport, error) {
members := []*model.ChannelMemberForExport{}
err := s.GetReplicaX().Select(&members, `
SELECT
ChannelMembers.ChannelId,
ChannelMembers.UserId,
ChannelMembers.Roles,
ChannelMembers.LastViewedAt,
ChannelMembers.MsgCount,
ChannelMembers.MentionCount,
ChannelMembers.MentionCountRoot,
COALESCE(ChannelMembers.UrgentMentionCount, 0) AS UrgentMentionCount,
ChannelMembers.MsgCountRoot,
ChannelMembers.NotifyProps,
ChannelMembers.LastUpdateAt,
ChannelMembers.SchemeUser,
ChannelMembers.SchemeAdmin,
(ChannelMembers.SchemeGuest IS NOT NULL AND ChannelMembers.SchemeGuest) as SchemeGuest,
Channels.Name as ChannelName
FROM
ChannelMembers
INNER JOIN
Channels ON ChannelMembers.ChannelId = Channels.Id
WHERE
ChannelMembers.UserId = ?
AND Channels.TeamId = ?
AND Channels.DeleteAt = 0`,
userId, teamId)
if err != nil {
return nil, errors.Wrap(err, "failed to find Channels for export")
}
return members, nil
}
func (s SqlChannelStore) GetAllDirectChannelsForExportAfter(limit int, afterId string) ([]*model.DirectChannelForExport, error) {
directChannelsForExport := []*model.DirectChannelForExport{}
query := s.getQueryBuilder().
Select("Channels.*").
From("Channels").
Where(sq.And{
sq.Gt{"Channels.Id": afterId},
sq.Eq{"Channels.DeleteAt": int(0)},
sq.Eq{"Channels.Type": []model.ChannelType{model.ChannelTypeDirect, model.ChannelTypeGroup}},
}).
OrderBy("Channels.Id").
Limit(uint64(limit))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_tosql")
}
if err2 := s.GetReplicaX().Select(&directChannelsForExport, queryString, args...); err2 != nil {
return nil, errors.Wrap(err2, "failed to find direct Channels for export")
}
var channelIds []string
for _, channel := range directChannelsForExport {
channelIds = append(channelIds, channel.Id)
}
query = s.getQueryBuilder().
Select("u.Username as Username, ChannelId, UserId, cm.Roles as Roles, LastViewedAt, MsgCount, MentionCount, MentionCountRoot, COALESCE(UrgentMentionCount, 0) UrgentMentionCount, cm.NotifyProps as NotifyProps, LastUpdateAt, SchemeUser, SchemeAdmin, (SchemeGuest IS NOT NULL AND SchemeGuest) as SchemeGuest").
From("ChannelMembers cm").
Join("Users u ON ( u.Id = cm.UserId )").
Where(sq.And{
sq.Eq{"cm.ChannelId": channelIds},
sq.Eq{"u.DeleteAt": int(0)},
})
queryString, args, err = query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_tosql")
}
channelMembers := []*model.ChannelMemberForExport{}
if err2 := s.GetReplicaX().Select(&channelMembers, queryString, args...); err2 != nil {
return nil, errors.Wrap(err2, "failed to find ChannelMembers")
}
// Populate each channel with its members
dmChannelsMap := make(map[string]*model.DirectChannelForExport)
for _, channel := range directChannelsForExport {
channel.Members = &[]string{}
dmChannelsMap[channel.Id] = channel
}
for _, member := range channelMembers {
members := dmChannelsMap[member.ChannelId].Members
*members = append(*members, member.Username)
}
return directChannelsForExport, nil
}
func (s SqlChannelStore) GetChannelsBatchForIndexing(startTime int64, startChannelID string, limit int) ([]*model.Channel, error) {
query :=
`SELECT
*
FROM
Channels
WHERE
CreateAt > ?
OR
(CreateAt = ? AND Id > ?)
ORDER BY
CreateAt ASC, Id ASC
LIMIT
?`
channels := []*model.Channel{}
err := s.GetSearchReplicaX().Select(&channels, query, startTime, startTime, startChannelID, limit)
if err != nil {
return nil, errors.Wrap(err, "failed to find Channels")
}
return channels, nil
}
func (s SqlChannelStore) UserBelongsToChannels(userId string, channelIds []string) (bool, error) {
query := s.getQueryBuilder().
Select("Count(*)").
From("ChannelMembers").
Where(sq.And{
sq.Eq{"UserId": userId},
sq.Eq{"ChannelId": channelIds},
})
queryString, args, err := query.ToSql()
if err != nil {
return false, errors.Wrap(err, "channel_tosql")
}
var c int64
err = s.GetReplicaX().Get(&c, queryString, args...)
if err != nil {
return false, errors.Wrap(err, "failed to count ChannelMembers")
}
return c > 0, nil
}
// TODO: parameterize userIDs
func (s SqlChannelStore) UpdateMembersRole(channelID string, userIDs []string) error {
sql := fmt.Sprintf(`
UPDATE
ChannelMembers
SET
SchemeAdmin = CASE WHEN UserId IN ('%s') THEN
TRUE
ELSE
FALSE
END
WHERE
ChannelId = ?
AND (SchemeGuest = false OR SchemeGuest IS NULL)
`, strings.Join(userIDs, "', '"))
if _, err := s.GetMasterX().Exec(sql, channelID); err != nil {
return errors.Wrap(err, "failed to update ChannelMembers")
}
return nil
}
func (s SqlChannelStore) GroupSyncedChannelCount() (int64, error) {
query := s.getQueryBuilder().
Select("COUNT(*)").
From("Channels").
Where(sq.Eq{"GroupConstrained": true, "DeleteAt": 0})
sql, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "channel_tosql")
}
var count int64
err = s.GetReplicaX().Get(&count, sql, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to count Channels")
}
return count, nil
}
// SetShared sets the Shared flag true/false
func (s SqlChannelStore) SetShared(channelId string, shared bool) error {
squery, args, err := s.getQueryBuilder().
Update("Channels").
Set("Shared", shared).
Where(sq.Eq{"Id": channelId}).
ToSql()
if err != nil {
return errors.Wrap(err, "channel_set_shared_tosql")
}
result, err := s.GetMasterX().Exec(squery, args...)
if err != nil {
return errors.Wrap(err, "failed to update `Shared` for Channels")
}
count, err := result.RowsAffected()
if err != nil {
return errors.Wrap(err, "failed to determine rows affected")
}
if count == 0 {
return fmt.Errorf("id not found: %s", channelId)
}
return nil
}
// GetTeamForChannel returns the team for a given channelID.
func (s SqlChannelStore) GetTeamForChannel(channelID string) (*model.Team, error) {
nestedQ, nestedArgs, err := s.getQueryBuilder().Select("TeamId").From("Channels").Where(sq.Eq{"Id": channelID}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_team_for_channel_nested_tosql")
}
query, args, err := s.getQueryBuilder().
Select("*").
From("Teams").Where(sq.Expr("Id = ("+nestedQ+")", nestedArgs...)).ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_team_for_channel_tosql")
}
team := model.Team{}
err = s.GetReplicaX().Get(&team, query, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Team", fmt.Sprintf("channel_id=%s", channelID))
}
return nil, errors.Wrapf(err, "failed to find team with channel_id=%s", channelID)
}
return &team, nil
}
// GetTopChannelsForTeamSince returns the filtered post counts of the following Channels sets:
// a) those that are private channels in the given user's membership graph on the given team, and
// b) those that are public channels in the given team.
func (s SqlChannelStore) GetTopChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
channels := make([]*model.TopChannel, 0)
var args []any
postgresPropQuery := `AND (Posts.Props ->> 'from_bot' IS NULL OR Posts.Props ->> 'from_bot' = 'false') AND (Posts.Props ->> 'from_webhook' IS NULL OR Posts.Props ->> 'from_webhook' = 'false') AND (Posts.Props ->> 'from_oauth_app' IS NULL OR Posts.Props ->> 'from_oauth_app' = 'false') AND (Posts.Props ->> 'from_plugin' IS NULL OR Posts.Props ->> 'from_plugin' = 'false')`
mySqlPropsQuery := `AND (JSON_EXTRACT(Posts.Props, '$.from_bot') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_bot') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_webhook') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_webhook') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_plugin') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_plugin') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_oauth_app') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_oauth_app') = 'false')`
query := `
SELECT
ID,
Type,
DisplayName,
Name,
TeamID,
MessageCount
FROM
((SELECT
Posts.ChannelId AS ID,
'O' AS Type,
PublicChannels.DisplayName AS DisplayName,
PublicChannels.Name AS Name,
PublicChannels.TeamId AS TeamID,
count(Posts.Id) AS MessageCount,
PublicChannels.DeleteAt AS DeleteAt
FROM
Posts
LEFT JOIN PublicChannels on Posts.ChannelId = PublicChannels.Id
WHERE
Posts.DeleteAt = 0
AND Posts.CreateAt > ?
AND Posts.Type = ''`
args = []any{since}
if s.DriverName() == model.DatabaseDriverMysql {
query += mySqlPropsQuery
} else if s.DriverName() == model.DatabaseDriverPostgres {
query += postgresPropQuery
}
query += `
AND PublicChannels.TeamId = ?
GROUP BY
Posts.ChannelId,
PublicChannels.DisplayName,
PublicChannels.Name,
PublicChannels.TeamId,
PublicChannels.DeleteAt)
UNION ALL
(SELECT
Posts.ChannelId AS ID,
Channels.Type AS Type,
Channels.DisplayName AS DisplayName,
Channels.Name AS Name,
Channels.TeamId AS TeamID,
count(Posts.Id) AS MessageCount,
Channels.DeleteAt AS DeleteAt
FROM
Posts
LEFT JOIN Channels on Posts.ChannelId = Channels.Id
LEFT JOIN ChannelMembers on Posts.ChannelId = ChannelMembers.ChannelId
WHERE
Posts.DeleteAt = 0
AND Posts.CreateAt > ?
AND Posts.Type = ''`
args = append(args, teamID, since)
if s.DriverName() == model.DatabaseDriverMysql {
query += mySqlPropsQuery
} else if s.DriverName() == model.DatabaseDriverPostgres {
query += postgresPropQuery
}
query += `
AND Channels.TeamId = ?
AND Channels.Type = 'P'
AND ChannelMembers.UserId = ?
GROUP BY
Posts.ChannelId,
Channels.Type,
Channels.DisplayName,
Channels.Name,
Channels.TeamId,
Channels.DeleteAt)) AS A
WHERE
DeleteAt = 0
ORDER BY
MessageCount DESC,
Name ASC
LIMIT ?
OFFSET ?`
args = append(args, teamID, userID, limit+1, offset)
if err := s.GetReplicaX().Select(&channels, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to get top Channels")
}
return model.GetTopChannelListWithPagination(channels, limit), nil
}
// GetTopChannelsForUserSince returns the filtered post counts of channels with with posts created by the user
// after the given timestamp within the given team (or across the workspace if no team is given). Excludes DM and GM channels.
func (s SqlChannelStore) GetTopChannelsForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
channels := make([]*model.TopChannel, 0)
var args []any
var query string
var propsQuery string
if s.DriverName() == model.DatabaseDriverMysql {
propsQuery = `AND (JSON_EXTRACT(Posts.Props, '$.from_bot') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_bot') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_webhook') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_webhook') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_plugin') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_plugin') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_oauth_app') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_oauth_app') = 'false')`
} else if s.DriverName() == model.DatabaseDriverPostgres {
propsQuery = `AND (Posts.Props ->> 'from_bot' IS NULL OR Posts.Props ->> 'from_bot' = 'false') AND (Posts.Props ->> 'from_webhook' IS NULL OR Posts.Props ->> 'from_webhook' = 'false') AND (Posts.Props ->> 'from_oauth_app' IS NULL OR Posts.Props ->> 'from_oauth_app' = 'false') AND (Posts.Props ->> 'from_plugin' IS NULL OR Posts.Props ->> 'from_plugin' = 'false')`
}
query = `
SELECT
Posts.ChannelId AS ID,
Channels.Type AS Type,
Channels.DisplayName AS DisplayName,
Channels.Name AS Name,
Channels.TeamId AS TeamID,
count(Posts.Id) AS MessageCount
FROM
Posts
LEFT JOIN Channels on Posts.ChannelId = Channels.Id
LEFT JOIN ChannelMembers on Posts.ChannelId = ChannelMembers.ChannelId
WHERE
Posts.DeleteAt = 0
AND Posts.CreateAt > ?
AND Posts.Type = ''
AND Posts.UserID = ?
AND Channels.DeleteAt = 0
AND (Channels.Type = 'O' OR Channels.Type = 'P')
AND ChannelMembers.UserId = ? `
query += propsQuery
args = []any{since, userID, userID}
if teamID != "" {
query += `
AND Channels.TeamID = ?`
args = append(args, teamID)
}
query += `
Group By
Posts.ChannelId,
Channels.Type,
Channels.DisplayName,
Channels.Name,
Channels.TeamId
ORDER BY
MessageCount DESC,
Name ASC
LIMIT ?
OFFSET ?`
args = append(args, limit+1, offset)
if err := s.GetReplicaX().Select(&channels, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to get top Channels")
}
return model.GetTopChannelListWithPagination(channels, limit), nil
}
// GetTopInactiveChannelsForTeamSince returns the filtered post counts of the following Channels sets:
// a) those that are private channels in the given user's membership graph on the given team, and
// b) those that are public channels in the given team.
func (s SqlChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
channels := make([]*model.TopInactiveChannel, 0)
var args []any
query := `
SELECT
ID,
Type,
DisplayName,
Name,
MessageCount,
LastActivityAt
FROM
((SELECT
PublicChannels.Id AS ID,
'O' AS Type,
PublicChannels.DisplayName AS DisplayName,
PublicChannels.Name AS Name,
COALESCE(count(Posts.Id), 0) AS MessageCount,
COALESCE(max(Posts.CreateAt), 0) AS LastActivityAt
FROM
PublicChannels
LEFT JOIN Posts on Posts.ChannelId = PublicChannels.Id AND Posts.Type = '' AND Posts.CreateAt > ? AND Posts.DeleteAt = 0
LEFT JOIN Channels on Channels.Id = PublicChannels.Id
WHERE
PublicChannels.TeamId = ?
AND PublicChannels.DeleteAt = 0
AND Channels.CreateAt < ?
GROUP BY
PublicChannels.Id,
PublicChannels.DisplayName,
PublicChannels.Name,
PublicChannels.TeamId)
UNION ALL
(SELECT
Channels.Id AS ID,
Channels.Type AS Type,
Channels.DisplayName AS DisplayName,
Channels.Name AS Name,
COALESCE(count(Posts.Id), 0) AS MessageCount,
COALESCE(max(Posts.CreateAt), 0) AS LastActivityAt
FROM
Channels
LEFT JOIN Posts on Posts.ChannelId = Channels.Id AND Posts.Type = '' AND Posts.CreateAt > ? AND Posts.DeleteAt = 0
LEFT JOIN ChannelMembers on Channels.Id = ChannelMembers.ChannelId
WHERE
Channels.TeamId = ?
AND Channels.CreateAt < ?
AND Channels.Type = 'P'
AND Channels.DeleteAt = 0
AND ChannelMembers.UserId = ?
GROUP BY
Channels.Id,
Channels.Type,
Channels.DisplayName,
Channels.Name)) AS A
ORDER BY
MessageCount ASC,
Name ASC
LIMIT ?
OFFSET ?`
args = append(args, since, teamID, since, since, teamID, since, userID, limit+1, offset)
if err := s.GetReplicaX().Select(&channels, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to get top Channels")
}
channels, err := postProcessTopInactiveChannels(s, channels)
if err != nil {
return nil, err
}
return model.GetTopInactiveChannelListWithPagination(channels, limit), nil
}
// GetTopInactiveChannelsForUserSince returns the filtered post counts of channels with with posts created by the user
// after the given timestamp within the given team (or across the workspace if no team is given). Excludes DM and GM channels.
func (s SqlChannelStore) GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
channels := make([]*model.TopInactiveChannel, 0)
var args []any
var query string
query = `
SELECT
Channels.Id AS ID,
Channels.Type AS Type,
Channels.DisplayName AS DisplayName,
Channels.Name AS Name,
COALESCE(count(Posts.Id), 0) AS MessageCount,
COALESCE(max(Posts.CreateAt), 0) AS LastActivityAt
FROM
Channels
LEFT JOIN Posts on Posts.ChannelId = Channels.Id AND Posts.Type = '' AND Posts.CreateAt > ? AND Posts.DeleteAt = 0
LEFT JOIN ChannelMembers on Channels.Id = ChannelMembers.ChannelId
WHERE
Channels.DeleteAt = 0
AND Channels.CreateAt < ?
AND (Channels.Type = 'O' OR Channels.Type = 'P')
AND ChannelMembers.UserId = ? `
args = []any{since, since, userID}
if teamID != "" {
query += `
AND Channels.TeamID = ?`
args = append(args, teamID)
}
query += `
Group By
Channels.Id,
Channels.Type,
Channels.DisplayName,
Channels.Name
ORDER BY
MessageCount ASC,
Name ASC
LIMIT ?
OFFSET ?`
args = append(args, limit+1, offset)
if err := s.GetReplicaX().Select(&channels, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to get top Inactive Channels")
}
channels, err := postProcessTopInactiveChannels(s, channels)
if err != nil {
return nil, err
}
return model.GetTopInactiveChannelListWithPagination(channels, limit), nil
}
func postProcessTopInactiveChannels(s SqlChannelStore, channels []*model.TopInactiveChannel) ([]*model.TopInactiveChannel, error) {
// query channel members for Ids
var conditionalAggrSelector string
if s.DriverName() == model.DatabaseDriverMysql {
conditionalAggrSelector = "GROUP_CONCAT(UserId SEPARATOR ',') as UserIds"
} else if s.DriverName() == model.DatabaseDriverPostgres {
conditionalAggrSelector = "string_agg(UserId, ',') as UserIds"
}
var channelIds []string
for _, channel := range channels {
channelIds = append(channelIds, channel.ID)
}
q := s.getQueryBuilder().Select("ChannelId", conditionalAggrSelector).From("ChannelMembers").
Where(sq.Eq{
"ChannelId": channelIds,
}).GroupBy("ChannelId")
channelsUserIdsMap := make(map[string]string, len(channels))
type ChannelUserIdsResult struct {
ChannelId string
UserIds string
}
channelsUserIdsResultList := make([]ChannelUserIdsResult, len(channels))
sql, args, err := q.ToSql()
if err != nil {
return nil, errors.Wrap(err, "failed to stringify squirrel query")
}
if err := s.GetReplicaX().Select(&channelsUserIdsResultList, sql, args...); err != nil {
return nil, errors.Wrap(err, "failed to get top Inactive Channels users")
}
for _, channelUserIds := range channelsUserIdsResultList {
channelsUserIdsMap[channelUserIds.ChannelId] = channelUserIds.UserIds
}
for index, channel := range channels {
userIds := channelsUserIdsMap[channel.ID]
userIdsSlice := strings.Split(userIds, ",")
channels[index].Participants = userIdsSlice
// handle channels with 0 participants
if len(userIdsSlice) == 1 && userIdsSlice[0] == "" {
channels[index].Participants = make([]string, 0)
}
}
return channels, nil
}
func (s SqlChannelStore) PostCountsByDuration(channelIDs []string, sinceUnixMillis int64, userID *string, duration model.PostCountGrouping, atLocation *time.Location) ([]*model.DurationPostCount, error) {
var unixSelect string
var propsQuery string
loc := atLocation.String()
if loc == "Local" {
loc = "UTC"
}
var format string
if s.DriverName() == model.DatabaseDriverMysql {
if duration == model.PostsByDay {
format = `%Y-%m-%d`
} else {
format = `%Y-%m-%dT%H`
}
unixSelect = fmt.Sprintf(`DATE_FORMAT(
COALESCE(
CONVERT_TZ(FROM_UNIXTIME(Posts.CreateAt / 1000), 'GMT', '%s'),
FROM_UNIXTIME(Posts.CreateAt / 1000)
),
'%s') AS duration`, loc, format)
propsQuery = `(JSON_EXTRACT(Posts.Props, '$.from_bot') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_bot') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_webhook') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_webhook') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_plugin') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_plugin') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_oauth_app') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_oauth_app') = 'false')`
} else if s.DriverName() == model.DatabaseDriverPostgres {
if duration == model.PostsByDay {
format = "YYYY-MM-DD"
} else {
format = `YYYY-MM-DD"T"HH24`
}
unixSelect = fmt.Sprintf(`TO_CHAR(TO_TIMESTAMP(Posts.CreateAt / 1000) AT TIME ZONE '%s', '%s') AS duration`, loc, format)
propsQuery = `(Posts.Props ->> 'from_bot' IS NULL OR Posts.Props ->> 'from_bot' = 'false') AND (Posts.Props ->> 'from_webhook' IS NULL OR Posts.Props ->> 'from_webhook' = 'false') AND (Posts.Props ->> 'from_oauth_app' IS NULL OR Posts.Props ->> 'from_oauth_app' = 'false') AND (Posts.Props ->> 'from_plugin' IS NULL OR Posts.Props ->> 'from_plugin' = 'false')`
}
query := sq.
Select("Posts.ChannelId AS channelid", unixSelect, "count(Posts.Id) AS postcount").
From("Posts").
LeftJoin("Channels ON Posts.ChannelId = Channels.Id").
Where(sq.And{
sq.Eq{"Posts.DeleteAt": 0},
sq.Gt{"Posts.CreateAt": sinceUnixMillis},
sq.Eq{"Posts.Type": ""},
sq.Eq{"Channels.Id": channelIDs},
}).
Where(propsQuery).
GroupBy("channelid", "duration").
OrderBy("channelid", "duration")
if userID != nil && model.IsValidId(*userID) {
query = query.Where(sq.And{sq.Eq{"Posts.UserId": *userID}})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "failed to parse query")
}
dailyPostCounts := make([]*model.DurationPostCount, 0)
if err := s.GetReplicaX().Select(&dailyPostCounts, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to get post counts by duration")
}
return dailyPostCounts, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"fmt"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
// dbSelecter is an interface used to enable some internal store methods
// using both transaction and normal queries.
type dbSelecter interface {
Select(i any, query string, args ...any) error
}
func (s SqlChannelStore) CreateInitialSidebarCategories(userId string, opts *store.SidebarCategorySearchOpts) (_ *model.OrderedSidebarCategories, err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "CreateInitialSidebarCategories: begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
teamsWithExclude, err := s.SqlStore.stores.team.GetTeamsForUser(context.Background(), userId, opts.TeamID, false)
if err != nil {
return nil, errors.Wrap(err, "CreateInitialSidebarCategories: GetTeamsForUser")
}
excludedTeamIDs := make([]string, 0, len(teamsWithExclude))
for _, tm := range teamsWithExclude {
excludedTeamIDs = append(excludedTeamIDs, tm.TeamId)
}
if err = s.createInitialSidebarCategoriesT(transaction, userId, excludedTeamIDs, opts); err != nil {
return nil, errors.Wrap(err, "CreateInitialSidebarCategories: createInitialSidebarCategoriesT")
}
oc, err := s.getSidebarCategoriesT(transaction, userId, opts)
if err != nil {
return nil, errors.Wrap(err, "CreateInitialSidebarCategories: getSidebarCategoriesT")
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "CreateInitialSidebarCategories: commit_transaction")
}
return oc, nil
}
func (s SqlChannelStore) createInitialSidebarCategoriesT(transaction *sqlxTxWrapper, userId string, excludedTeamIDs []string, opts *store.SidebarCategorySearchOpts) error {
query := s.getQueryBuilder().
Select("Type, TeamId").
From("SidebarCategories").
Where(sq.Eq{
"UserId": userId,
"Type": []model.SidebarCategoryType{
model.SidebarCategoryFavorites,
model.SidebarCategoryChannels,
model.SidebarCategoryDirectMessages,
},
})
if !opts.ExcludeTeam {
query = query.Where(sq.Eq{"TeamId": opts.TeamID})
} else {
query = query.Where(sq.NotEq{"TeamId": opts.TeamID})
}
selectQuery, selectParams, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "createInitialSidebarCategoriesT_Tosql")
}
existingTypes := []struct {
Type model.SidebarCategoryType
TeamId string
}{}
err = transaction.Select(&existingTypes, selectQuery, selectParams...)
if err != nil {
return errors.Wrap(err, "createInitialSidebarCategoriesT: failed to select existing categories")
}
hasCategoryOfType := make(map[model.SidebarCategoryType]map[string]bool, len(existingTypes))
for _, existingType := range existingTypes {
if hasCategoryOfType[existingType.Type] == nil {
hasCategoryOfType[existingType.Type] = make(map[string]bool)
hasCategoryOfType[existingType.Type][existingType.TeamId] = true
}
}
insertBuilder := s.getQueryBuilder().Insert("SidebarCategories").
Columns("Id, UserId, TeamId, SortOrder, Sorting, Type, DisplayName, Muted, Collapsed")
hasInsert := false
getRequiredTeamIDs := func(category model.SidebarCategoryType, opts *store.SidebarCategorySearchOpts) []string {
// if category == nil - nothing
// if not exclude - just that team
// otherwise get all teams excluding that team
// if != nil - then partial
// if not exclude, and team exists in map then skip.
// otherwise, get all teams excluding that team, subtract all items from map.
if hasCategoryOfType[category] == nil {
// If not exclude, do for only single team
// if exclude, get all teams, excluding that team
if !opts.ExcludeTeam {
return []string{opts.TeamID}
}
return excludedTeamIDs
}
mapEntry := hasCategoryOfType[category]
if !opts.ExcludeTeam && mapEntry[opts.TeamID] {
// continue, nothing to do since entry already exists.
} else {
for i, tID := range excludedTeamIDs {
if mapEntry[tID] {
// remove from slice
copy(excludedTeamIDs[i:], excludedTeamIDs[i+1:])
excludedTeamIDs[len(excludedTeamIDs)-1] = ""
excludedTeamIDs = excludedTeamIDs[:len(excludedTeamIDs)-1]
}
}
return excludedTeamIDs
}
return []string{}
}
teamIDs := getRequiredTeamIDs(model.SidebarCategoryFavorites, opts)
for _, teamID := range teamIDs {
// Use deterministic IDs for default categories to prevent potentially creating multiple copies of a default category
favoritesCategoryId := fmt.Sprintf("%s_%s_%s", model.SidebarCategoryFavorites, userId, teamID)
// Create the SidebarChannels first since there's more opportunity for something to fail here
if err := s.migrateFavoritesToSidebarT(transaction, userId, teamID, favoritesCategoryId); err != nil {
return errors.Wrap(err, "createInitialSidebarCategoriesT: failed to migrate favorites to sidebar")
}
insertBuilder = insertBuilder.Values(favoritesCategoryId, userId, teamID, model.DefaultSidebarSortOrderFavorites, model.SidebarCategorySortDefault, model.SidebarCategoryFavorites, "Favorites" /* This will be retranslated by the client into the user's locale */, false, false)
hasInsert = true
}
teamIDs = getRequiredTeamIDs(model.SidebarCategoryChannels, opts)
for _, teamID := range teamIDs {
channelsCategoryId := fmt.Sprintf("%s_%s_%s", model.SidebarCategoryChannels, userId, teamID)
insertBuilder = insertBuilder.Values(channelsCategoryId, userId, teamID, model.DefaultSidebarSortOrderChannels, model.SidebarCategorySortDefault, model.SidebarCategoryChannels, "Channels" /* This will be retranslated by the client into the user's locale */, false, false)
hasInsert = true
}
teamIDs = getRequiredTeamIDs(model.SidebarCategoryDirectMessages, opts)
for _, teamID := range teamIDs {
directMessagesCategoryId := fmt.Sprintf("%s_%s_%s", model.SidebarCategoryDirectMessages, userId, teamID)
insertBuilder = insertBuilder.Values(directMessagesCategoryId, userId, teamID, model.DefaultSidebarSortOrderDMs, model.SidebarCategorySortRecent, model.SidebarCategoryDirectMessages, "Direct Messages" /* This will be retranslated by the client into the user's locale */, false, false)
hasInsert = true
}
if hasInsert {
sql, args, err := insertBuilder.ToSql()
if err != nil {
return errors.Wrap(err, "insertSidebarCategories_Tosql")
}
_, err = transaction.Exec(sql, args...)
if err != nil {
return errors.Wrap(err, "createInitialSidebarCategoriesT: failed to insert categories")
}
}
return nil
}
type userMembership struct {
UserId string
ChannelId string
CategoryId string
}
func (s SqlChannelStore) migrateMembershipToSidebar(transaction *sqlxTxWrapper, runningOrder *int64, sql string, args ...any) ([]userMembership, error) {
memberships := []userMembership{}
if err := transaction.Select(&memberships, sql, args...); err != nil {
return nil, err
}
for _, favorite := range memberships {
sql, args, err := s.getQueryBuilder().
Insert("SidebarChannels").
Columns("ChannelId", "UserId", "CategoryId", "SortOrder").
Values(favorite.ChannelId, favorite.UserId, favorite.CategoryId, *runningOrder).ToSql()
if err != nil {
return nil, err
}
if _, err := transaction.Exec(sql, args...); err != nil && !IsUniqueConstraintError(err, []string{"UserId", "PRIMARY"}) {
return nil, err
}
*runningOrder = *runningOrder + model.MinimalSidebarSortDistance
}
if err := transaction.Commit(); err != nil {
return nil, err
}
return memberships, nil
}
func (s SqlChannelStore) migrateFavoritesToSidebarT(transaction *sqlxTxWrapper, userId, teamId, favoritesCategoryId string) error {
favoritesQuery, favoritesParams, err := s.getQueryBuilder().
Select("Preferences.Name").
From("Preferences").
Join("Channels on Preferences.Name = Channels.Id").
Join("ChannelMembers on Preferences.Name = ChannelMembers.ChannelId and Preferences.UserId = ChannelMembers.UserId").
Where(sq.Eq{
"Preferences.UserId": userId,
"Preferences.Category": model.PreferenceCategoryFavoriteChannel,
"Preferences.Value": "true",
}).
Where(sq.Or{
sq.Eq{"Channels.TeamId": teamId},
sq.Eq{"Channels.TeamId": ""},
}).
OrderBy(
"Channels.DisplayName",
"Channels.Name ASC",
).ToSql()
if err != nil {
return err
}
favoriteChannelIds := []string{}
if err := transaction.Select(&favoriteChannelIds, favoritesQuery, favoritesParams...); err != nil {
return errors.Wrap(err, "migrateFavoritesToSidebarT: unable to get favorite channel IDs")
}
for i, channelId := range favoriteChannelIds {
if _, err := transaction.NamedExec(`INSERT INTO
SidebarChannels(ChannelId, UserId, CategoryId, SortOrder)
VALUES(:ChannelId, :UserId, :CategoryId, :SortOrder)`, &model.SidebarChannel{
ChannelId: channelId,
CategoryId: favoritesCategoryId,
UserId: userId,
SortOrder: int64(i * model.MinimalSidebarSortDistance),
}); err != nil {
return errors.Wrap(err, "migrateFavoritesToSidebarT: unable to insert SidebarChannel")
}
}
return nil
}
// MigrateFavoritesToSidebarChannels populates the SidebarChannels table by analyzing existing user preferences for favorites
// **IMPORTANT** This function should only be called from the migration task and shouldn't be used by itself
func (s SqlChannelStore) MigrateFavoritesToSidebarChannels(lastUserId string, runningOrder int64) (_ map[string]any, err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, err
}
defer finalizeTransactionX(transaction, &err)
sb := s.
getQueryBuilder().
Select("Preferences.UserId", "Preferences.Name AS ChannelId", "SidebarCategories.Id AS CategoryId").
From("Preferences").
Where(sq.And{
sq.Eq{"Preferences.Category": model.PreferenceCategoryFavoriteChannel},
sq.NotEq{"Preferences.Value": "false"},
sq.NotEq{"SidebarCategories.Id": nil},
sq.Gt{"Preferences.UserId": lastUserId},
}).
LeftJoin("Channels ON (Channels.Id=Preferences.Name)").
LeftJoin("SidebarCategories ON (SidebarCategories.UserId=Preferences.UserId AND SidebarCategories.Type='"+string(model.SidebarCategoryFavorites)+"' AND (SidebarCategories.TeamId=Channels.TeamId OR Channels.TeamId=''))").
OrderBy("Preferences.UserId", "Channels.Name DESC").
Limit(100)
sql, args, err := sb.ToSql()
if err != nil {
return nil, err
}
userFavorites, err := s.migrateMembershipToSidebar(transaction, &runningOrder, sql, args...)
if err != nil {
return nil, err
}
if len(userFavorites) == 0 {
return nil, nil
}
data := make(map[string]any)
data["UserId"] = userFavorites[len(userFavorites)-1].UserId
data["SortOrder"] = runningOrder
return data, nil
}
type sidebarCategoryForJoin struct {
model.SidebarCategory
ChannelId *string
}
func (s SqlChannelStore) CreateSidebarCategory(userId, teamId string, newCategory *model.SidebarCategoryWithChannels) (_ *model.SidebarCategoryWithChannels, err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
opts := &store.SidebarCategorySearchOpts{
TeamID: teamId,
ExcludeTeam: false,
}
categoriesWithOrder, err := s.getSidebarCategoriesT(transaction, userId, opts)
if err != nil {
return nil, err
} else if len(categoriesWithOrder.Categories) == 0 {
return nil, store.NewErrNotFound("categories not found", fmt.Sprintf("userId=%s,teamId=%s", userId, teamId))
}
newOrder := categoriesWithOrder.Order
newCategoryId := model.NewId()
newCategorySortOrder := 0
/*
When a new category is created, it should be placed as follows:
1. If the Favorites category is first, the new category should be placed after it
2. Otherwise, the new category should be placed first.
*/
if categoriesWithOrder.Categories[0].Type == model.SidebarCategoryFavorites {
newOrder = append([]string{newOrder[0], newCategoryId}, newOrder[1:]...)
newCategorySortOrder = model.MinimalSidebarSortDistance
} else {
newOrder = append([]string{newCategoryId}, newOrder...)
}
category := &model.SidebarCategory{
DisplayName: newCategory.DisplayName,
Id: newCategoryId,
UserId: userId,
TeamId: teamId,
Sorting: model.SidebarCategorySortDefault,
SortOrder: int64(model.MinimalSidebarSortDistance * len(newOrder)), // first we place it at the end of the list
Type: model.SidebarCategoryCustom,
Muted: newCategory.Muted,
}
if _, err2 := transaction.NamedExec(`INSERT INTO
SidebarCategories(Id, UserId, TeamId, SortOrder, Sorting, Type, DisplayName, Muted, Collapsed)
VALUES(:Id, :UserId, :TeamId, :SortOrder, :Sorting, :Type, :DisplayName, :Muted, :Collapsed)`, category); err2 != nil {
return nil, errors.Wrap(err2, "failed to save SidebarCategory")
}
if len(newCategory.Channels) > 0 {
placeHolder, channelIdArgs := constructArrayArgs(newCategory.Channels)
// Remove any channels from their previous categories and add them to the new one
var deleteQuery string
if s.DriverName() == model.DatabaseDriverMysql {
deleteQuery = `
DELETE
SidebarChannels
FROM
SidebarChannels
JOIN
SidebarCategories ON SidebarChannels.CategoryId = SidebarCategories.Id
WHERE
SidebarChannels.UserId = ?
AND SidebarChannels.ChannelId IN ` + placeHolder + `
AND SidebarCategories.TeamId = ?`
} else {
deleteQuery = `
DELETE FROM
SidebarChannels
USING
SidebarCategories
WHERE
SidebarChannels.CategoryId = SidebarCategories.Id
AND SidebarChannels.UserId = ?
AND SidebarChannels.ChannelId IN ` + placeHolder + `
AND SidebarCategories.TeamId = ?`
}
args := []any{userId}
args = append(args, channelIdArgs...)
args = append(args, teamId)
_, err = transaction.Exec(deleteQuery, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to delete SidebarChannels")
}
insertQuery := s.getQueryBuilder().
Insert("SidebarChannels").
Columns("ChannelId", "UserId", "CategoryId", "SortOrder")
for i, channelID := range newCategory.Channels {
insertQuery = insertQuery.Values(channelID, userId, newCategoryId, int64(i*model.MinimalSidebarSortDistance))
}
sql, args, err := insertQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "InsertSidebarChannels_Tosql")
}
if _, err := transaction.Exec(sql, args...); err != nil {
return nil, errors.Wrap(err, "failed to save SidebarChannels")
}
}
// now we re-order the categories according to the new order
if err := s.updateSidebarCategoryOrderT(transaction, newOrder); err != nil {
return nil, err
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
// patch category to return proper sort order
category.SortOrder = int64(newCategorySortOrder)
result := &model.SidebarCategoryWithChannels{
SidebarCategory: *category,
Channels: newCategory.Channels,
}
return result, nil
}
func (s SqlChannelStore) completePopulatingCategoryChannels(category *model.SidebarCategoryWithChannels) (_ *model.SidebarCategoryWithChannels, err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
result, err := s.completePopulatingCategoryChannelsT(transaction, category)
if err != nil {
return nil, err
}
if err = transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return result, nil
}
func (s SqlChannelStore) completePopulatingCategoryChannelsT(db dbSelecter, category *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) {
if category.Type == model.SidebarCategoryCustom || category.Type == model.SidebarCategoryFavorites {
return category, nil
}
var channelTypeFilter sq.Sqlizer
if category.Type == model.SidebarCategoryDirectMessages {
// any DM/GM channels that aren't in any category should be returned as part of the Direct Messages category
channelTypeFilter = sq.Eq{"Channels.Type": []model.ChannelType{model.ChannelTypeDirect, model.ChannelTypeGroup}}
} else if category.Type == model.SidebarCategoryChannels {
// any public/private channels that are on the current team and aren't in any category should be returned as part of the Channels category
channelTypeFilter = sq.And{
sq.Eq{"Channels.Type": []model.ChannelType{model.ChannelTypeOpen, model.ChannelTypePrivate}},
sq.Eq{"Channels.TeamId": category.TeamId},
}
} else {
return nil, fmt.Errorf("invalid category type: %q", category.Type)
}
// A subquery that is true if the channel does not have a SidebarChannel entry for the current user on the current team
doesNotHaveSidebarChannel := sq.Select("1").
Prefix("NOT EXISTS (").
From("SidebarChannels").
Join("SidebarCategories on SidebarChannels.CategoryId=SidebarCategories.Id").
Where(sq.And{
sq.Expr("SidebarChannels.ChannelId = ChannelMembers.ChannelId"),
sq.Eq{"SidebarCategories.UserId": category.UserId},
sq.Eq{"SidebarCategories.TeamId": category.TeamId},
}).
Suffix(")")
channels := []string{}
sql, args, err := s.getQueryBuilder().
Select("Id").
From("ChannelMembers").
LeftJoin("Channels ON Channels.Id=ChannelMembers.ChannelId").
Where(sq.And{
sq.Eq{"ChannelMembers.UserId": category.UserId},
channelTypeFilter,
sq.Eq{"Channels.DeleteAt": 0},
doesNotHaveSidebarChannel,
}).
OrderBy("DisplayName ASC").ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_tosql")
}
if err := db.Select(&channels, sql, args...); err != nil {
return nil, store.NewErrNotFound("ChannelMembers", "<too many fields>").Wrap(err)
}
category.Channels = append(channels, category.Channels...)
return category, nil
}
func (s SqlChannelStore) GetSidebarCategory(categoryId string) (*model.SidebarCategoryWithChannels, error) {
sql, args, err := s.getQueryBuilder().
Select("SidebarCategories.*", "SidebarChannels.ChannelId").
From("SidebarCategories").
LeftJoin("SidebarChannels ON SidebarChannels.CategoryId=SidebarCategories.Id").
Where(sq.Eq{"SidebarCategories.Id": categoryId}).
OrderBy("SidebarChannels.SortOrder ASC").ToSql()
if err != nil {
return nil, errors.Wrap(err, "sidebar_category_tosql")
}
categories := []*sidebarCategoryForJoin{}
if err = s.GetReplicaX().Select(&categories, sql, args...); err != nil {
return nil, store.NewErrNotFound("SidebarCategories", categoryId).Wrap(err)
}
if len(categories) == 0 {
return nil, store.NewErrNotFound("SidebarCategories", categoryId)
}
result := &model.SidebarCategoryWithChannels{
SidebarCategory: categories[0].SidebarCategory,
Channels: make([]string, 0),
}
for _, category := range categories {
if category.ChannelId != nil {
result.Channels = append(result.Channels, *category.ChannelId)
}
}
return s.completePopulatingCategoryChannels(result)
}
func (s SqlChannelStore) getSidebarCategoriesT(db dbSelecter, userId string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
oc := model.OrderedSidebarCategories{
Categories: make(model.SidebarCategoriesWithChannels, 0),
Order: make([]string, 0),
}
categories := []*sidebarCategoryForJoin{}
query := s.getQueryBuilder().
Select("SidebarCategories.*", "SidebarChannels.ChannelId").
From("SidebarCategories").
LeftJoin("SidebarChannels ON SidebarChannels.CategoryId=Id").
InnerJoin("Teams ON Teams.Id=SidebarCategories.TeamId").
InnerJoin("TeamMembers ON TeamMembers.TeamId=SidebarCategories.TeamId").
Where(sq.And{
sq.Eq{"TeamMembers.UserId": userId},
sq.Eq{"TeamMembers.DeleteAt": 0},
sq.Eq{"Teams.DeleteAt": 0},
}).
Where(sq.And{
sq.Eq{"SidebarCategories.UserId": userId},
}).
OrderBy("SidebarCategories.SortOrder ASC, SidebarChannels.SortOrder ASC")
if opts.ExcludeTeam {
query = query.Where(sq.NotEq{"SidebarCategories.TeamId": opts.TeamID})
} else {
query = query.Where(sq.Eq{"SidebarCategories.TeamId": opts.TeamID})
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "sidebar_categories_tosql")
}
if err := db.Select(&categories, sql, args...); err != nil {
return nil, store.NewErrNotFound("SidebarCategories", fmt.Sprintf("userId=%s,teamId=%s", userId, opts.TeamID)).Wrap(err)
}
for _, category := range categories {
var prevCategory *model.SidebarCategoryWithChannels
for _, existing := range oc.Categories {
if existing.Id == category.Id {
prevCategory = existing
break
}
}
if prevCategory == nil {
prevCategory = &model.SidebarCategoryWithChannels{
SidebarCategory: category.SidebarCategory,
Channels: make([]string, 0),
}
oc.Categories = append(oc.Categories, prevCategory)
oc.Order = append(oc.Order, category.Id)
}
if category.ChannelId != nil {
prevCategory.Channels = append(prevCategory.Channels, *category.ChannelId)
}
}
for _, category := range oc.Categories {
if _, err := s.completePopulatingCategoryChannelsT(db, category); err != nil {
return nil, err
}
}
return &oc, nil
}
func (s SqlChannelStore) GetSidebarCategoriesForTeamForUser(userId, teamId string) (*model.OrderedSidebarCategories, error) {
opts := &store.SidebarCategorySearchOpts{
TeamID: teamId,
ExcludeTeam: false,
}
return s.getSidebarCategoriesT(s.GetReplicaX(), userId, opts)
}
func (s SqlChannelStore) GetSidebarCategories(userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
return s.getSidebarCategoriesT(s.GetReplicaX(), userID, opts)
}
func (s SqlChannelStore) GetSidebarCategoryOrder(userId, teamId string) ([]string, error) {
ids := []string{}
sql, args, err := s.getQueryBuilder().
Select("Id").
From("SidebarCategories").
Where(sq.And{
sq.Eq{"UserId": userId},
sq.Eq{"TeamId": teamId},
}).
OrderBy("SidebarCategories.SortOrder ASC").ToSql()
if err != nil {
return nil, errors.Wrap(err, "sidebar_category_tosql")
}
if err := s.GetReplicaX().Select(&ids, sql, args...); err != nil {
return nil, store.NewErrNotFound("SidebarCategories", fmt.Sprintf("userId=%s,teamId=%s", userId, teamId)).Wrap(err)
}
return ids, nil
}
func (s SqlChannelStore) updateSidebarCategoryOrderT(transaction *sqlxTxWrapper, categoryOrder []string) error {
runningOrder := 0
for _, categoryId := range categoryOrder {
sql, args, err := s.getQueryBuilder().
Update("SidebarCategories").
Set("SortOrder", runningOrder).
Where(sq.Eq{"Id": categoryId}).ToSql()
if err != nil {
return errors.Wrap(err, "updateSidebarCategoryOrderT_Tosql")
}
if _, err := transaction.Exec(sql, args...); err != nil {
return errors.Wrap(err, "Error updating sidebar category order")
}
runningOrder += model.MinimalSidebarSortDistance
}
return nil
}
func (s SqlChannelStore) UpdateSidebarCategoryOrder(userId, teamId string, categoryOrder []string) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
// Ensure no invalid categories are included and that no categories are left out
existingOrder, err := s.GetSidebarCategoryOrder(userId, teamId)
if err != nil {
return err
}
if len(existingOrder) != len(categoryOrder) {
return errors.New("cannot update category order, passed list of categories different size than in DB")
}
for _, originalCategoryId := range existingOrder {
found := false
for _, newCategoryId := range categoryOrder {
if newCategoryId == originalCategoryId {
found = true
break
}
}
if !found {
return store.NewErrInvalidInput("SidebarCategories", "id", fmt.Sprintf("%v", categoryOrder))
}
}
if err := s.updateSidebarCategoryOrderT(transaction, categoryOrder); err != nil {
return err
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
//nolint:unparam
func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categories []*model.SidebarCategoryWithChannels) (updated []*model.SidebarCategoryWithChannels, original []*model.SidebarCategoryWithChannels, err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
updatedCategories := []*model.SidebarCategoryWithChannels{}
originalCategories := []*model.SidebarCategoryWithChannels{}
for _, category := range categories {
srcCategory, err2 := s.GetSidebarCategory(category.Id)
if err2 != nil {
return nil, nil, errors.Wrap(err2, "failed to find SidebarCategories")
}
// Copy category to avoid modifying an argument
destCategory := &model.SidebarCategoryWithChannels{
SidebarCategory: category.SidebarCategory,
}
// Prevent any changes to read-only fields of SidebarCategories
destCategory.UserId = srcCategory.UserId
destCategory.TeamId = srcCategory.TeamId
destCategory.SortOrder = srcCategory.SortOrder
destCategory.Type = srcCategory.Type
destCategory.Muted = srcCategory.Muted
if destCategory.Type != model.SidebarCategoryCustom {
destCategory.DisplayName = srcCategory.DisplayName
}
if destCategory.Type != model.SidebarCategoryDirectMessages {
destCategory.Channels = make([]string, len(category.Channels))
copy(destCategory.Channels, category.Channels)
destCategory.Muted = category.Muted
}
// The order in which the queries are executed in the transaction is important.
// SidebarCategories need to be update first, and then SidebarChannels should be deleted.
// The net effect remains the same, but it prevents deadlocks from other transactions
// operating on the tables in reverse order.
updateQuery, updateParams, err2 := s.getQueryBuilder().
Update("SidebarCategories").
Set("DisplayName", destCategory.DisplayName).
Set("Sorting", destCategory.Sorting).
Set("Muted", destCategory.Muted).
Set("Collapsed", destCategory.Collapsed).
Where(sq.Eq{"Id": destCategory.Id}).ToSql()
if err2 != nil {
return nil, nil, errors.Wrap(err2, "update_sidebar_categories_tosql1")
}
if _, err = transaction.Exec(updateQuery, updateParams...); err != nil {
return nil, nil, errors.Wrap(err, "failed to update SidebarCategories")
}
// if we are updating DM category, it's order can't channel order cannot be changed.
if category.Type != model.SidebarCategoryDirectMessages {
// Remove any SidebarChannels entries that were either:
// - previously in this category (and any ones that are still in the category will be recreated below)
// - in another category and are being added to this category
query, args, err2 := s.getQueryBuilder().
Delete("SidebarChannels").
Where(
sq.And{
sq.Eq{"ChannelId": srcCategory.Channels},
sq.Eq{"CategoryId": category.Id},
},
).ToSql()
if err2 != nil {
return nil, nil, errors.Wrap(err2, "update_sidebar_categories_tosql2")
}
if _, err = transaction.Exec(query, args...); err != nil {
return nil, nil, errors.Wrap(err, "failed to delete SidebarChannels")
}
runningOrder := 0
insertQuery := s.getQueryBuilder().
Insert("SidebarChannels").
Columns("ChannelId", "UserId", "CategoryId", "SortOrder")
for _, channelID := range category.Channels {
insertQuery = insertQuery.Values(channelID, userId, category.Id, int64(runningOrder))
runningOrder += model.MinimalSidebarSortDistance
}
if len(category.Channels) > 0 {
sql, args, err2 := insertQuery.ToSql()
if err2 != nil {
return nil, nil, errors.Wrap(err2, "InsertSidebarChannels_Tosql")
}
if _, err2 := transaction.Exec(sql, args...); err2 != nil {
return nil, nil, errors.Wrap(err2, "failed to save SidebarChannels")
}
}
}
// Update the favorites preferences based on channels moving into or out of the Favorites category for compatibility
if category.Type == model.SidebarCategoryFavorites {
// Remove any old favorites
sql, args, err2 := s.getQueryBuilder().Delete("Preferences").Where(
sq.Eq{
"UserId": userId,
"Name": srcCategory.Channels,
"Category": model.PreferenceCategoryFavoriteChannel,
},
).ToSql()
if err2 != nil {
return nil, nil, errors.Wrap(err2, "UpdateSidebarChannels_Tosql_DeletePreferences")
}
if _, err = transaction.Exec(sql, args...); err != nil {
return nil, nil, errors.Wrap(err, "failed to delete Preferences")
}
// And then add the new ones
for _, channelID := range category.Channels {
// This breaks the PreferenceStore abstraction, but it should be safe to assume that everything is a SQL
// store in this package.
if err = s.Preference().(*SqlPreferenceStore).save(transaction, &model.Preference{
Name: channelID,
UserId: userId,
Category: model.PreferenceCategoryFavoriteChannel,
Value: "true",
}); err != nil {
return nil, nil, errors.Wrap(err, "failed to save Preference")
}
}
} else {
// Remove any old favorites that might have been in this category
query, args, nErr := s.getQueryBuilder().Delete("Preferences").Where(
sq.Eq{
"UserId": userId,
"Name": category.Channels,
"Category": model.PreferenceCategoryFavoriteChannel,
},
).ToSql()
if nErr != nil {
return nil, nil, errors.Wrap(nErr, "update_sidebar_categories_tosql")
}
if _, nErr = transaction.Exec(query, args...); nErr != nil {
return nil, nil, errors.Wrap(nErr, "failed to delete Preferences")
}
}
updatedCategories = append(updatedCategories, destCategory)
originalCategories = append(originalCategories, srcCategory)
}
// Ensure Channels are populated for Channels/Direct Messages category if they change
for i, updatedCategory := range updatedCategories {
populated, nErr := s.completePopulatingCategoryChannelsT(transaction, updatedCategory)
if nErr != nil {
return nil, nil, nErr
}
updatedCategories[i] = populated
}
if err = transaction.Commit(); err != nil {
return nil, nil, errors.Wrap(err, "commit_transaction")
}
return updatedCategories, originalCategories, nil
}
// UpdateSidebarChannelsByPreferences is called when the Preference table is being updated to keep SidebarCategories in sync
// At the moment, it's only handling Favorites and NOT DMs/GMs (those will be handled client side)
func (s SqlChannelStore) UpdateSidebarChannelsByPreferences(preferences model.Preferences) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "UpdateSidebarChannelsByPreferences: begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
for _, preference := range preferences {
preference := preference
if preference.Category != model.PreferenceCategoryFavoriteChannel {
continue
}
// if new preference is false - remove the channel from the appropriate sidebar category
if preference.Value == "false" {
if err := s.removeSidebarEntriesForPreferenceT(transaction, &preference); err != nil {
return errors.Wrap(err, "UpdateSidebarChannelsByPreferences: removeSidebarEntriesForPreferenceT")
}
} else {
if err := s.addChannelToFavoritesCategoryT(transaction, &preference); err != nil {
return errors.Wrap(err, "UpdateSidebarChannelsByPreferences: addChannelToFavoritesCategoryT")
}
}
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "UpdateSidebarChannelsByPreferences: commit_transaction")
}
return nil
}
func (s SqlChannelStore) removeSidebarEntriesForPreferenceT(transaction *sqlxTxWrapper, preference *model.Preference) error {
if preference.Category != model.PreferenceCategoryFavoriteChannel {
return nil
}
// Delete any corresponding SidebarChannels entries in a Favorites category corresponding to this preference.
var query string
if s.DriverName() == model.DatabaseDriverMysql {
query = `
DELETE
SidebarChannels
FROM
SidebarChannels
JOIN
SidebarCategories ON SidebarChannels.CategoryId = SidebarCategories.Id
WHERE
SidebarChannels.UserId = ?
AND SidebarChannels.ChannelId = ?
AND SidebarCategories.Type = ?`
} else {
query = `
DELETE FROM
SidebarChannels
USING
SidebarCategories
WHERE
SidebarChannels.CategoryId = SidebarCategories.Id
AND SidebarChannels.UserId = ?
AND SidebarChannels.ChannelId = ?
AND SidebarCategories.Type = ?`
}
if _, err := transaction.Exec(query, preference.UserId, preference.Name, model.SidebarCategoryFavorites); err != nil {
return errors.Wrap(err, "Failed to remove sidebar entries for preference")
}
return nil
}
func (s SqlChannelStore) addChannelToFavoritesCategoryT(transaction *sqlxTxWrapper, preference *model.Preference) error {
if preference.Category != model.PreferenceCategoryFavoriteChannel {
return nil
}
var channel model.Channel
if err := transaction.Get(&channel, `SELECT * FROM Channels WHERE Id=?`, preference.Name); err != nil {
return errors.Wrapf(err, "Failed to get favorited channel with id=%s", preference.Name)
} else if channel.Id == "" {
return store.NewErrNotFound("Channel", preference.Name)
}
// Get the IDs of the Favorites category/categories that the channel needs to be added to
builder := s.getQueryBuilder().
Select("SidebarCategories.Id").
From("SidebarCategories").
LeftJoin("SidebarChannels on SidebarCategories.Id = SidebarChannels.CategoryId and SidebarChannels.ChannelId = ?", preference.Name).
Where(sq.Eq{
"SidebarCategories.UserId": preference.UserId,
"Type": model.SidebarCategoryFavorites,
}).
Where("SidebarChannels.ChannelId is null")
if channel.TeamId != "" {
builder = builder.Where(sq.Eq{"TeamId": channel.TeamId})
}
idsQuery, idsParams, err := builder.ToSql()
if err != nil {
return errors.Wrap(err, "addChannelToFavoritesCategoryT_ToSql_Select")
}
categoryIds := []string{}
if err = transaction.Select(&categoryIds, idsQuery, idsParams...); err != nil {
return errors.Wrap(err, "Failed to get Favorites sidebar categories")
}
if len(categoryIds) == 0 {
// The channel is already in the Favorites category/categories
return nil
}
// For each category ID, insert a row into SidebarChannels with the given channel ID and a SortOrder that's less than
// all existing SortOrders in the category so that the newly favorited channel comes first
insertQuery, insertParams, err := s.getQueryBuilder().
Insert("SidebarChannels").
Columns(
"ChannelId",
"CategoryId",
"UserId",
"SortOrder",
).
Select(
sq.Select().
Column("? as ChannelId", preference.Name).
Column("SidebarCategories.Id as CategoryId").
Column("? as UserId", preference.UserId).
Column("COALESCE(MIN(SidebarChannels.SortOrder) - 10, 0) as SortOrder").
From("SidebarCategories").
LeftJoin("SidebarChannels on SidebarCategories.Id = SidebarChannels.CategoryId").
Where(sq.Eq{
"SidebarCategories.Id": categoryIds,
}).
GroupBy("SidebarCategories.Id")).ToSql()
if err != nil {
return errors.Wrap(err, "addChannelToFavoritesCategoryT_ToSql_Insert")
}
if _, err := transaction.Exec(insertQuery, insertParams...); err != nil {
return errors.Wrap(err, "Failed to add sidebar entries for favorited channel")
}
return nil
}
// DeleteSidebarChannelsByPreferences is called when the Preference table is being updated to keep SidebarCategories in sync
// At the moment, it's only handling Favorites and NOT DMs/GMs (those will be handled client side)
func (s SqlChannelStore) DeleteSidebarChannelsByPreferences(preferences model.Preferences) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "DeleteSidebarChannelsByPreferences: begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
for _, preference := range preferences {
preference := preference
if preference.Category != model.PreferenceCategoryFavoriteChannel {
continue
}
if err := s.removeSidebarEntriesForPreferenceT(transaction, &preference); err != nil {
return errors.Wrap(err, "DeleteSidebarChannelsByPreferences: removeSidebarEntriesForPreferenceT")
}
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "DeleteSidebarChannelsByPreferences: commit_transaction")
}
return nil
}
//nolint:unparam
func (s SqlChannelStore) UpdateSidebarChannelCategoryOnMove(channel *model.Channel, newTeamId string) error {
// if channel is being moved, remove it from the categories, since it's possible that there's no matching category in the new team
if _, err := s.GetMasterX().Exec("DELETE FROM SidebarChannels WHERE ChannelId=?", channel.Id); err != nil {
return errors.Wrapf(err, "failed to delete SidebarChannels with channelId=%s", channel.Id)
}
return nil
}
func (s SqlChannelStore) ClearSidebarOnTeamLeave(userId, teamId string) error {
// if user leaves the team, clean their team related entries in sidebar channels and categories
var deleteQuery string
if s.DriverName() == model.DatabaseDriverMysql {
deleteQuery = "DELETE SidebarChannels FROM SidebarChannels LEFT JOIN SidebarCategories ON SidebarCategories.Id = SidebarChannels.CategoryId WHERE SidebarCategories.TeamId=? AND SidebarCategories.UserId=?"
} else {
deleteQuery = `
DELETE FROM
SidebarChannels
WHERE
CategoryId IN (
SELECT
CategoryId
FROM
SidebarChannels,
SidebarCategories
WHERE
SidebarChannels.CategoryId = SidebarCategories.Id
AND SidebarCategories.TeamId = ?
AND SidebarChannels.UserId = ?)`
}
if _, err := s.GetMasterX().Exec(deleteQuery, teamId, userId); err != nil {
return errors.Wrap(err, "failed to delete from SidebarChannels")
}
if _, err := s.GetMasterX().Exec("DELETE FROM SidebarCategories WHERE SidebarCategories.TeamId = ? AND SidebarCategories.UserId = ?", teamId, userId); err != nil {
return errors.Wrap(err, "failed to delete from SidebarCategories")
}
return nil
}
// DeleteSidebarCategory removes a custom category and moves any channels into it into the Channels and Direct Messages
// categories respectively. Assumes that the provided user ID and team ID match the given category ID.
func (s SqlChannelStore) DeleteSidebarCategory(categoryId string) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
// Ensure that we're deleting a custom category
var category model.SidebarCategory
if err = transaction.Get(&category, "SELECT * FROM SidebarCategories WHERE Id = ?", categoryId); err != nil {
return errors.Wrapf(err, "failed to find SidebarCategories with id=%s", categoryId)
}
if category.Type != model.SidebarCategoryCustom {
return store.NewErrInvalidInput("SidebarCategory", "id", categoryId)
}
// The order in which the queries are executed in the transaction is important.
// SidebarCategories need to be deleted first, and then SidebarChannels.
// The net effect remains the same, but it prevents deadlocks from other transactions
// operating on the tables in reverse order.
// Delete the category itself
query, args, err := s.getQueryBuilder().
Delete("SidebarCategories").
Where(sq.Eq{"Id": categoryId}).ToSql()
if err != nil {
return errors.Wrap(err, "delete_sidebar_category_tosql")
}
if _, err = transaction.Exec(query, args...); err != nil {
return errors.Wrap(err, "failed to delete SidebarCategory")
}
// Delete the channels in the category
query, args, err = s.getQueryBuilder().
Delete("SidebarChannels").
Where(sq.Eq{"CategoryId": categoryId}).ToSql()
if err != nil {
return errors.Wrap(err, "delete_sidebar_category_tosql")
}
if _, err = transaction.Exec(query, args...); err != nil {
return errors.Wrap(err, "failed to delete SidebarChannel")
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type sqlClusterDiscoveryStore struct {
*SqlStore
}
func newSqlClusterDiscoveryStore(sqlStore *SqlStore) store.ClusterDiscoveryStore {
return &sqlClusterDiscoveryStore{sqlStore}
}
func (s sqlClusterDiscoveryStore) Save(ClusterDiscovery *model.ClusterDiscovery) error {
ClusterDiscovery.PreSave()
if err := ClusterDiscovery.IsValid(); err != nil {
return err
}
if _, err := s.GetMasterX().NamedExec(`
INSERT INTO
ClusterDiscovery
(Id, Type, ClusterName, Hostname, GossipPort, Port, CreateAt, LastPingAt)
VALUES
(:Id, :Type, :ClusterName, :Hostname, :GossipPort, :Port, :CreateAt, :LastPingAt)
`, ClusterDiscovery); err != nil {
return errors.Wrap(err, "failed to save ClusterDiscovery")
}
return nil
}
func (s sqlClusterDiscoveryStore) Delete(ClusterDiscovery *model.ClusterDiscovery) (bool, error) {
query := s.getQueryBuilder().
Delete("ClusterDiscovery").
Where(sq.Eq{"Type": ClusterDiscovery.Type}).
Where(sq.Eq{"ClusterName": ClusterDiscovery.ClusterName}).
Where(sq.Eq{"Hostname": ClusterDiscovery.Hostname})
queryString, args, err := query.ToSql()
if err != nil {
return false, errors.Wrap(err, "cluster_discovery_tosql")
}
res, err := s.GetMasterX().Exec(queryString, args...)
if err != nil {
return false, errors.Wrap(err, "failed to delete ClusterDiscovery")
}
count, err := res.RowsAffected()
if err != nil {
return false, errors.Wrap(err, "failed to count rows affected")
}
return count != 0, nil
}
func (s sqlClusterDiscoveryStore) Exists(ClusterDiscovery *model.ClusterDiscovery) (bool, error) {
query := s.getQueryBuilder().
Select("COUNT(*)").
From("ClusterDiscovery").
Where(sq.Eq{"Type": ClusterDiscovery.Type}).
Where(sq.Eq{"ClusterName": ClusterDiscovery.ClusterName}).
Where(sq.Eq{"Hostname": ClusterDiscovery.Hostname})
queryString, args, err := query.ToSql()
if err != nil {
return false, errors.Wrap(err, "cluster_discovery_tosql")
}
var count int
if err := s.GetMasterX().Get(&count, queryString, args...); err != nil {
return false, errors.Wrap(err, "failed to count ClusterDiscovery")
}
return count != 0, nil
}
func (s sqlClusterDiscoveryStore) GetAll(ClusterDiscoveryType, clusterName string) ([]*model.ClusterDiscovery, error) {
query := s.getQueryBuilder().
Select("*").
From("ClusterDiscovery").
Where(sq.Eq{"Type": ClusterDiscoveryType}).
Where(sq.Eq{"ClusterName": clusterName}).
Where(sq.Gt{"LastPingAt": model.GetMillis() - model.CDSOfflineAfterMillis})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "cluster_discovery_tosql")
}
list := []*model.ClusterDiscovery{}
if err := s.GetMasterX().Select(&list, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find ClusterDiscovery")
}
return list, nil
}
func (s sqlClusterDiscoveryStore) SetLastPingAt(ClusterDiscovery *model.ClusterDiscovery) error {
query := s.getQueryBuilder().
Update("ClusterDiscovery").
Set("LastPingAt", model.GetMillis()).
Where(sq.Eq{"Type": ClusterDiscovery.Type}).
Where(sq.Eq{"ClusterName": ClusterDiscovery.ClusterName}).
Where(sq.Eq{"Hostname": ClusterDiscovery.Hostname})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "cluster_discovery_tosql")
}
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrap(err, "failed to update ClusterDiscovery")
}
return nil
}
func (s sqlClusterDiscoveryStore) Cleanup() error {
query := s.getQueryBuilder().
Delete("ClusterDiscovery").
Where(sq.Lt{"LastPingAt": model.GetMillis() - model.CDSOfflineAfterMillis})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "cluster_discovery_tosql")
}
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrap(err, "failed to delete ClusterDiscoveries")
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlCommandStore struct {
*SqlStore
commandsQuery sq.SelectBuilder
}
func newSqlCommandStore(sqlStore *SqlStore) store.CommandStore {
s := &SqlCommandStore{SqlStore: sqlStore}
s.commandsQuery = s.getQueryBuilder().
Select("*").
From("Commands")
return s
}
func (s SqlCommandStore) Save(command *model.Command) (*model.Command, error) {
if command.Id != "" {
return nil, store.NewErrInvalidInput("Command", "CommandId", command.Id)
}
command.PreSave()
if err := command.IsValid(); err != nil {
return nil, err
}
// Trigger is a keyword
trigger := s.toReserveCase("trigger")
if _, err := s.GetMasterX().NamedExec(`INSERT INTO Commands (Id, Token, CreateAt,
UpdateAt, DeleteAt, CreatorId, TeamId, `+trigger+`, Method, Username,
IconURL, AutoComplete, AutoCompleteDesc, AutoCompleteHint, DisplayName, Description,
URL, PluginId)
VALUES (:Id, :Token, :CreateAt, :UpdateAt, :DeleteAt, :CreatorId, :TeamId, :Trigger, :Method,
:Username, :IconURL, :AutoComplete, :AutoCompleteDesc, :AutoCompleteHint, :DisplayName,
:Description, :URL, :PluginId)`, command); err != nil {
return nil, errors.Wrapf(err, "insert: command_id=%s", command.Id)
}
return command, nil
}
func (s SqlCommandStore) Get(id string) (*model.Command, error) {
var command model.Command
query, args, err := s.commandsQuery.
Where(sq.Eq{"Id": id, "DeleteAt": 0}).ToSql()
if err != nil {
return nil, errors.Wrapf(err, "commands_tosql")
}
if err = s.GetReplicaX().Get(&command, query, args...); err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Command", id)
} else if err != nil {
return nil, errors.Wrapf(err, "selectone: command_id=%s", id)
}
return &command, nil
}
func (s SqlCommandStore) GetByTeam(teamId string) ([]*model.Command, error) {
commands := []*model.Command{}
sql, args, err := s.commandsQuery.
Where(sq.Eq{"TeamId": teamId, "DeleteAt": 0}).ToSql()
if err != nil {
return nil, errors.Wrapf(err, "commands_tosql")
}
if err := s.GetReplicaX().Select(&commands, sql, args...); err != nil {
return nil, errors.Wrapf(err, "select: team_id=%s", teamId)
}
return commands, nil
}
func (s SqlCommandStore) GetByTrigger(teamId string, trigger string) (*model.Command, error) {
var command model.Command
var triggerStr string
if s.DriverName() == "mysql" {
triggerStr = "`Trigger`"
} else {
triggerStr = "\"trigger\""
}
query, args, err := s.commandsQuery.
Where(sq.Eq{"TeamId": teamId, "DeleteAt": 0, triggerStr: trigger}).ToSql()
if err != nil {
return nil, errors.Wrapf(err, "commands_tosql")
}
if err := s.GetReplicaX().Get(&command, query, args...); err == sql.ErrNoRows {
errorId := "teamId=" + teamId + ", trigger=" + trigger
return nil, store.NewErrNotFound("Command", errorId)
} else if err != nil {
return nil, errors.Wrapf(err, "selectone: team_id=%s, trigger=%s", teamId, trigger)
}
return &command, nil
}
func (s SqlCommandStore) Delete(commandId string, time int64) error {
sql, args, err := s.getQueryBuilder().
Update("Commands").
SetMap(sq.Eq{"DeleteAt": time, "UpdateAt": time}).
Where(sq.Eq{"Id": commandId}).ToSql()
if err != nil {
return errors.Wrapf(err, "commands_tosql")
}
_, err = s.GetMasterX().Exec(sql, args...)
if err != nil {
errors.Wrapf(err, "delete: command_id=%s", commandId)
}
return nil
}
func (s SqlCommandStore) PermanentDeleteByTeam(teamId string) error {
sql, args, err := s.getQueryBuilder().
Delete("Commands").
Where(sq.Eq{"TeamId": teamId}).ToSql()
if err != nil {
return errors.Wrapf(err, "commands_tosql")
}
_, err = s.GetMasterX().Exec(sql, args...)
if err != nil {
return errors.Wrapf(err, "delete: team_id=%s", teamId)
}
return nil
}
func (s SqlCommandStore) PermanentDeleteByUser(userId string) error {
sql, args, err := s.getQueryBuilder().
Delete("Commands").
Where(sq.Eq{"CreatorId": userId}).ToSql()
if err != nil {
return errors.Wrapf(err, "commands_tosql")
}
_, err = s.GetMasterX().Exec(sql, args...)
if err != nil {
return errors.Wrapf(err, "delete: user_id=%s", userId)
}
return nil
}
func (s SqlCommandStore) Update(cmd *model.Command) (*model.Command, error) {
cmd.UpdateAt = model.GetMillis()
if err := cmd.IsValid(); err != nil {
return nil, err
}
query := s.getQueryBuilder().
Update("Commands").
Set("Token", cmd.Token).
Set("CreateAt", cmd.CreateAt).
Set("UpdateAt", cmd.UpdateAt).
Set("CreatorId", cmd.CreatorId).
Set("TeamId", cmd.TeamId).
Set("Method", cmd.Method).
Set("Username", cmd.Username).
Set("IconURL", cmd.IconURL).
Set("AutoComplete", cmd.AutoComplete).
Set("AutoCompleteDesc", cmd.AutoCompleteDesc).
Set("AutoCompleteHint", cmd.AutoCompleteHint).
Set("DisplayName", cmd.DisplayName).
Set("Description", cmd.Description).
Set("URL", cmd.URL).
Set("PluginId", cmd.PluginId).
Where(sq.Eq{"Id": cmd.Id})
// Trigger is a keyword
query = query.Set(s.toReserveCase("trigger"), cmd.Trigger)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "commands_tosql")
}
res, err := s.GetMasterX().Exec(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to update commands")
}
count, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "error while getting rows_affected")
}
if count > 1 {
return nil, fmt.Errorf("unexpected count while updating commands: count=%d, Id=%s", count, cmd.Id)
}
return cmd, nil
}
func (s SqlCommandStore) AnalyticsCommandCount(teamId string) (int64, error) {
query := s.getQueryBuilder().
Select("COUNT(*)").
From("Commands").
Where(sq.Eq{"DeleteAt": 0})
if teamId != "" {
query = query.Where(sq.Eq{"TeamId": teamId})
}
sql, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrapf(err, "commands_tosql")
}
var c int64
err = s.GetReplicaX().Get(&c, sql, args...)
if err != nil {
return 0, errors.Wrapf(err, "unable to count the commands: team_id=%s", teamId)
}
return c, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SqlCommandWebhookStore struct {
*SqlStore
}
func newSqlCommandWebhookStore(sqlStore *SqlStore) store.CommandWebhookStore {
return &SqlCommandWebhookStore{sqlStore}
}
func (s SqlCommandWebhookStore) Save(webhook *model.CommandWebhook) (*model.CommandWebhook, error) {
if webhook.Id != "" {
return nil, store.NewErrInvalidInput("CommandWebhook", "id", webhook.Id)
}
webhook.PreSave()
if err := webhook.IsValid(); err != nil {
return nil, err
}
if _, err := s.GetMasterX().NamedExec(`INSERT INTO CommandWebhooks
(Id,CreateAt,CommandId,UserId,ChannelId,RootId,UseCount)
Values
(:Id, :CreateAt, :CommandId, :UserId, :ChannelId, :RootId, :UseCount)`, webhook); err != nil {
return nil, errors.Wrapf(err, "save: id=%s", webhook.Id)
}
return webhook, nil
}
func (s SqlCommandWebhookStore) Get(id string) (*model.CommandWebhook, error) {
var webhook model.CommandWebhook
exptime := model.GetMillis() - model.CommandWebhookLifetime
query := s.getQueryBuilder().
Select("*").
From("CommandWebhooks").
Where(sq.Eq{"Id": id}).
Where(sq.Gt{"CreateAt": exptime})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_tosql")
}
if err := s.GetReplicaX().Get(&webhook, queryString, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("CommandWebhook", id)
}
return nil, errors.Wrapf(err, "get: id=%s", id)
}
return &webhook, nil
}
func (s SqlCommandWebhookStore) TryUse(id string, limit int) error {
query := s.getQueryBuilder().
Update("CommandWebhooks").
Set("UseCount", sq.Expr("UseCount + 1")).
Where(sq.Eq{"Id": id}).
Where(sq.Lt{"UseCount": limit})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "tryuse_tosql")
}
if sqlResult, err := s.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrapf(err, "tryuse: id=%s limit=%d", id, limit)
} else if rows, err := sqlResult.RowsAffected(); rows == 0 {
return store.NewErrInvalidInput("CommandWebhook", "id", id).Wrap(err)
}
return nil
}
func (s SqlCommandWebhookStore) Cleanup() {
mlog.Debug("Cleaning up command webhook store.")
exptime := model.GetMillis() - model.CommandWebhookLifetime
query := s.getQueryBuilder().
Delete("CommandWebhooks").
Where(sq.Lt{"CreateAt": exptime})
queryString, args, err := query.ToSql()
if err != nil {
mlog.Error("Failed to build query when trying to perform a cleanup in command webhook store.")
return
}
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
mlog.Error("Unable to cleanup command webhook store.")
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
"fmt"
"strings"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlComplianceStore struct {
*SqlStore
}
func newSqlComplianceStore(sqlStore *SqlStore) store.ComplianceStore {
return &SqlComplianceStore{sqlStore}
}
func (s SqlComplianceStore) Save(compliance *model.Compliance) (*model.Compliance, error) {
compliance.PreSave()
if err := compliance.IsValid(); err != nil {
return nil, err
}
// DESC is a keyword
desc := s.toReserveCase("desc")
query := `INSERT INTO Compliances (Id, CreateAt, UserId, Status, Count, ` + desc + `, Type, StartAt, EndAt, Keywords, Emails)
VALUES
(:Id, :CreateAt, :UserId, :Status, :Count, :Desc, :Type, :StartAt, :EndAt, :Keywords, :Emails)`
if _, err := s.GetMasterX().NamedExec(query, compliance); err != nil {
return nil, errors.Wrap(err, "failed to save Compliance")
}
return compliance, nil
}
func (s SqlComplianceStore) Update(compliance *model.Compliance) (*model.Compliance, error) {
if err := compliance.IsValid(); err != nil {
return nil, err
}
query := s.getQueryBuilder().
Update("Compliances").
Set("CreateAt", compliance.CreateAt).
Set("UserId", compliance.UserId).
Set("Status", compliance.Status).
Set("Count", compliance.Count).
Set("Type", compliance.Type).
Set("StartAt", compliance.StartAt).
Set("EndAt", compliance.EndAt).
Set("Keywords", compliance.Keywords).
Set("Emails", compliance.Emails).
Where(sq.Eq{"Id": compliance.Id})
// DESC is a keyword
query = query.Set(s.toReserveCase("desc"), compliance.Desc)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "compliances_tosql")
}
res, err := s.GetMasterX().Exec(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to update Compliance")
}
count, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "error while getting rows_affected")
}
if count > 1 {
return nil, fmt.Errorf("unexpected count while updating compliances: count=%d, Id=%s", count, compliance.Id)
}
return compliance, nil
}
func (s SqlComplianceStore) GetAll(offset, limit int) (model.Compliances, error) {
query := "SELECT * FROM Compliances ORDER BY CreateAt DESC LIMIT ? OFFSET ?"
compliances := model.Compliances{}
if err := s.GetReplicaX().Select(&compliances, query, limit, offset); err != nil {
return nil, errors.Wrap(err, "failed to find all Compliances")
}
return compliances, nil
}
func (s SqlComplianceStore) Get(id string) (*model.Compliance, error) {
var compliance model.Compliance
if err := s.GetReplicaX().Get(&compliance, `SELECT * FROM Compliances WHERE Id = ?`, id); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Compliances", id)
}
return nil, errors.Wrapf(err, "failed to get Compliance with id=%s", id)
}
if compliance.Id == "" {
return nil, store.NewErrNotFound("Compliance", id)
}
return &compliance, nil
}
func (s SqlComplianceStore) ComplianceExport(job *model.Compliance, cursor model.ComplianceExportCursor, limit int) ([]*model.CompliancePost, model.ComplianceExportCursor, error) {
keywordQuery := ""
var argsKeywords []any
keywords := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(job.Keywords, ",", " ", -1))))
if len(keywords) > 0 {
clauses := make([]string, len(keywords))
for i, keyword := range keywords {
keyword = sanitizeSearchTerm(keyword, "\\")
clauses[i] = "LOWER(Posts.Message) LIKE ?"
argsKeywords = append(argsKeywords, "%"+keyword+"%")
}
keywordQuery = "AND (" + strings.Join(clauses, " OR ") + ")"
}
emailQuery := ""
var argsEmails []any
emails := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(job.Emails, ",", " ", -1))))
if len(emails) > 0 {
clauses := make([]string, len(emails))
for i, email := range emails {
clauses[i] = "Users.Email = ?"
argsEmails = append(argsEmails, email)
}
emailQuery = "AND (" + strings.Join(clauses, " OR ") + ")"
}
// The idea is to first iterate over the channel posts, and then when we run out of those,
// start iterating over the direct message posts.
channelPosts := []*model.CompliancePost{}
channelsQuery := ""
var argsChannelsQuery []any
if !cursor.ChannelsQueryCompleted {
if cursor.LastChannelsQueryPostCreateAt == 0 {
cursor.LastChannelsQueryPostCreateAt = job.StartAt
}
// append the named parameters of SQL query in the correct order to argsChannelsQuery
argsChannelsQuery = append(argsChannelsQuery, cursor.LastChannelsQueryPostCreateAt, cursor.LastChannelsQueryPostCreateAt, cursor.LastChannelsQueryPostID, job.EndAt)
argsChannelsQuery = append(argsChannelsQuery, argsEmails...)
argsChannelsQuery = append(argsChannelsQuery, argsKeywords...)
argsChannelsQuery = append(argsChannelsQuery, limit)
channelsQuery = `
SELECT
Teams.Name AS TeamName,
Teams.DisplayName AS TeamDisplayName,
Channels.Name AS ChannelName,
Channels.DisplayName AS ChannelDisplayName,
Channels.Type AS ChannelType,
Users.Username AS UserUsername,
Users.Email AS UserEmail,
Users.Nickname AS UserNickname,
Posts.Id AS PostId,
Posts.CreateAt AS PostCreateAt,
Posts.UpdateAt AS PostUpdateAt,
Posts.DeleteAt AS PostDeleteAt,
Posts.RootId AS PostRootId,
Posts.OriginalId AS PostOriginalId,
Posts.Message AS PostMessage,
Posts.Type AS PostType,
Posts.Props AS PostProps,
Posts.Hashtags AS PostHashtags,
Posts.FileIds AS PostFileIds,
Bots.UserId IS NOT NULL AS IsBot
FROM
Teams,
Channels,
Users,
Posts
LEFT JOIN
Bots ON Bots.UserId = Posts.UserId
WHERE
Teams.Id = Channels.TeamId
AND Posts.ChannelId = Channels.Id
AND Posts.UserId = Users.Id
AND (
Posts.CreateAt > ?
OR (Posts.CreateAt = ? AND Posts.Id > ?)
)
AND Posts.CreateAt < ?
` + emailQuery + `
` + keywordQuery + `
ORDER BY Posts.CreateAt, Posts.Id
LIMIT ?`
if err := s.GetReplicaX().Select(&channelPosts, channelsQuery, argsChannelsQuery...); err != nil {
return nil, cursor, errors.Wrap(err, "unable to export compliance")
}
if len(channelPosts) < limit {
cursor.ChannelsQueryCompleted = true
} else {
cursor.LastChannelsQueryPostCreateAt = channelPosts[len(channelPosts)-1].PostCreateAt
cursor.LastChannelsQueryPostID = channelPosts[len(channelPosts)-1].PostId
}
}
directMessagePosts := []*model.CompliancePost{}
directMessagesQuery := ""
var argsDirectMessagesQuery []any
if !cursor.DirectMessagesQueryCompleted && len(channelPosts) < limit {
if cursor.LastDirectMessagesQueryPostCreateAt == 0 {
cursor.LastDirectMessagesQueryPostCreateAt = job.StartAt
}
// append the named parameters of SQL query in the correct order to argsDirectMessagesQuery
argsDirectMessagesQuery = append(argsDirectMessagesQuery, cursor.LastDirectMessagesQueryPostCreateAt, cursor.LastDirectMessagesQueryPostCreateAt, cursor.LastDirectMessagesQueryPostID, job.EndAt)
argsDirectMessagesQuery = append(argsDirectMessagesQuery, argsEmails...)
argsDirectMessagesQuery = append(argsDirectMessagesQuery, argsKeywords...)
argsDirectMessagesQuery = append(argsDirectMessagesQuery, limit-len(channelPosts))
directMessagesQuery = `
SELECT
'direct-messages' AS TeamName,
'Direct Messages' AS TeamDisplayName,
Channels.Name AS ChannelName,
Channels.DisplayName AS ChannelDisplayName,
Channels.Type AS ChannelType,
Users.Username AS UserUsername,
Users.Email AS UserEmail,
Users.Nickname AS UserNickname,
Posts.Id AS PostId,
Posts.CreateAt AS PostCreateAt,
Posts.UpdateAt AS PostUpdateAt,
Posts.DeleteAt AS PostDeleteAt,
Posts.RootId AS PostRootId,
Posts.OriginalId AS PostOriginalId,
Posts.Message AS PostMessage,
Posts.Type AS PostType,
Posts.Props AS PostProps,
Posts.Hashtags AS PostHashtags,
Posts.FileIds AS PostFileIds,
Bots.UserId IS NOT NULL AS IsBot
FROM
Channels,
Users,
Posts
LEFT JOIN
Bots ON Bots.UserId = Posts.UserId
WHERE
Channels.TeamId = ''
AND Posts.ChannelId = Channels.Id
AND Posts.UserId = Users.Id
AND (
Posts.CreateAt > ?
OR (Posts.CreateAt = ? AND Posts.Id > ?)
)
AND Posts.CreateAt < ?
` + emailQuery + `
` + keywordQuery + `
ORDER BY Posts.CreateAt, Posts.Id
LIMIT ?`
if err := s.GetReplicaX().Select(&directMessagePosts, directMessagesQuery, argsDirectMessagesQuery...); err != nil {
return nil, cursor, errors.Wrap(err, "unable to export compliance")
}
if len(directMessagePosts) < limit {
cursor.DirectMessagesQueryCompleted = true
} else {
cursor.LastDirectMessagesQueryPostCreateAt = directMessagePosts[len(directMessagePosts)-1].PostCreateAt
cursor.LastDirectMessagesQueryPostID = directMessagePosts[len(directMessagePosts)-1].PostId
}
}
return append(channelPosts, directMessagePosts...), cursor, nil
}
func (s SqlComplianceStore) MessageExport(ctx context.Context, cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) {
var args []any
args = append(args, model.ChannelTypeDirect, model.ChannelTypeGroup, cursor.LastPostUpdateAt, cursor.LastPostUpdateAt, cursor.LastPostId, limit)
query :=
`SELECT
Posts.Id AS PostId,
Posts.CreateAt AS PostCreateAt,
Posts.UpdateAt AS PostUpdateAt,
Posts.DeleteAt AS PostDeleteAt,
Posts.Message AS PostMessage,
Posts.Type AS PostType,
Posts.Props AS PostProps,
Posts.OriginalId AS PostOriginalId,
Posts.RootId AS PostRootId,
Posts.FileIds AS PostFileIds,
Teams.Id AS TeamId,
Teams.Name AS TeamName,
Teams.DisplayName AS TeamDisplayName,
Channels.Id AS ChannelId,
CASE
WHEN Channels.Type = ? THEN 'Direct Message'
WHEN Channels.Type = ? THEN 'Group Message'
ELSE Channels.DisplayName
END AS ChannelDisplayName,
Channels.Name AS ChannelName,
Channels.Type AS ChannelType,
Users.Id AS UserId,
Users.Email AS UserEmail,
Users.Username,
Bots.UserId IS NOT NULL AS IsBot
FROM
Posts
LEFT OUTER JOIN Channels ON Posts.ChannelId = Channels.Id
LEFT OUTER JOIN Teams ON Channels.TeamId = Teams.Id
LEFT OUTER JOIN Users ON Posts.UserId = Users.Id
LEFT JOIN Bots ON Bots.UserId = Posts.UserId
WHERE (
Posts.UpdateAt > ?
OR (
Posts.UpdateAt = ?
AND Posts.Id > ?
)
) AND Posts.Type NOT LIKE 'system_%'
ORDER BY PostUpdateAt, PostId
LIMIT ?`
cposts := []*model.MessageExport{}
if err := s.GetReplicaX().SelectCtx(ctx, &cposts, query, args...); err != nil {
return nil, cursor, errors.Wrap(err, "unable to export messages")
}
if len(cposts) > 0 {
cursor.LastPostUpdateAt = *cposts[len(cposts)-1].PostUpdateAt
cursor.LastPostId = *cposts[len(cposts)-1].PostId
}
return cposts, cursor, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
)
// storeContextKey is the base type for all context keys for the store.
type storeContextKey string
// contextValue is a type to hold some pre-determined context values.
type contextValue string
// Different possible values of contextValue.
const (
useMaster contextValue = "useMaster"
)
// WithMaster adds the context value that master DB should be selected for this request.
func WithMaster(ctx context.Context) context.Context {
return context.WithValue(ctx, storeContextKey(useMaster), true)
}
// hasMaster is a helper function to check whether master DB should be selected or not.
func hasMaster(ctx context.Context) bool {
if v := ctx.Value(storeContextKey(useMaster)); v != nil {
if res, ok := v.(bool); ok && res {
return true
}
}
return false
}
// DBXFromContext is a helper utility that returns the sqlx DB handle from a given context.
func (ss *SqlStore) DBXFromContext(ctx context.Context) *sqlxDBWrapper {
if hasMaster(ctx) {
return ss.GetMasterX()
}
return ss.GetReplicaX()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"sync"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SqlDraftStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
maxDraftSizeOnce sync.Once
maxDraftSizeCached int
}
func draftSliceColumns() []string {
return []string{
"CreateAt",
"UpdateAt",
"DeleteAt",
"Message",
"RootId",
"ChannelId",
"UserId",
"FileIds",
"Props",
"Priority",
}
}
func draftToSlice(draft *model.Draft) []interface{} {
return []interface{}{
draft.CreateAt,
draft.UpdateAt,
draft.DeleteAt,
draft.Message,
draft.RootId,
draft.ChannelId,
draft.UserId,
model.ArrayToJSON(draft.FileIds),
model.StringInterfaceToJSON(draft.Props),
model.StringInterfaceToJSON(draft.Priority),
}
}
func newSqlDraftStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.DraftStore {
return &SqlDraftStore{
SqlStore: sqlStore,
metrics: metrics,
maxDraftSizeCached: model.PostMessageMaxRunesV1,
}
}
func (s *SqlDraftStore) Get(userId, channelId, rootId string, includeDeleted bool) (*model.Draft, error) {
query := s.getQueryBuilder().
Select(draftSliceColumns()...).
From("Drafts").
Where(sq.Eq{
"UserId": userId,
"ChannelId": channelId,
"RootId": rootId,
})
if !includeDeleted {
query = query.Where(sq.Eq{"DeleteAt": 0})
}
dt := model.Draft{}
err := s.GetReplicaX().GetBuilder(&dt, query)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Draft", channelId)
}
return nil, errors.Wrapf(err, "failed to find draft with channelid = %s", channelId)
}
return &dt, nil
}
func (s *SqlDraftStore) Save(draft *model.Draft) (*model.Draft, error) {
draft.PreSave()
maxDraftSize := s.GetMaxDraftSize()
if err := draft.IsValid(maxDraftSize); err != nil {
return nil, err
}
builder := s.getQueryBuilder().Insert("Drafts").Columns(draftSliceColumns()...).Values(draftToSlice(draft)...)
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "save_draft_tosql")
}
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrap(err, "failed to save Draft")
}
return draft, nil
}
func (s *SqlDraftStore) Update(draft *model.Draft) (*model.Draft, error) {
draft.PreUpdate()
maxDraftSize := s.GetMaxDraftSize()
if err := draft.IsValid(maxDraftSize); err != nil {
return nil, err
}
query := s.getQueryBuilder().
Update("Drafts").
Set("UpdateAt", draft.UpdateAt).
Set("Message", draft.Message).
Set("Props", draft.Props).
Set("FileIds", draft.FileIds).
Set("Priority", draft.Priority).
Set("DeleteAt", 0).
Where(sq.Eq{
"UserId": draft.UserId,
"ChannelId": draft.ChannelId,
"RootId": draft.RootId,
})
if _, err := s.GetMasterX().ExecBuilder(query); err != nil {
return nil, errors.Wrapf(err, "failed to update Draft with channelid=%s", draft.ChannelId)
}
return draft, nil
}
func (s *SqlDraftStore) GetDraftsForUser(userID, teamID string) ([]*model.Draft, error) {
var drafts []*model.Draft
query := s.getQueryBuilder().
Select(
"Drafts.CreateAt",
"Drafts.UpdateAt",
"Drafts.Message",
"Drafts.RootId",
"Drafts.ChannelId",
"Drafts.UserId",
"Drafts.FileIds",
"Drafts.Props",
"Drafts.Priority",
).
From("Drafts").
InnerJoin("ChannelMembers ON ChannelMembers.ChannelId = Drafts.ChannelId").
Where(sq.And{
sq.Eq{"Drafts.DeleteAt": 0},
sq.Eq{"Drafts.UserId": userID},
sq.Eq{"ChannelMembers.UserId": userID},
}).
OrderBy("Drafts.UpdateAt DESC")
if teamID != "" {
query = query.
Join("Channels ON Drafts.ChannelId = Channels.Id").
Where(sq.Or{
sq.Eq{"Channels.TeamId": teamID},
sq.Eq{"Channels.TeamId": ""},
})
}
err := s.GetReplicaX().SelectBuilder(&drafts, query)
if err != nil {
return nil, errors.Wrap(err, "failed to get user drafts")
}
return drafts, nil
}
func (s *SqlDraftStore) Delete(userID, channelID, rootID string) error {
time := model.GetMillis()
query := s.getQueryBuilder().
Update("Drafts").
Set("UpdateAt", time).
Set("DeleteAt", time).
Where(sq.Eq{
"UserId": userID,
"ChannelId": channelID,
"RootId": rootID,
})
sql, args, err := query.ToSql()
if err != nil {
return errors.Wrapf(err, "failed to convert to sql")
}
_, err = s.GetMasterX().Exec(sql, args...)
if err != nil {
return errors.Wrap(err, "failed to delete Draft")
}
return nil
}
// GetMaxDraftSize returns the maximum number of runes that may be stored in a post.
func (s *SqlDraftStore) GetMaxDraftSize() int {
s.maxDraftSizeOnce.Do(func() {
s.maxDraftSizeCached = s.determineMaxDraftSize()
})
return s.maxDraftSizeCached
}
func (s *SqlDraftStore) determineMaxDraftSize() int {
var maxDraftSizeBytes int32
if s.DriverName() == model.DatabaseDriverPostgres {
// The Draft.Message column in Postgres has historically been VARCHAR(4000), but
// may be manually enlarged to support longer drafts.
if err := s.GetReplicaX().Get(&maxDraftSizeBytes, `
SELECT
COALESCE(character_maximum_length, 0)
FROM
information_schema.columns
WHERE
table_name = 'drafts'
AND column_name = 'message'
`); err != nil {
mlog.Warn("Unable to determine the maximum supported draft size", mlog.Err(err))
}
} else if s.DriverName() == model.DatabaseDriverMysql {
// The Draft.Message column in MySQL has historically been TEXT, with a maximum
// limit of 65535.
if err := s.GetReplicaX().Get(&maxDraftSizeBytes, `
SELECT
COALESCE(CHARACTER_MAXIMUM_LENGTH, 0)
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
table_schema = DATABASE()
AND table_name = 'Drafts'
AND column_name = 'Message'
LIMIT 0, 1
`); err != nil {
mlog.Warn("Unable to determine the maximum supported draft size", mlog.Err(err))
}
} else {
mlog.Warn("No implementation found to determine the maximum supported draft size")
}
// Assume a worst-case representation of four bytes per rune.
maxDraftSize := int(maxDraftSizeBytes) / 4
mlog.Info("Draft.Message has size restrictions", mlog.Int("max_characters", maxDraftSize), mlog.Int32("max_bytes", maxDraftSizeBytes))
return maxDraftSize
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlEmojiStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
}
func newSqlEmojiStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.EmojiStore {
return &SqlEmojiStore{
SqlStore: sqlStore,
metrics: metrics,
}
}
func (es SqlEmojiStore) Save(emoji *model.Emoji) (*model.Emoji, error) {
emoji.PreSave()
if err := emoji.IsValid(); err != nil {
return nil, err
}
if _, err := es.GetMasterX().NamedExec(`INSERT INTO Emoji
(Id, CreateAt, UpdateAt, DeleteAt, CreatorId, Name)
VALUES
(:Id, :CreateAt, :UpdateAt, :DeleteAt, :CreatorId, :Name)`, emoji); err != nil {
return nil, errors.Wrap(err, "error saving emoji")
}
return emoji, nil
}
func (es SqlEmojiStore) Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error) {
return es.getBy(ctx, "Id", id)
}
func (es SqlEmojiStore) GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error) {
return es.getBy(ctx, "Name", name)
}
func (es SqlEmojiStore) GetMultipleByName(names []string) ([]*model.Emoji, error) {
// Creating (?, ?, ?) len(names) number of times.
keys := strings.Join(strings.Fields(strings.Repeat("? ", len(names))), ",")
args := makeStringArgs(names)
emojis := []*model.Emoji{}
if err := es.GetReplicaX().Select(&emojis,
`SELECT
*
FROM
Emoji
WHERE
Name IN (`+keys+`)
AND DeleteAt = 0`, args...); err != nil {
return nil, errors.Wrapf(err, "error getting emoji by names %v", names)
}
return emojis, nil
}
func (es SqlEmojiStore) GetList(offset, limit int, sort string) ([]*model.Emoji, error) {
emojis := []*model.Emoji{}
query := "SELECT * FROM Emoji WHERE DeleteAt = 0"
if sort == model.EmojiSortByName {
query += " ORDER BY Name"
}
query += " LIMIT ? OFFSET ?"
if err := es.GetReplicaX().Select(&emojis, query, limit, offset); err != nil {
return nil, errors.Wrap(err, "could not get list of emojis")
}
return emojis, nil
}
func (es SqlEmojiStore) Delete(emoji *model.Emoji, time int64) error {
if sqlResult, err := es.GetMasterX().Exec(
`UPDATE
Emoji
SET
DeleteAt = ?,
UpdateAt = ?
WHERE
Id = ?
AND DeleteAt = 0`, time, time, emoji.Id); err != nil {
return errors.Wrap(err, "could not delete emoji")
} else if rows, err := sqlResult.RowsAffected(); rows == 0 {
return store.NewErrNotFound("Emoji", emoji.Id).Wrap(err)
}
return nil
}
func (es SqlEmojiStore) Search(name string, prefixOnly bool, limit int) ([]*model.Emoji, error) {
emojis := []*model.Emoji{}
name = sanitizeSearchTerm(name, "\\")
term := ""
if !prefixOnly {
term = "%"
}
term += name + "%"
if err := es.GetReplicaX().Select(&emojis,
`SELECT
*
FROM
Emoji
WHERE
Name LIKE ?
AND DeleteAt = 0
ORDER BY Name
LIMIT ?`, term, limit); err != nil {
return nil, errors.Wrapf(err, "could not search emojis by name %s", name)
}
return emojis, nil
}
// getBy returns one active (not deleted) emoji, found by any one column (what/key).
func (es SqlEmojiStore) getBy(ctx context.Context, what, key string) (*model.Emoji, error) {
var emoji model.Emoji
err := es.DBXFromContext(ctx).Get(&emoji,
`SELECT
*
FROM
Emoji
WHERE
`+what+` = ?
AND DeleteAt = 0`, key)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Emoji", fmt.Sprintf("%s=%s", what, key))
}
return nil, errors.Wrapf(err, "could not get emoji by %s with value %s", what, key)
}
return &emoji, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type fileInfoWithChannelID struct {
Id string
CreatorId string
PostId string
ChannelId string
CreateAt int64
UpdateAt int64
DeleteAt int64
Path string
ThumbnailPath string
PreviewPath string
Name string
Extension string
Size int64
MimeType string
Width int
Height int
HasPreviewImage bool
MiniPreview *[]byte
Content string
RemoteId *string
Archived bool
}
func (fi fileInfoWithChannelID) ToModel() *model.FileInfo {
return &model.FileInfo{
Id: fi.Id,
CreatorId: fi.CreatorId,
PostId: fi.PostId,
ChannelId: fi.ChannelId,
CreateAt: fi.CreateAt,
UpdateAt: fi.UpdateAt,
DeleteAt: fi.DeleteAt,
Path: fi.Path,
ThumbnailPath: fi.ThumbnailPath,
PreviewPath: fi.PreviewPath,
Name: fi.Name,
Extension: fi.Extension,
Size: fi.Size,
MimeType: fi.MimeType,
Width: fi.Width,
Height: fi.Height,
HasPreviewImage: fi.HasPreviewImage,
MiniPreview: fi.MiniPreview,
Content: fi.Content,
RemoteId: fi.RemoteId,
}
}
type SqlFileInfoStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
queryFields []string
}
func (fs SqlFileInfoStore) ClearCaches() {
}
func newSqlFileInfoStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.FileInfoStore {
s := &SqlFileInfoStore{
SqlStore: sqlStore,
metrics: metrics,
}
s.queryFields = []string{
"FileInfo.Id",
"FileInfo.CreatorId",
"FileInfo.PostId",
"COALESCE(FileInfo.ChannelId, '') AS ChannelId",
"FileInfo.CreateAt",
"FileInfo.UpdateAt",
"FileInfo.DeleteAt",
"FileInfo.Path",
"FileInfo.ThumbnailPath",
"FileInfo.PreviewPath",
"FileInfo.Name",
"FileInfo.Extension",
"FileInfo.Size",
"FileInfo.MimeType",
"FileInfo.Width",
"FileInfo.Height",
"FileInfo.HasPreviewImage",
"FileInfo.MiniPreview",
"Coalesce(FileInfo.Content, '') AS Content",
"Coalesce(FileInfo.RemoteId, '') AS RemoteId",
"FileInfo.Archived",
}
return s
}
func (fs SqlFileInfoStore) Save(info *model.FileInfo) (*model.FileInfo, error) {
info.PreSave()
if err := info.IsValid(); err != nil {
return nil, err
}
query := `
INSERT INTO FileInfo
(Id, CreatorId, PostId, ChannelId, CreateAt, UpdateAt, DeleteAt, Path, ThumbnailPath, PreviewPath,
Name, Extension, Size, MimeType, Width, Height, HasPreviewImage, MiniPreview, Content, RemoteId)
VALUES
(:Id, :CreatorId, :PostId, :ChannelId, :CreateAt, :UpdateAt, :DeleteAt, :Path, :ThumbnailPath, :PreviewPath,
:Name, :Extension, :Size, :MimeType, :Width, :Height, :HasPreviewImage, :MiniPreview, :Content, :RemoteId)
`
if _, err := fs.GetMasterX().NamedExec(query, info); err != nil {
return nil, errors.Wrap(err, "failed to save FileInfo")
}
return info, nil
}
func (fs SqlFileInfoStore) GetByIds(ids []string) ([]*model.FileInfo, error) {
query := fs.getQueryBuilder().
Select(fs.queryFields...).
From("FileInfo").
Where(sq.Eq{"FileInfo.Id": ids}).
Where(sq.Eq{"FileInfo.DeleteAt": 0}).
OrderBy("FileInfo.CreateAt DESC")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "file_info_tosql")
}
items := []fileInfoWithChannelID{}
if err := fs.GetReplicaX().Select(&items, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find FileInfos")
}
if len(items) == 0 {
return nil, nil
}
infos := make([]*model.FileInfo, 0, len(items))
for _, item := range items {
infos = append(infos, item.ToModel())
}
return infos, nil
}
func (fs SqlFileInfoStore) Upsert(info *model.FileInfo) (*model.FileInfo, error) {
info.PreSave()
if err := info.IsValid(); err != nil {
return nil, err
}
// PostID and ChannelID are deliberately ignored
// from the list of fields to keep those two immutable.
queryString, args, err := fs.getQueryBuilder().
Update("FileInfo").
SetMap(map[string]any{
"UpdateAt": info.UpdateAt,
"DeleteAt": info.DeleteAt,
"Path": info.Path,
"ThumbnailPath": info.ThumbnailPath,
"PreviewPath": info.PreviewPath,
"Name": info.Name,
"Extension": info.Extension,
"Size": info.Size,
"MimeType": info.MimeType,
"Width": info.Width,
"Height": info.Height,
"HasPreviewImage": info.HasPreviewImage,
"MiniPreview": info.MiniPreview,
"Content": info.Content,
"RemoteId": info.RemoteId,
}).
Where(sq.Eq{"Id": info.Id}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "file_info_tosql")
}
sqlResult, err := fs.GetMasterX().Exec(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to update FileInfo")
}
count, err := sqlResult.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "unable to retrieve rows affected")
}
if count == 0 {
return fs.Save(info)
}
return info, nil
}
func (fs SqlFileInfoStore) get(id string, fromMaster bool) (*model.FileInfo, error) {
info := &model.FileInfo{}
query := fs.getQueryBuilder().
Select(fs.queryFields...).
From("FileInfo").
Where(sq.Eq{"Id": id}).
Where(sq.Eq{"DeleteAt": 0})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "file_info_tosql")
}
db := fs.GetReplicaX()
if fromMaster {
db = fs.GetMasterX()
}
if err := db.Get(info, queryString, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("FileInfo", id)
}
return nil, errors.Wrapf(err, "failed to get FileInfo with id=%s", id)
}
return info, nil
}
func (fs SqlFileInfoStore) Get(id string) (*model.FileInfo, error) {
return fs.get(id, false)
}
func (fs SqlFileInfoStore) GetFromMaster(id string) (*model.FileInfo, error) {
return fs.get(id, true)
}
func (fs SqlFileInfoStore) GetWithOptions(page, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, error) {
if perPage < 0 {
return nil, store.NewErrLimitExceeded("perPage", perPage, "value used in pagination while getting FileInfos")
} else if page < 0 {
return nil, store.NewErrLimitExceeded("page", page, "value used in pagination while getting FileInfos")
}
if perPage == 0 {
return nil, nil
}
if opt == nil {
opt = &model.GetFileInfosOptions{}
}
query := fs.getQueryBuilder().
Select(fs.queryFields...).
From("FileInfo")
if len(opt.ChannelIds) > 0 {
query = query.Where(sq.Eq{"FileInfo.ChannelId": opt.ChannelIds})
}
if len(opt.UserIds) > 0 {
query = query.Where(sq.Eq{"FileInfo.CreatorId": opt.UserIds})
}
if opt.Since > 0 {
query = query.Where(sq.GtOrEq{"FileInfo.CreateAt": opt.Since})
}
if !opt.IncludeDeleted {
query = query.Where("FileInfo.DeleteAt = 0")
}
if opt.SortBy == "" {
opt.SortBy = model.FileinfoSortByCreated
}
sortDirection := "ASC"
if opt.SortDescending {
sortDirection = "DESC"
}
switch opt.SortBy {
case model.FileinfoSortByCreated:
query = query.OrderBy("FileInfo.CreateAt " + sortDirection)
case model.FileinfoSortBySize:
query = query.OrderBy("FileInfo.Size " + sortDirection)
default:
return nil, store.NewErrInvalidInput("FileInfo", "<sortOption>", opt.SortBy)
}
query = query.OrderBy("FileInfo.Id ASC") // secondary sort for sort stability
query = query.Limit(uint64(perPage)).Offset(uint64(perPage * page))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "file_info_tosql")
}
infos := []*model.FileInfo{}
if err := fs.GetReplicaX().Select(&infos, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find FileInfos")
}
return infos, nil
}
func (fs SqlFileInfoStore) GetByPath(path string) (*model.FileInfo, error) {
info := &model.FileInfo{}
query := fs.getQueryBuilder().
Select(fs.queryFields...).
From("FileInfo").
Where(sq.Eq{"Path": path}).
Where(sq.Eq{"DeleteAt": 0}).
Limit(1)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "file_info_tosql")
}
if err := fs.GetReplicaX().Get(info, queryString, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("FileInfo", fmt.Sprintf("path=%s", path))
}
return nil, errors.Wrapf(err, "failed to get FileInfo with path=%s", path)
}
return info, nil
}
func (fs SqlFileInfoStore) InvalidateFileInfosForPostCache(postId string, deleted bool) {
}
func (fs SqlFileInfoStore) GetForPost(postId string, readFromMaster, includeDeleted, allowFromCache bool) ([]*model.FileInfo, error) {
infos := []*model.FileInfo{}
dbmap := fs.GetReplicaX()
if readFromMaster {
dbmap = fs.GetMasterX()
}
query := fs.getQueryBuilder().
Select(fs.queryFields...).
From("FileInfo").
Where(sq.Eq{"PostId": postId}).
OrderBy("CreateAt")
if !includeDeleted {
query = query.Where("DeleteAt = 0")
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "file_info_tosql")
}
if err := dbmap.Select(&infos, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find FileInfos with postId=%s", postId)
}
return infos, nil
}
func (fs SqlFileInfoStore) GetForUser(userId string) ([]*model.FileInfo, error) {
infos := []*model.FileInfo{}
query := fs.getQueryBuilder().
Select(fs.queryFields...).
From("FileInfo").
Where(sq.Eq{"CreatorId": userId}).
Where(sq.Eq{"DeleteAt": 0}).
OrderBy("CreateAt")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "file_info_tosql")
}
if err := fs.GetReplicaX().Select(&infos, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find FileInfos with creatorId=%s", userId)
}
return infos, nil
}
func (fs SqlFileInfoStore) AttachToPost(fileId, postId, channelId, creatorId string) error {
query := fs.getQueryBuilder().
Update("FileInfo").
Set("PostId", postId).
Set("ChannelId", channelId).
Where(sq.And{
sq.Eq{"Id": fileId},
sq.Eq{"PostId": ""},
sq.Or{
sq.Eq{"CreatorId": creatorId},
sq.Eq{"CreatorId": "nouser"},
},
})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "file_info_tosql")
}
sqlResult, err := fs.GetMasterX().Exec(queryString, args...)
if err != nil {
return errors.Wrapf(err, "failed to update FileInfo with id=%s and postId=%s", fileId, postId)
}
count, err := sqlResult.RowsAffected()
if err != nil {
// RowsAffected should never fail with the MySQL or Postgres drivers
return errors.Wrap(err, "unable to retrieve rows affected")
} else if count == 0 {
// Could not attach the file to the post
return store.NewErrInvalidInput("FileInfo", "<id, postId, creatorId>", fmt.Sprintf("<%s, %s, %s>", fileId, postId, creatorId))
}
return nil
}
func (fs SqlFileInfoStore) SetContent(fileId, content string) error {
query := fs.getQueryBuilder().
Update("FileInfo").
Set("Content", content).
Where(sq.Eq{"Id": fileId})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "file_info_tosql")
}
_, err = fs.GetMasterX().Exec(queryString, args...)
if err != nil {
return errors.Wrapf(err, "failed to update FileInfo content with id=%s", fileId)
}
return nil
}
func (fs SqlFileInfoStore) DeleteForPost(postId string) (string, error) {
if _, err := fs.GetMasterX().Exec(
`UPDATE
FileInfo
SET
DeleteAt = ?
WHERE
PostId = ?`, model.GetMillis(), postId); err != nil {
return "", errors.Wrapf(err, "failed to update FileInfo with postId=%s", postId)
}
return postId, nil
}
func (fs SqlFileInfoStore) PermanentDelete(fileId string) error {
if _, err := fs.GetMasterX().Exec(`DELETE FROM FileInfo WHERE Id = ?`, fileId); err != nil {
return errors.Wrapf(err, "failed to delete FileInfo with id=%s", fileId)
}
return nil
}
func (fs SqlFileInfoStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
var query string
if fs.DriverName() == "postgres" {
query = "DELETE from FileInfo WHERE Id = any (array (SELECT Id FROM FileInfo WHERE CreateAt < ? LIMIT ?))"
} else {
query = "DELETE from FileInfo WHERE CreateAt < ? LIMIT ?"
}
sqlResult, err := fs.GetMasterX().Exec(query, endTime, limit)
if err != nil {
return 0, errors.Wrap(err, "failed to delete FileInfos in batch")
}
rowsAffected, err := sqlResult.RowsAffected()
if err != nil {
return 0, errors.Wrapf(err, "unable to retrieve rows affected")
}
return rowsAffected, nil
}
func (fs SqlFileInfoStore) PermanentDeleteByUser(userId string) (int64, error) {
query := "DELETE from FileInfo WHERE CreatorId = ?"
sqlResult, err := fs.GetMasterX().Exec(query, userId)
if err != nil {
return 0, errors.Wrapf(err, "failed to delete FileInfo with creatorId=%s", userId)
}
rowsAffected, err := sqlResult.RowsAffected()
if err != nil {
return 0, errors.Wrapf(err, "unable to retrieve rows affected")
}
return rowsAffected, nil
}
func (fs SqlFileInfoStore) Search(paramsList []*model.SearchParams, userId, teamId string, page, perPage int) (*model.FileInfoList, error) {
// Since we don't support paging for DB search, we just return nothing for later pages
if page > 0 {
return model.NewFileInfoList(), nil
}
if err := model.IsSearchParamsListValid(paramsList); err != nil {
return nil, err
}
query := fs.getQueryBuilder().
Select(fs.queryFields...).
From("FileInfo").
LeftJoin("Channels as C ON C.Id=FileInfo.ChannelId").
LeftJoin("ChannelMembers as CM ON C.Id=CM.ChannelId").
Where(sq.Eq{"FileInfo.DeleteAt": 0}).
OrderBy("FileInfo.CreateAt DESC").
Limit(100)
if teamId != "" {
query = query.Where(sq.Or{
sq.Eq{"C.TeamId": teamId},
sq.Eq{"C.TeamId": ""},
})
}
now := model.GetMillis()
for _, params := range paramsList {
if params.Modifier == model.ModifierFiles {
// Deliberately keeping non-alphanumeric characters to
// prevent surprises in UI.
buf, err := json.Marshal(params)
if err != nil {
return nil, err
}
err = fs.stores.post.LogRecentSearch(userId, buf, now)
if err != nil {
return nil, err
}
}
params.Terms = removeNonAlphaNumericUnquotedTerms(params.Terms, " ")
if !params.IncludeDeletedChannels {
query = query.Where(sq.Eq{"C.DeleteAt": 0})
}
if !params.SearchWithoutUserId {
query = query.Where(sq.Eq{"CM.UserId": userId})
}
if len(params.InChannels) != 0 {
query = query.Where(sq.Eq{"C.Id": params.InChannels})
}
if len(params.Extensions) != 0 {
query = query.Where(sq.Eq{"FileInfo.Extension": params.Extensions})
}
if len(params.ExcludedExtensions) != 0 {
query = query.Where(sq.NotEq{"FileInfo.Extension": params.ExcludedExtensions})
}
if len(params.ExcludedChannels) != 0 {
query = query.Where(sq.NotEq{"C.Id": params.ExcludedChannels})
}
if len(params.FromUsers) != 0 {
query = query.Where(sq.Eq{"FileInfo.CreatorId": params.FromUsers})
}
if len(params.ExcludedUsers) != 0 {
query = query.Where(sq.NotEq{"FileInfo.CreatorId": params.ExcludedUsers})
}
// handle after: before: on: filters
if params.OnDate != "" {
onDateStart, onDateEnd := params.GetOnDateMillis()
query = query.Where(sq.Expr("FileInfo.CreateAt BETWEEN ? AND ?", strconv.FormatInt(onDateStart, 10), strconv.FormatInt(onDateEnd, 10)))
} else {
if params.ExcludedDate != "" {
excludedDateStart, excludedDateEnd := params.GetExcludedDateMillis()
query = query.Where(sq.Expr("FileInfo.CreateAt NOT BETWEEN ? AND ?", strconv.FormatInt(excludedDateStart, 10), strconv.FormatInt(excludedDateEnd, 10)))
}
if params.AfterDate != "" {
afterDate := params.GetAfterDateMillis()
query = query.Where(sq.GtOrEq{"FileInfo.CreateAt": strconv.FormatInt(afterDate, 10)})
}
if params.BeforeDate != "" {
beforeDate := params.GetBeforeDateMillis()
query = query.Where(sq.LtOrEq{"FileInfo.CreateAt": strconv.FormatInt(beforeDate, 10)})
}
if params.ExcludedAfterDate != "" {
afterDate := params.GetExcludedAfterDateMillis()
query = query.Where(sq.Lt{"FileInfo.CreateAt": strconv.FormatInt(afterDate, 10)})
}
if params.ExcludedBeforeDate != "" {
beforeDate := params.GetExcludedBeforeDateMillis()
query = query.Where(sq.Gt{"FileInfo.CreateAt": strconv.FormatInt(beforeDate, 10)})
}
}
terms := params.Terms
excludedTerms := params.ExcludedTerms
for _, c := range fs.specialSearchChars() {
terms = strings.Replace(terms, c, " ", -1)
excludedTerms = strings.Replace(excludedTerms, c, " ", -1)
}
if terms == "" && excludedTerms == "" {
// we've already confirmed that we have a channel or user to search for
} else if fs.DriverName() == model.DatabaseDriverPostgres {
// Parse text for wildcards
if wildcard, err := regexp.Compile(`\*($| )`); err == nil {
terms = wildcard.ReplaceAllLiteralString(terms, ":* ")
excludedTerms = wildcard.ReplaceAllLiteralString(excludedTerms, ":* ")
}
excludeClause := ""
if excludedTerms != "" {
excludeClause = " & !(" + strings.Join(strings.Fields(excludedTerms), " | ") + ")"
}
queryTerms := ""
if params.OrTerms {
queryTerms = "(" + strings.Join(strings.Fields(terms), " | ") + ")" + excludeClause
} else {
queryTerms = "(" + strings.Join(strings.Fields(terms), " & ") + ")" + excludeClause
}
query = query.Where(sq.Or{
sq.Expr(fmt.Sprintf("to_tsvector('%[1]s', FileInfo.Name) @@ to_tsquery('%[1]s', ?)", fs.pgDefaultTextSearchConfig), queryTerms),
sq.Expr(fmt.Sprintf("to_tsvector('%[1]s', Translate(FileInfo.Name, '.,-', ' ')) @@ to_tsquery('%[1]s', ?)", fs.pgDefaultTextSearchConfig), queryTerms),
sq.Expr(fmt.Sprintf("to_tsvector('%[1]s', FileInfo.Content) @@ to_tsquery('%[1]s', ?)", fs.pgDefaultTextSearchConfig), queryTerms),
})
} else if fs.DriverName() == model.DatabaseDriverMysql {
var err error
terms, err = removeMysqlStopWordsFromTerms(terms)
if err != nil {
return nil, errors.Wrap(err, "failed to remove Mysql stop-words from terms")
}
if terms == "" {
return model.NewFileInfoList(), nil
}
excludeClause := ""
if excludedTerms != "" {
excludeClause = " -(" + excludedTerms + ")"
}
queryTerms := ""
if params.OrTerms {
queryTerms = terms + excludeClause
} else {
splitTerms := []string{}
for _, t := range strings.Fields(terms) {
splitTerms = append(splitTerms, "+"+t)
}
queryTerms = strings.Join(splitTerms, " ") + excludeClause
}
query = query.Where(sq.Or{
sq.Expr("MATCH (FileInfo.Name) AGAINST (? IN BOOLEAN MODE)", queryTerms),
sq.Expr("MATCH (FileInfo.Content) AGAINST (? IN BOOLEAN MODE)", queryTerms),
})
}
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "file_info_tosql")
}
list := model.NewFileInfoList()
items := []fileInfoWithChannelID{}
err = fs.GetSearchReplicaX().Select(&items, queryString, args...)
if err != nil {
mlog.Warn("Query error searching files.", mlog.Err(err))
// Don't return the error to the caller as it is of no use to the user. Instead return an empty set of search results.
} else {
for _, item := range items {
info := item.ToModel()
list.AddFileInfo(info)
list.AddOrder(info.Id)
}
}
list.MakeNonNil()
return list, nil
}
func (fs SqlFileInfoStore) CountAll() (int64, error) {
query := fs.getQueryBuilder().
Select("COUNT(*)").
From("FileInfo").
Where("DeleteAt = 0")
queryString, args, err := query.ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "count_tosql")
}
var count int64
err = fs.GetReplicaX().Get(&count, queryString, args...)
if err != nil {
return int64(0), errors.Wrap(err, "failed to count Files")
}
return count, nil
}
func (fs SqlFileInfoStore) GetFilesBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.FileForIndexing, error) {
files := []*model.FileForIndexing{}
sql, args, _ := fs.getQueryBuilder().
Select(fs.queryFields...).
From("FileInfo").
Where(sq.Or{
sq.Gt{"FileInfo.CreateAt": startTime},
sq.And{
sq.Eq{"FileInfo.CreateAt": startTime},
sq.Gt{"FileInfo.Id": startFileID},
},
}).
OrderBy("FileInfo.CreateAt ASC, FileInfo.Id ASC").
Limit(uint64(limit)).
ToSql()
err := fs.GetSearchReplicaX().Select(&files, sql, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Files")
}
return files, nil
}
func (fs SqlFileInfoStore) GetStorageUsage(allowFromCache, includeDeleted bool) (int64, error) {
query := fs.getQueryBuilder().
Select("COALESCE(SUM(Size), 0)").
From("FileInfo")
if !includeDeleted {
query = query.Where("DeleteAt = 0")
}
var size int64
err := fs.GetReplicaX().GetBuilder(&size, query)
if err != nil {
return int64(0), errors.Wrap(err, "failed to get storage usage")
}
return size, nil
}
// GetUptoNSizeFileTime returns the CreateAt time of the last accessible file with a running-total size upto n bytes.
func (fs *SqlFileInfoStore) GetUptoNSizeFileTime(n int64) (int64, error) {
if n <= 0 {
return 0, errors.New("n can't be less than 1")
}
var sizeSubQuery sq.SelectBuilder
// Separate query for MySql, as current min-version 5.x doesn't support window-functions
if fs.DriverName() == model.DatabaseDriverMysql {
sizeSubQuery = sq.
Select("(@runningSum := @runningSum + fi.Size) RunningTotal", "fi.CreateAt").
From("FileInfo fi").
Join("(SELECT @runningSum := 0) as tmp").
Where(sq.Eq{"fi.DeleteAt": 0}).
OrderBy("fi.CreateAt DESC, fi.Id")
} else {
sizeSubQuery = sq.
Select("SUM(fi.Size) OVER(ORDER BY CreateAt DESC, fi.Id) RunningTotal", "fi.CreateAt").
From("FileInfo fi").
Where(sq.Eq{"fi.DeleteAt": 0})
}
builder := fs.getQueryBuilder().
Select("fi2.CreateAt").
FromSelect(sizeSubQuery, "fi2").
Where(sq.LtOrEq{"fi2.RunningTotal": n}).
OrderBy("fi2.CreateAt").
Limit(1)
query, queryArgs, err := builder.ToSql()
if err != nil {
return 0, errors.Wrap(err, "GetUptoNSizeFileTime_tosql")
}
var createAt int64
if err := fs.GetReplicaX().Get(&createAt, query, queryArgs...); err != nil {
if err == sql.ErrNoRows {
return 0, store.NewErrNotFound("File", "none")
}
return 0, errors.Wrapf(err, "failed to get the File for size upto=%d", n)
}
return createAt, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"strings"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type selectType int
const (
selectGroups selectType = iota
selectCountGroups
)
type groupTeam struct {
model.GroupSyncable
TeamId string
}
type groupChannel struct {
model.GroupSyncable
ChannelId string
}
type groupTeamJoin struct {
groupTeam
TeamDisplayName string
TeamType string
}
type groupChannelJoin struct {
groupChannel
ChannelDisplayName string
TeamDisplayName string
TeamType string
ChannelType string
TeamId string
}
type SqlGroupStore struct {
*SqlStore
}
func newSqlGroupStore(sqlStore *SqlStore) store.GroupStore {
return &SqlGroupStore{SqlStore: sqlStore}
}
func (s *SqlGroupStore) Create(group *model.Group) (*model.Group, error) {
if group.Id != "" {
return nil, store.NewErrInvalidInput("Group", "id", group.Id)
}
if err := group.IsValidForCreate(); err != nil {
return nil, err
}
group.Id = model.NewId()
group.CreateAt = model.GetMillis()
group.UpdateAt = group.CreateAt
if _, err := s.GetMasterX().NamedExec(`INSERT INTO UserGroups
(Id, Name, DisplayName, Description, Source, RemoteId, CreateAt, UpdateAt, DeleteAt, AllowReference)
VALUES
(:Id, :Name, :DisplayName, :Description, :Source, :RemoteId, :CreateAt, :UpdateAt, :DeleteAt, :AllowReference)`, group); err != nil {
if IsUniqueConstraintError(err, []string{"Name", "groups_name_key"}) {
return nil, errors.Wrapf(err, "Group with name %s already exists", *group.Name)
}
return nil, errors.Wrap(err, "failed to save Group")
}
return group, nil
}
func (s *SqlGroupStore) CreateWithUserIds(g *model.GroupWithUserIds) (_ *model.Group, err error) {
if g.Id != "" {
return nil, store.NewErrInvalidInput("Group", "id", g.Id)
}
// Check if group values are formatted correctly
if appErr := g.IsValidForCreate(); appErr != nil {
return nil, appErr
}
// Check Users exist
if err = s.checkUsersExist(g.UserIds); err != nil {
return nil, err
}
g.Id = model.NewId()
g.CreateAt = model.GetMillis()
g.UpdateAt = g.CreateAt
groupInsertQuery, groupInsertArgs, err := s.getQueryBuilder().
Insert("UserGroups").
Columns("Id", "Name", "DisplayName", "Description", "Source", "RemoteId", "CreateAt", "UpdateAt", "DeleteAt", "AllowReference").
Values(g.Id, g.Name, g.DisplayName, g.Description, g.Source, g.RemoteId, g.CreateAt, g.UpdateAt, 0, g.AllowReference).
ToSql()
if err != nil {
return nil, err
}
usersInsertQuery, usersInsertArgs, err := s.buildInsertGroupUsersQuery(g.Id, g.UserIds)
if err != nil {
return nil, err
}
txn, err := s.GetMasterX().Beginx()
if err != nil {
return nil, err
}
defer finalizeTransactionX(txn, &err)
// Create a new usergroup
if _, err = txn.Exec(groupInsertQuery, groupInsertArgs...); err != nil {
if IsUniqueConstraintError(err, []string{"Name", "groups_name_key"}) {
return nil, store.NewErrUniqueConstraint("Name")
}
return nil, errors.Wrap(err, "failed to save Group")
}
// Insert the Group Members
if _, err = executePossiblyEmptyQuery(txn, usersInsertQuery, usersInsertArgs...); err != nil {
return nil, err
}
// Get the new Group along with the member count
groupGroupQuery := `
SELECT
UserGroups.*,
A.Count AS MemberCount
FROM
UserGroups
INNER JOIN (
SELECT
UserGroups.Id,
COUNT(GroupMembers.UserId) AS Count
FROM
UserGroups
LEFT JOIN GroupMembers ON UserGroups.Id = GroupMembers.GroupId
WHERE
UserGroups.Id = ?
GROUP BY
UserGroups.Id
ORDER BY
UserGroups.DisplayName,
UserGroups.Id
LIMIT
? OFFSET ?
) AS A ON UserGroups.Id = A.Id
ORDER BY
UserGroups.CreateAt DESC`
var newGroup group
if err = txn.Get(&newGroup, groupGroupQuery, g.Id, 1, 0); err != nil {
return nil, err
}
if err = txn.Commit(); err != nil {
return nil, err
}
return newGroup.ToModel(), nil
}
func (s *SqlGroupStore) checkUsersExist(userIDs []string) error {
if len(userIDs) == 0 {
return nil
}
usersSelectQuery, usersSelectArgs, err := s.getQueryBuilder().
Select("Id").
From("Users").
Where(sq.Eq{"Id": userIDs, "DeleteAt": 0}).
ToSql()
if err != nil {
return err
}
var rows []string
err = s.GetReplicaX().Select(&rows, usersSelectQuery, usersSelectArgs...)
if err != nil {
return err
}
if len(rows) == len(userIDs) {
return nil
}
retrievedIDs := make(map[string]bool)
for _, userID := range rows {
retrievedIDs[userID] = true
}
for _, userID := range userIDs {
if _, ok := retrievedIDs[userID]; !ok {
return store.NewErrNotFound("User", userID)
}
}
return nil
}
func (s *SqlGroupStore) buildInsertGroupUsersQuery(groupId string, userIds []string) (query string, args []any, err error) {
if len(userIds) > 0 {
builder := s.getQueryBuilder().
Insert("GroupMembers").
Columns("GroupId", "UserId", "CreateAt", "DeleteAt")
for _, userId := range userIds {
builder = builder.Values(groupId, userId, model.GetMillis(), 0)
}
query, args, err = builder.ToSql()
}
return
}
func (s *SqlGroupStore) Get(groupId string) (*model.Group, error) {
var group model.Group
if err := s.GetReplicaX().Get(&group, "SELECT * from UserGroups WHERE Id = ?", groupId); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Group", groupId)
}
return nil, errors.Wrapf(err, "failed to get Group with id=%s", groupId)
}
return &group, nil
}
func (s *SqlGroupStore) GetByName(name string, opts model.GroupSearchOpts) (*model.Group, error) {
var group model.Group
query := s.getQueryBuilder().Select("*").From("UserGroups").Where(sq.Eq{"Name": name})
if opts.FilterAllowReference {
query = query.Where("AllowReference = true")
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_by_name_tosql")
}
if err := s.GetReplicaX().Get(&group, queryString, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Group", fmt.Sprintf("name=%s", name))
}
return nil, errors.Wrapf(err, "failed to get Group with name=%s", name)
}
return &group, nil
}
func (s *SqlGroupStore) GetByIDs(groupIDs []string) ([]*model.Group, error) {
groups := []*model.Group{}
query := s.getQueryBuilder().Select("*").From("UserGroups").Where(sq.Eq{"Id": groupIDs})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_by_ids_tosql")
}
if err := s.GetReplicaX().Select(&groups, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Groups by ids")
}
return groups, nil
}
func (s *SqlGroupStore) GetByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, error) {
var group model.Group
if err := s.GetReplicaX().Get(&group, "SELECT * from UserGroups WHERE RemoteId = ? AND Source = ?", remoteID, groupSource); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Group", fmt.Sprintf("remoteId=%s", remoteID))
}
return nil, errors.Wrapf(err, "failed to get Group with remoteId=%s", remoteID)
}
return &group, nil
}
func (s *SqlGroupStore) GetAllBySource(groupSource model.GroupSource) ([]*model.Group, error) {
groups := []*model.Group{}
if err := s.GetReplicaX().Select(&groups, "SELECT * from UserGroups WHERE DeleteAt = 0 AND Source = ?", groupSource); err != nil {
return nil, errors.Wrapf(err, "failed to find Groups by groupSource=%v", groupSource)
}
return groups, nil
}
func (s *SqlGroupStore) GetByUser(userId string) ([]*model.Group, error) {
groups := []*model.Group{}
query := `
SELECT
UserGroups.*
FROM
GroupMembers
JOIN UserGroups ON UserGroups.Id = GroupMembers.GroupId
WHERE
GroupMembers.DeleteAt = 0
AND UserId = ?`
if err := s.GetReplicaX().Select(&groups, query, userId); err != nil {
return nil, errors.Wrapf(err, "failed to find Groups with userId=%s", userId)
}
return groups, nil
}
func (s *SqlGroupStore) Update(group *model.Group) (*model.Group, error) {
var retrievedGroup model.Group
if err := s.GetReplicaX().Get(&retrievedGroup, "SELECT * FROM UserGroups WHERE Id = ?", group.Id); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Group", group.Id)
}
return nil, errors.Wrapf(err, "failed to get Group with id=%s", group.Id)
}
// If updating DeleteAt it can only be to 0
if group.DeleteAt != retrievedGroup.DeleteAt && group.DeleteAt != 0 {
return nil, errors.New("DeleteAt should be 0 when updating")
}
// Reset these properties, don't update them based on input
group.CreateAt = retrievedGroup.CreateAt
group.UpdateAt = model.GetMillis()
if err := group.IsValidForUpdate(); err != nil {
return nil, err
}
res, err := s.GetMasterX().NamedExec(`UPDATE UserGroups
SET Name=:Name, DisplayName=:DisplayName, Description=:Description, Source=:Source,
RemoteId=:RemoteId, CreateAt=:CreateAt, UpdateAt=:UpdateAt, DeleteAt=:DeleteAt, AllowReference=:AllowReference
WHERE Id=:Id`, group)
if err != nil {
if IsUniqueConstraintError(err, []string{"Name", "groups_name_key"}) {
return nil, store.NewErrUniqueConstraint("Name")
}
return nil, errors.Wrap(err, "failed to update Group")
}
rowsChanged, _ := res.RowsAffected()
if rowsChanged > 1 {
return nil, errors.Wrapf(err, "multiple Groups were update: %d", rowsChanged)
}
return group, nil
}
func (s *SqlGroupStore) Delete(groupID string) (*model.Group, error) {
var group model.Group
if err := s.GetReplicaX().Get(&group, "SELECT * from UserGroups WHERE Id = ? AND DeleteAt = 0", groupID); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Group", groupID)
}
return nil, errors.Wrapf(err, "failed to get Group with id=%s", groupID)
}
time := model.GetMillis()
if _, err := s.GetMasterX().Exec(`UPDATE UserGroups
SET DeleteAt=?, UpdateAt=?
WHERE Id=? AND DeleteAt=0`, time, time, groupID); err != nil {
return nil, errors.Wrapf(err, "failed to update Group with id=%s", groupID)
}
return &group, nil
}
func (s *SqlGroupStore) Restore(groupID string) (*model.Group, error) {
var group model.Group
if err := s.GetReplicaX().Get(&group, "SELECT * from UserGroups WHERE Id = ? AND DeleteAt != 0", groupID); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Group", groupID)
}
return nil, errors.Wrapf(err, "failed to get Group with id=%s", groupID)
}
time := model.GetMillis()
if _, err := s.GetMasterX().Exec(`UPDATE UserGroups
SET DeleteAt=0, UpdateAt=?
WHERE Id=? AND DeleteAt!=0`, time, groupID); err != nil {
return nil, errors.Wrapf(err, "failed to update Group with id=%s", groupID)
}
return &group, nil
}
func (s *SqlGroupStore) GetMember(groupID, userID string) (*model.GroupMember, error) {
query, args, err := s.getQueryBuilder().
Select("*").
From("GroupMembers").
Where(sq.Eq{"UserId": userID}).
Where(sq.Eq{"GroupId": groupID}).
Where(sq.Eq{"DeleteAt": 0}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_member_query")
}
var groupMember model.GroupMember
err = s.GetReplicaX().Get(&groupMember, query, args...)
if err != nil {
return nil, errors.Wrap(err, "GetMember")
}
return &groupMember, nil
}
func (s *SqlGroupStore) GetMemberUsers(groupID string) ([]*model.User, error) {
groupMembers := []*model.User{}
query := `
SELECT
Users.*
FROM
GroupMembers
JOIN Users ON Users.Id = GroupMembers.UserId
WHERE
GroupMembers.DeleteAt = 0
AND Users.DeleteAt = 0
AND GroupId = ?`
if err := s.GetReplicaX().Select(&groupMembers, query, groupID); err != nil {
return nil, errors.Wrapf(err, "failed to find member Users for Group with id=%s", groupID)
}
return groupMembers, nil
}
func (s *SqlGroupStore) GetMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
return s.GetMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, model.ShowUsername)
}
func (s *SqlGroupStore) GetMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, error) {
groupMembers := []*model.User{}
userQuery := s.getQueryBuilder().
Select(`u.*`).
From("GroupMembers").
Join("Users u ON u.Id = GroupMembers.UserId").
Where(sq.Eq{"GroupMembers.DeleteAt": 0}).
Where(sq.Eq{"u.DeleteAt": 0}).
Where(sq.Eq{"GroupId": groupID})
userQuery = applyViewRestrictionsFilter(userQuery, viewRestrictions, true)
queryString, args, err := userQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "")
}
orderQuery := s.getQueryBuilder().
Select("u.*").
From("(" + queryString + ") AS u")
if teammateNameDisplay == model.ShowNicknameFullName {
orderQuery = orderQuery.OrderBy(`
CASE
WHEN u.Nickname != '' THEN u.Nickname
WHEN u.FirstName != '' AND u.LastName != '' THEN CONCAT(u.FirstName, ' ', u.LastName)
WHEN u.FirstName != '' THEN u.FirstName
WHEN u.LastName != '' THEN u.LastName
ELSE u.Username
END`)
} else if teammateNameDisplay == model.ShowFullName {
orderQuery = orderQuery.OrderBy(`
CASE
WHEN u.FirstName != '' AND u.LastName != '' THEN CONCAT(u.FirstName, ' ', u.LastName)
WHEN u.FirstName != '' THEN u.FirstName
WHEN u.LastName != '' THEN u.LastName
ELSE u.Username
END`)
} else {
orderQuery = orderQuery.OrderBy("u.Username")
}
orderQuery = orderQuery.
Limit(uint64(perPage)).
Offset(uint64(page * perPage))
queryString, _, err = orderQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "")
}
if err := s.GetReplicaX().Select(&groupMembers, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find member Users for Group with id=%s", groupID)
}
return groupMembers, nil
}
func (s *SqlGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
groupMembers := []*model.User{}
if err := s.GetReplicaX().Get(&model.Group{}, "SELECT * FROM UserGroups WHERE Id = ?", groupID); err != nil {
return nil, errors.Wrap(err, "GetNonMemberUsersPage")
}
query := s.getQueryBuilder().
Select("u.*").
From("Users u").
LeftJoin("GroupMembers ON (GroupMembers.UserId = u.Id AND GroupMembers.GroupId = ?)", groupID).
Where(sq.Eq{"u.DeleteAt": 0}).
Where("(GroupMembers.UserID IS NULL OR GroupMembers.DeleteAt != 0)").
Limit(uint64(perPage)).
Offset(uint64(page * perPage)).
OrderBy("u.Username ASC")
query = applyViewRestrictionsFilter(query, viewRestrictions, true)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "")
}
if err := s.GetReplicaX().Select(&groupMembers, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find member Users for Group with id=%s", groupID)
}
return groupMembers, nil
}
func (s *SqlGroupStore) GetMemberCount(groupID string) (int64, error) {
return s.GetMemberCountWithRestrictions(groupID, nil)
}
func (s *SqlGroupStore) GetMemberCountWithRestrictions(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, error) {
query := s.getQueryBuilder().
Select("COUNT(DISTINCT u.Id)").
From("GroupMembers").
Join("Users u ON u.Id = GroupMembers.UserId").
Where(sq.Eq{"GroupMembers.GroupId": groupID}).
Where(sq.Eq{"u.DeleteAt": 0}).
Where(sq.Eq{"GroupMembers.DeleteAt": 0})
query = applyViewRestrictionsFilter(query, viewRestrictions, false)
queryString, args, err := query.ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "")
}
var count int64
err = s.GetReplicaX().Get(&count, queryString, args...)
if err != nil {
return int64(0), errors.Wrapf(err, "failed to count member Users for Group with id=%s", groupID)
}
return count, nil
}
func (s *SqlGroupStore) GetMemberUsersInTeam(groupID string, teamID string) ([]*model.User, error) {
groupMembers := []*model.User{}
query := `
SELECT
Users.*
FROM
GroupMembers
JOIN Users ON Users.Id = GroupMembers.UserId
WHERE
GroupId = ?
AND GroupMembers.UserId IN (
SELECT TeamMembers.UserId
FROM TeamMembers
JOIN Teams ON Teams.Id = ?
WHERE TeamMembers.TeamId = Teams.Id
AND TeamMembers.DeleteAt = 0
)
AND GroupMembers.DeleteAt = 0
AND Users.DeleteAt = 0
`
if err := s.GetReplicaX().Select(&groupMembers, query, groupID, teamID); err != nil {
return nil, errors.Wrapf(err, "failed to member Users for groupId=%s and teamId=%s", groupID, teamID)
}
return groupMembers, nil
}
func (s *SqlGroupStore) GetMemberUsersNotInChannel(groupID string, channelID string) ([]*model.User, error) {
groupMembers := []*model.User{}
query := `
SELECT
Users.*
FROM
GroupMembers
JOIN Users ON Users.Id = GroupMembers.UserId
WHERE
GroupId = ?
AND GroupMembers.UserId NOT IN (
SELECT ChannelMembers.UserId
FROM ChannelMembers
WHERE ChannelMembers.ChannelId = ?
)
AND GroupMembers.UserId IN (
SELECT TeamMembers.UserId
FROM TeamMembers
JOIN Channels ON Channels.Id = ?
JOIN Teams ON Teams.Id = Channels.TeamId
WHERE TeamMembers.TeamId = Teams.Id
AND TeamMembers.DeleteAt = 0
)
AND GroupMembers.DeleteAt = 0
AND Users.DeleteAt = 0
`
if err := s.GetReplicaX().Select(&groupMembers, query, groupID, channelID, channelID); err != nil {
return nil, errors.Wrapf(err, "failed to member Users for groupId=%s and channelId!=%s", groupID, channelID)
}
return groupMembers, nil
}
func (s *SqlGroupStore) UpsertMember(groupID string, userID string) (*model.GroupMember, error) {
members, query, args, err := s.buildUpsertMembersQuery(groupID, []string{userID})
if err != nil {
return nil, err
}
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrap(err, "failed to save GroupMember")
}
return members[0], nil
}
func (s *SqlGroupStore) DeleteMember(groupID string, userID string) (*model.GroupMember, error) {
members, query, args, err := s.buildDeleteMembersQuery(groupID, []string{userID})
if err != nil {
return nil, err
}
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to update GroupMember with groupId=%s and userId=%s", groupID, userID)
}
return members[0], nil
}
func (s *SqlGroupStore) PermanentDeleteMembersByUser(userId string) error {
if _, err := s.GetMasterX().Exec("DELETE FROM GroupMembers WHERE UserId = ?", userId); err != nil {
return errors.Wrapf(err, "failed to permanent delete GroupMember with userId=%s", userId)
}
return nil
}
func (s *SqlGroupStore) CreateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
if err := groupSyncable.IsValid(); err != nil {
return nil, err
}
// Reset values that shouldn't be updatable by parameter
groupSyncable.DeleteAt = 0
groupSyncable.CreateAt = model.GetMillis()
groupSyncable.UpdateAt = groupSyncable.CreateAt
var insertErr error
switch groupSyncable.Type {
case model.GroupSyncableTypeTeam:
if _, err := s.Team().Get(groupSyncable.SyncableId); err != nil {
return nil, err
}
_, insertErr = s.GetMasterX().NamedExec(`INSERT INTO GroupTeams
(GroupId, AutoAdd, SchemeAdmin, CreateAt, DeleteAt, UpdateAt, TeamId)
VALUES
(:GroupId, :AutoAdd, :SchemeAdmin, :CreateAt, :DeleteAt, :UpdateAt, :TeamId)`, groupSyncableToGroupTeam(groupSyncable))
case model.GroupSyncableTypeChannel:
var channel *model.Channel
channel, err := s.Channel().Get(groupSyncable.SyncableId, false)
if err != nil {
return nil, err
}
_, insertErr = s.GetMasterX().NamedExec(`INSERT INTO GroupChannels
(GroupId, AutoAdd, SchemeAdmin, CreateAt, DeleteAt, UpdateAt, ChannelId)
VALUES
(:GroupId, :AutoAdd, :SchemeAdmin, :CreateAt, :DeleteAt, :UpdateAt, :ChannelId)`, groupSyncableToGroupChannel(groupSyncable))
groupSyncable.TeamID = channel.TeamId
default:
return nil, fmt.Errorf("invalid GroupSyncableType: %s", groupSyncable.Type)
}
if insertErr != nil {
return nil, errors.Wrap(insertErr, "unable to insert GroupSyncable")
}
return groupSyncable, nil
}
func (s *SqlGroupStore) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
groupSyncable, err := s.getGroupSyncable(groupID, syncableID, syncableType)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("GroupSyncable", fmt.Sprintf("groupId=%s, syncableId=%s, syncableType=%s", groupID, syncableID, syncableType))
}
return nil, errors.Wrapf(err, "failed to find GroupSyncable with groupId=%s, syncableId=%s, syncableType=%s", groupID, syncableID, syncableType)
}
return groupSyncable, nil
}
func (s *SqlGroupStore) getGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
var err error
var result any
switch syncableType {
case model.GroupSyncableTypeTeam:
var team groupTeam
err = s.GetReplicaX().Get(&team, `SELECT * FROM GroupTeams WHERE GroupId=? AND TeamId=?`, groupID, syncableID)
result = &team
case model.GroupSyncableTypeChannel:
var ch groupChannel
err = s.GetReplicaX().Get(&ch, `SELECT * FROM GroupChannels WHERE GroupId=? AND ChannelId=?`, groupID, syncableID)
result = &ch
}
if err != nil {
return nil, err
}
if result == nil {
return nil, sql.ErrNoRows
}
groupSyncable := model.GroupSyncable{}
switch syncableType {
case model.GroupSyncableTypeTeam:
groupTeam := result.(*groupTeam)
groupSyncable.SyncableId = groupTeam.TeamId
groupSyncable.GroupId = groupTeam.GroupId
groupSyncable.AutoAdd = groupTeam.AutoAdd
groupSyncable.CreateAt = groupTeam.CreateAt
groupSyncable.DeleteAt = groupTeam.DeleteAt
groupSyncable.UpdateAt = groupTeam.UpdateAt
groupSyncable.Type = syncableType
case model.GroupSyncableTypeChannel:
groupChannel := result.(*groupChannel)
groupSyncable.SyncableId = groupChannel.ChannelId
groupSyncable.GroupId = groupChannel.GroupId
groupSyncable.AutoAdd = groupChannel.AutoAdd
groupSyncable.CreateAt = groupChannel.CreateAt
groupSyncable.DeleteAt = groupChannel.DeleteAt
groupSyncable.UpdateAt = groupChannel.UpdateAt
groupSyncable.Type = syncableType
default:
return nil, fmt.Errorf("unable to convert syncableType: %s", syncableType.String())
}
return &groupSyncable, nil
}
func (s *SqlGroupStore) GetAllGroupSyncablesByGroupId(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, error) {
groupSyncables := []*model.GroupSyncable{}
switch syncableType {
case model.GroupSyncableTypeTeam:
sqlQuery := `
SELECT
GroupTeams.*,
Teams.DisplayName AS TeamDisplayName,
Teams.Type AS TeamType
FROM
GroupTeams
JOIN Teams ON Teams.Id = GroupTeams.TeamId
WHERE
GroupId = ? AND GroupTeams.DeleteAt = 0`
results := []*groupTeamJoin{}
err := s.GetReplicaX().Select(&results, sqlQuery, groupID)
if err != nil {
return nil, errors.Wrapf(err, "failed to find GroupTeams with groupId=%s", groupID)
}
for _, result := range results {
groupSyncable := &model.GroupSyncable{
SyncableId: result.TeamId,
GroupId: result.GroupId,
AutoAdd: result.AutoAdd,
CreateAt: result.CreateAt,
DeleteAt: result.DeleteAt,
UpdateAt: result.UpdateAt,
Type: syncableType,
TeamDisplayName: result.TeamDisplayName,
TeamType: result.TeamType,
SchemeAdmin: result.SchemeAdmin,
}
groupSyncables = append(groupSyncables, groupSyncable)
}
case model.GroupSyncableTypeChannel:
sqlQuery := `
SELECT
GroupChannels.*,
Channels.DisplayName AS ChannelDisplayName,
Teams.DisplayName AS TeamDisplayName,
Channels.Type As ChannelType,
Teams.Type As TeamType,
Teams.Id AS TeamId
FROM
GroupChannels
JOIN Channels ON Channels.Id = GroupChannels.ChannelId
JOIN Teams ON Teams.Id = Channels.TeamId
WHERE
GroupId = ? AND GroupChannels.DeleteAt = 0`
results := []*groupChannelJoin{}
err := s.GetReplicaX().Select(&results, sqlQuery, groupID)
if err != nil {
return nil, errors.Wrapf(err, "failed to find GroupChannels with groupId=%s", groupID)
}
for _, result := range results {
groupSyncable := &model.GroupSyncable{
SyncableId: result.ChannelId,
GroupId: result.GroupId,
AutoAdd: result.AutoAdd,
CreateAt: result.CreateAt,
DeleteAt: result.DeleteAt,
UpdateAt: result.UpdateAt,
Type: syncableType,
ChannelDisplayName: result.ChannelDisplayName,
ChannelType: result.ChannelType,
TeamDisplayName: result.TeamDisplayName,
TeamType: result.TeamType,
TeamID: result.TeamID,
SchemeAdmin: result.SchemeAdmin,
}
groupSyncables = append(groupSyncables, groupSyncable)
}
}
return groupSyncables, nil
}
func (s *SqlGroupStore) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
retrievedGroupSyncable, err := s.getGroupSyncable(groupSyncable.GroupId, groupSyncable.SyncableId, groupSyncable.Type)
if err != nil {
if err == sql.ErrNoRows {
return nil, errors.Wrap(store.NewErrNotFound("GroupSyncable", fmt.Sprintf("groupId=%s, syncableId=%s, syncableType=%s", groupSyncable.GroupId, groupSyncable.SyncableId, groupSyncable.Type)), "GroupSyncable not found")
}
return nil, errors.Wrapf(err, "failed to find GroupSyncable with groupId=%s, syncableId=%s, syncableType=%s", groupSyncable.GroupId, groupSyncable.SyncableId, groupSyncable.Type)
}
if err := groupSyncable.IsValid(); err != nil {
return nil, err
}
// If updating DeleteAt it can only be to 0
if groupSyncable.DeleteAt != retrievedGroupSyncable.DeleteAt && groupSyncable.DeleteAt != 0 {
return nil, errors.New("DeleteAt should be 0 when updating")
}
// Reset these properties, don't update them based on input
groupSyncable.CreateAt = retrievedGroupSyncable.CreateAt
groupSyncable.UpdateAt = model.GetMillis()
switch groupSyncable.Type {
case model.GroupSyncableTypeTeam:
_, err = s.GetMasterX().NamedExec(`UPDATE GroupTeams
SET AutoAdd=:AutoAdd, SchemeAdmin=:SchemeAdmin, CreateAt=:CreateAt,
DeleteAt=:DeleteAt, UpdateAt=:UpdateAt
WHERE GroupId=:GroupId AND TeamId=:TeamId`, groupSyncableToGroupTeam(groupSyncable))
case model.GroupSyncableTypeChannel:
// We need to get the TeamId so redux can manage channels when teams are unlinked
var channel *model.Channel
channel, channelErr := s.Channel().Get(groupSyncable.SyncableId, false)
if channelErr != nil {
return nil, channelErr
}
_, err = s.GetMasterX().NamedExec(`UPDATE GroupChannels
SET AutoAdd=:AutoAdd, SchemeAdmin=:SchemeAdmin, CreateAt=:CreateAt,
DeleteAt=:DeleteAt, UpdateAt=:UpdateAt
WHERE GroupId=:GroupId AND ChannelId=:ChannelId`, groupSyncableToGroupChannel(groupSyncable))
groupSyncable.TeamID = channel.TeamId
default:
return nil, fmt.Errorf("invalid GroupSyncableType: %s", groupSyncable.Type)
}
if err != nil {
return nil, errors.Wrap(err, "failed to update GroupSyncable")
}
return groupSyncable, nil
}
func (s *SqlGroupStore) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
groupSyncable, err := s.getGroupSyncable(groupID, syncableID, syncableType)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("GroupSyncable", fmt.Sprintf("groupId=%s, syncableId=%s, syncableType=%s", groupID, syncableID, syncableType))
}
return nil, errors.Wrapf(err, "failed to find GroupSyncable with groupId=%s, syncableId=%s, syncableType=%s", groupID, syncableID, syncableType)
}
if groupSyncable.DeleteAt != 0 {
return nil, store.NewErrInvalidInput("GroupSyncable", "<groupId, syncableId, syncableType>", fmt.Sprintf("<%s, %s, %s>", groupSyncable.GroupId, groupSyncable.SyncableId, groupSyncable.Type))
}
time := model.GetMillis()
groupSyncable.DeleteAt = time
groupSyncable.UpdateAt = time
switch groupSyncable.Type {
case model.GroupSyncableTypeTeam:
_, err = s.GetMasterX().NamedExec(`UPDATE GroupTeams
SET AutoAdd=:AutoAdd, SchemeAdmin=:SchemeAdmin, CreateAt=:CreateAt,
DeleteAt=:DeleteAt, UpdateAt=:UpdateAt
WHERE GroupId=:GroupId AND TeamId=:TeamId`, groupSyncableToGroupTeam(groupSyncable))
case model.GroupSyncableTypeChannel:
_, err = s.GetMasterX().NamedExec(`UPDATE GroupChannels
SET AutoAdd=:AutoAdd, SchemeAdmin=:SchemeAdmin, CreateAt=:CreateAt,
DeleteAt=:DeleteAt, UpdateAt=:UpdateAt
WHERE GroupId=:GroupId AND ChannelId=:ChannelId`, groupSyncableToGroupChannel(groupSyncable))
default:
return nil, fmt.Errorf("invalid GroupSyncableType: %s", groupSyncable.Type)
}
if err != nil {
return nil, errors.Wrap(err, "failed to update GroupSyncable")
}
return groupSyncable, nil
}
func (s *SqlGroupStore) TeamMembersToAdd(since int64, teamID *string, includeRemovedMembers bool) ([]*model.UserTeamIDPair, error) {
builder := s.getQueryBuilder().Select("GroupMembers.UserId UserID", "GroupTeams.TeamId TeamID").
From("GroupMembers").
Join("GroupTeams ON GroupTeams.GroupId = GroupMembers.GroupId").
Join("UserGroups ON UserGroups.Id = GroupMembers.GroupId").
Join("Teams ON Teams.Id = GroupTeams.TeamId").
Where(sq.Eq{
"UserGroups.DeleteAt": 0,
"GroupTeams.DeleteAt": 0,
"GroupTeams.AutoAdd": true,
"GroupMembers.DeleteAt": 0,
"Teams.DeleteAt": 0,
})
if !includeRemovedMembers {
builder = builder.
JoinClause("LEFT OUTER JOIN TeamMembers ON TeamMembers.TeamId = GroupTeams.TeamId AND TeamMembers.UserId = GroupMembers.UserId").
Where(sq.Eq{"TeamMembers.UserId": nil}).
Where(sq.Or{
sq.GtOrEq{"GroupMembers.CreateAt": since},
sq.GtOrEq{"GroupTeams.UpdateAt": since},
})
}
if teamID != nil {
builder = builder.Where(sq.Eq{"Teams.Id": *teamID})
}
query, params, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_members_to_add_tosql")
}
teamMembers := []*model.UserTeamIDPair{}
err = s.GetMasterX().Select(&teamMembers, query, params...)
if err != nil {
return nil, errors.Wrap(err, "failed to find UserTeamIDPairs")
}
return teamMembers, nil
}
func (s *SqlGroupStore) ChannelMembersToAdd(since int64, channelID *string, includeRemovedMembers bool) ([]*model.UserChannelIDPair, error) {
builder := s.getQueryBuilder().Select("GroupMembers.UserId UserID", "GroupChannels.ChannelId ChannelID").
From("GroupMembers").
Join("GroupChannels ON GroupChannels.GroupId = GroupMembers.GroupId").
Join("UserGroups ON UserGroups.Id = GroupMembers.GroupId").
Join("Channels ON Channels.Id = GroupChannels.ChannelId").
Where(sq.Eq{
"UserGroups.DeleteAt": 0,
"GroupChannels.DeleteAt": 0,
"GroupChannels.AutoAdd": true,
"GroupMembers.DeleteAt": 0,
"Channels.DeleteAt": 0,
})
if !includeRemovedMembers {
builder = builder.
JoinClause("LEFT OUTER JOIN ChannelMemberHistory ON ChannelMemberHistory.ChannelId = GroupChannels.ChannelId AND ChannelMemberHistory.UserId = GroupMembers.UserId").
Where(sq.Eq{
"ChannelMemberHistory.UserId": nil,
"ChannelMemberHistory.LeaveTime": nil,
}).
Where(sq.Or{
sq.GtOrEq{"GroupMembers.CreateAt": since},
sq.GtOrEq{"GroupChannels.UpdateAt": since},
})
}
if channelID != nil {
builder = builder.Where(sq.Eq{"Channels.Id": *channelID})
}
query, params, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_members_to_add_tosql")
}
channelMembers := []*model.UserChannelIDPair{}
err = s.GetMasterX().Select(&channelMembers, query, params...)
if err != nil {
return nil, errors.Wrap(err, "failed to find UserChannelIDPairs")
}
return channelMembers, nil
}
func groupSyncableToGroupTeam(groupSyncable *model.GroupSyncable) *groupTeam {
return &groupTeam{
GroupSyncable: *groupSyncable,
TeamId: groupSyncable.SyncableId,
}
}
func groupSyncableToGroupChannel(groupSyncable *model.GroupSyncable) *groupChannel {
return &groupChannel{
GroupSyncable: *groupSyncable,
ChannelId: groupSyncable.SyncableId,
}
}
func (s *SqlGroupStore) TeamMembersToRemove(teamID *string) ([]*model.TeamMember, error) {
whereStmt := `
(TeamMembers.TeamId,
TeamMembers.UserId)
NOT IN (
SELECT
Teams.Id AS TeamId,
GroupMembers.UserId
FROM
Teams
JOIN GroupTeams ON GroupTeams.TeamId = Teams.Id
JOIN UserGroups ON UserGroups.Id = GroupTeams.GroupId
JOIN GroupMembers ON GroupMembers.GroupId = UserGroups.Id
WHERE
Teams.GroupConstrained = TRUE
AND GroupTeams.DeleteAt = 0
AND UserGroups.DeleteAt = 0
AND Teams.DeleteAt = 0
AND GroupMembers.DeleteAt = 0
GROUP BY
Teams.Id,
GroupMembers.UserId)`
builder := s.getQueryBuilder().Select(
"TeamMembers.TeamId",
"TeamMembers.UserId",
"TeamMembers.Roles",
"TeamMembers.DeleteAt",
"TeamMembers.SchemeUser",
"TeamMembers.SchemeAdmin",
"(TeamMembers.SchemeGuest IS NOT NULL AND TeamMembers.SchemeGuest) AS SchemeGuest",
).
From("TeamMembers").
Join("Teams ON Teams.Id = TeamMembers.TeamId").
LeftJoin("Bots ON Bots.UserId = TeamMembers.UserId").
Where(sq.Eq{"TeamMembers.DeleteAt": 0, "Teams.DeleteAt": 0, "Teams.GroupConstrained": true, "Bots.UserId": nil}).
Where(whereStmt)
if teamID != nil {
builder = builder.Where(sq.Eq{"TeamMembers.TeamId": *teamID})
}
query, params, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_members_to_remove_tosql")
}
teamMembers := []*model.TeamMember{}
err = s.GetReplicaX().Select(&teamMembers, query, params...)
if err != nil {
return nil, errors.Wrap(err, "failed to find TeamMembers")
}
return teamMembers, nil
}
func (s *SqlGroupStore) CountGroupsByChannel(channelId string, opts model.GroupSearchOpts) (int64, error) {
countQuery := s.groupsBySyncableBaseQuery(model.GroupSyncableTypeChannel, selectCountGroups, channelId, opts)
countQueryString, args, err := countQuery.ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "count_groups_by_channel_tosql")
}
var count int64
err = s.GetReplicaX().Get(&count, countQueryString, args...)
if err != nil {
return int64(0), errors.Wrapf(err, "failed to count Groups by channel with channelId=%s", channelId)
}
return count, nil
}
type group struct {
Id string
Name *string
DisplayName string
Description string
Source model.GroupSource
RemoteId *string
CreateAt int64
UpdateAt int64
DeleteAt int64
HasSyncables bool
MemberCount *int
AllowReference bool
ChannelMemberCount *int
ChannelMemberTimezonesCount *int
}
func (g group) ToModel() *model.Group {
return &model.Group{
Id: g.Id,
Name: g.Name,
DisplayName: g.DisplayName,
Description: g.Description,
Source: g.Source,
RemoteId: g.RemoteId,
CreateAt: g.CreateAt,
UpdateAt: g.UpdateAt,
DeleteAt: g.DeleteAt,
HasSyncables: g.HasSyncables,
AllowReference: g.AllowReference,
MemberCount: g.MemberCount,
ChannelMemberCount: g.ChannelMemberCount,
ChannelMemberTimezonesCount: g.ChannelMemberTimezonesCount,
}
}
type groups []*group
func (groups groups) ToModel() []*model.Group {
res := make([]*model.Group, 0, len(groups))
for _, g := range groups {
res = append(res, g.ToModel())
}
return res
}
type groupWithSchemeAdmin struct {
group
SyncableSchemeAdmin *bool
}
func (g groupWithSchemeAdmin) ToModel() *model.GroupWithSchemeAdmin {
if g.SyncableSchemeAdmin == nil {
g.SyncableSchemeAdmin = model.NewBool(false)
}
res := &model.GroupWithSchemeAdmin{
Group: *g.group.ToModel(),
SchemeAdmin: g.SyncableSchemeAdmin,
}
return res
}
type groupsWithSchemeAdmin []*groupWithSchemeAdmin
func (groups groupsWithSchemeAdmin) ToModel() []*model.GroupWithSchemeAdmin {
res := make([]*model.GroupWithSchemeAdmin, 0, len(groups))
for _, g := range groups {
res = append(res, g.ToModel())
}
return res
}
type groupAssociatedToChannelWithSchemeAdmin struct {
groupWithSchemeAdmin
ChannelId string
}
func (g groupAssociatedToChannelWithSchemeAdmin) ToModel() *model.GroupsAssociatedToChannelWithSchemeAdmin {
withSchemeAdmin := g.groupWithSchemeAdmin.ToModel()
return &model.GroupsAssociatedToChannelWithSchemeAdmin{
ChannelId: g.ChannelId,
SchemeAdmin: withSchemeAdmin.SchemeAdmin,
Group: withSchemeAdmin.Group,
}
}
type groupsAssociatedToChannelWithSchemeAdmin []groupAssociatedToChannelWithSchemeAdmin
func (groups groupsAssociatedToChannelWithSchemeAdmin) ToModel() []*model.GroupsAssociatedToChannelWithSchemeAdmin {
res := make([]*model.GroupsAssociatedToChannelWithSchemeAdmin, 0, len(groups))
for _, g := range groups {
res = append(res, g.ToModel())
}
return res
}
func (s *SqlGroupStore) GetGroupsByChannel(channelId string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
query := s.groupsBySyncableBaseQuery(model.GroupSyncableTypeChannel, selectGroups, channelId, opts)
if opts.PageOpts != nil {
offset := uint64(opts.PageOpts.Page * opts.PageOpts.PerPage)
query = query.OrderBy("ug.DisplayName").Limit(uint64(opts.PageOpts.PerPage)).Offset(offset)
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_groups_by_channel_tosql")
}
groups := groupsWithSchemeAdmin{}
err = s.GetReplicaX().Select(&groups, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Groups with channelId=%s", channelId)
}
return groups.ToModel(), nil
}
func (s *SqlGroupStore) ChannelMembersToRemove(channelID *string) ([]*model.ChannelMember, error) {
whereStmt := `
(ChannelMembers.ChannelId,
ChannelMembers.UserId)
NOT IN (
SELECT
Channels.Id AS ChannelId,
GroupMembers.UserId
FROM
Channels
JOIN GroupChannels ON GroupChannels.ChannelId = Channels.Id
JOIN UserGroups ON UserGroups.Id = GroupChannels.GroupId
JOIN GroupMembers ON GroupMembers.GroupId = UserGroups.Id
WHERE
Channels.GroupConstrained = TRUE
AND GroupChannels.DeleteAt = 0
AND UserGroups.DeleteAt = 0
AND Channels.DeleteAt = 0
AND GroupMembers.DeleteAt = 0
GROUP BY
Channels.Id,
GroupMembers.UserId)`
builder := s.getQueryBuilder().Select(
"ChannelMembers.ChannelId",
"ChannelMembers.UserId",
"ChannelMembers.LastViewedAt",
"ChannelMembers.MsgCount",
"ChannelMembers.MsgCountRoot",
"ChannelMembers.MentionCount",
"ChannelMembers.MentionCountRoot",
"ChannelMembers.NotifyProps",
"ChannelMembers.LastUpdateAt",
"ChannelMembers.LastUpdateAt",
"ChannelMembers.SchemeUser",
"ChannelMembers.SchemeAdmin",
"(ChannelMembers.SchemeGuest IS NOT NULL AND ChannelMembers.SchemeGuest) AS SchemeGuest",
).
From("ChannelMembers").
Join("Channels ON Channels.Id = ChannelMembers.ChannelId").
LeftJoin("Bots ON Bots.UserId = ChannelMembers.UserId").
Where(sq.Eq{"Channels.DeleteAt": 0, "Channels.GroupConstrained": true, "Bots.UserId": nil}).
Where(whereStmt)
if channelID != nil {
builder = builder.Where(sq.Eq{"ChannelMembers.ChannelId": *channelID})
}
query, params, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_members_to_remove_tosql")
}
channelMembers := []*model.ChannelMember{}
err = s.GetReplicaX().Select(&channelMembers, query, params...)
if err != nil {
return nil, errors.Wrap(err, "failed to find ChannelMembers")
}
return channelMembers, nil
}
func (s *SqlGroupStore) groupsBySyncableBaseQuery(st model.GroupSyncableType, t selectType, syncableID string, opts model.GroupSearchOpts) sq.SelectBuilder {
selectStrs := map[selectType]string{
selectGroups: "ug.*, gs.SchemeAdmin AS SyncableSchemeAdmin",
selectCountGroups: "COUNT(*)",
}
var table string
var idCol string
if st == model.GroupSyncableTypeTeam {
table = "GroupTeams"
idCol = "TeamId"
} else {
table = "GroupChannels"
idCol = "ChannelId"
}
query := s.getQueryBuilder().
Select(selectStrs[t]).
From(fmt.Sprintf("%s gs", table)).
LeftJoin("UserGroups ug ON gs.GroupId = ug.Id").
Where(fmt.Sprintf("ug.DeleteAt = 0 AND gs.%s = ? AND gs.DeleteAt = 0", idCol), syncableID)
if opts.IncludeMemberCount && t == selectGroups {
query = s.getQueryBuilder().
Select(fmt.Sprintf("ug.*, coalesce(Members.MemberCount, 0) AS MemberCount, Group%ss.SchemeAdmin AS SyncableSchemeAdmin", st)).
From("UserGroups ug").
LeftJoin("(SELECT GroupMembers.GroupId, COUNT(*) AS MemberCount FROM GroupMembers LEFT JOIN Users ON Users.Id = GroupMembers.UserId WHERE GroupMembers.DeleteAt = 0 AND Users.DeleteAt = 0 GROUP BY GroupId) AS Members ON Members.GroupId = ug.Id").
LeftJoin(fmt.Sprintf("%[1]s ON %[1]s.GroupId = ug.Id", table)).
Where(fmt.Sprintf("ug.DeleteAt = 0 AND %[1]s.DeleteAt = 0 AND %[1]s.%[2]s = ?", table, idCol), syncableID).
OrderBy("ug.DisplayName")
}
if opts.FilterAllowReference && t == selectGroups {
query = query.Where("ug.AllowReference = true")
}
if opts.Q != "" {
pattern := fmt.Sprintf("%%%s%%", sanitizeSearchTerm(opts.Q, "\\"))
operatorKeyword := "ILIKE"
if s.DriverName() == model.DatabaseDriverMysql {
operatorKeyword = "LIKE"
}
query = query.Where(fmt.Sprintf("(ug.Name %[1]s ? OR ug.DisplayName %[1]s ?)", operatorKeyword), pattern, pattern)
}
return query
}
func (s *SqlGroupStore) getGroupsAssociatedToChannelsByTeam(teamID string, opts model.GroupSearchOpts) sq.SelectBuilder {
query := s.getQueryBuilder().
Select("gc.ChannelId, ug.*, gc.SchemeAdmin AS SyncableSchemeAdmin").
From("UserGroups ug").
LeftJoin(`
(SELECT
GroupChannels.GroupId, GroupChannels.ChannelId, GroupChannels.DeleteAt, GroupChannels.SchemeAdmin
FROM
GroupChannels
LEFT JOIN
Channels ON (Channels.Id = GroupChannels.ChannelId)
WHERE
GroupChannels.DeleteAt = 0
AND Channels.DeleteAt = 0
AND Channels.TeamId = ?) AS gc ON gc.GroupId = ug.Id`, teamID).
Where("ug.DeleteAt = 0 AND gc.DeleteAt = 0").
OrderBy("ug.DisplayName")
if opts.IncludeMemberCount {
query = s.getQueryBuilder().
Select("gc.ChannelId, ug.*, coalesce(Members.MemberCount, 0) AS MemberCount, gc.SchemeAdmin AS SyncableSchemeAdmin").
From("UserGroups ug").
LeftJoin(`
(SELECT
GroupChannels.ChannelId, GroupChannels.DeleteAt, GroupChannels.GroupId, GroupChannels.SchemeAdmin
FROM
GroupChannels
LEFT JOIN
Channels ON (Channels.Id = GroupChannels.ChannelId)
WHERE
GroupChannels.DeleteAt = 0
AND Channels.DeleteAt = 0
AND Channels.TeamId = ?) AS gc ON gc.GroupId = ug.Id`, teamID).
LeftJoin(`(
SELECT
GroupMembers.GroupId, COUNT(*) AS MemberCount
FROM
GroupMembers
LEFT JOIN
Users ON Users.Id = GroupMembers.UserId
WHERE
GroupMembers.DeleteAt = 0
AND Users.DeleteAt = 0
GROUP BY GroupId) AS Members
ON Members.GroupId = ug.Id`).
Where("ug.DeleteAt = 0 AND gc.DeleteAt = 0").
OrderBy("ug.DisplayName")
}
if opts.FilterAllowReference {
query = query.Where("ug.AllowReference = true")
}
if opts.Q != "" {
pattern := fmt.Sprintf("%%%s%%", sanitizeSearchTerm(opts.Q, "\\"))
operatorKeyword := "ILIKE"
if s.DriverName() == model.DatabaseDriverMysql {
operatorKeyword = "LIKE"
}
query = query.Where(fmt.Sprintf("(ug.Name %[1]s ? OR ug.DisplayName %[1]s ?)", operatorKeyword), pattern, pattern)
}
return query
}
func (s *SqlGroupStore) CountGroupsByTeam(teamId string, opts model.GroupSearchOpts) (int64, error) {
countQuery := s.groupsBySyncableBaseQuery(model.GroupSyncableTypeTeam, selectCountGroups, teamId, opts)
countQueryString, args, err := countQuery.ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "count_groups_by_team_tosql")
}
var count int64
err = s.GetReplicaX().Get(&count, countQueryString, args...)
if err != nil {
return int64(0), errors.Wrapf(err, "failed to count Groups with teamId=%s", teamId)
}
return count, nil
}
func (s *SqlGroupStore) GetGroupsByTeam(teamId string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
query := s.groupsBySyncableBaseQuery(model.GroupSyncableTypeTeam, selectGroups, teamId, opts)
if opts.PageOpts != nil {
offset := uint64(opts.PageOpts.Page * opts.PageOpts.PerPage)
query = query.OrderBy("ug.DisplayName").Limit(uint64(opts.PageOpts.PerPage)).Offset(offset)
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_groups_by_team_tosql")
}
groups := groupsWithSchemeAdmin{}
err = s.GetReplicaX().Select(&groups, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Groups with teamId=%s", teamId)
}
return groups.ToModel(), nil
}
func (s *SqlGroupStore) GetGroupsAssociatedToChannelsByTeam(teamId string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, error) {
query := s.getGroupsAssociatedToChannelsByTeam(teamId, opts)
if opts.PageOpts != nil {
offset := uint64(opts.PageOpts.Page * opts.PageOpts.PerPage)
query = query.OrderBy("ug.DisplayName").Limit(uint64(opts.PageOpts.PerPage)).Offset(offset)
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_groups_associated_to_channel_by_team_tosql")
}
tgroups := groupsAssociatedToChannelWithSchemeAdmin{}
err = s.GetReplicaX().Select(&tgroups, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Groups with teamId=%s", teamId)
}
groups := map[string][]*model.GroupWithSchemeAdmin{}
for _, tgroup := range tgroups {
group := tgroup.groupWithSchemeAdmin.ToModel()
groups[tgroup.ChannelId] = append(groups[tgroup.ChannelId], group)
}
return groups, nil
}
func (s *SqlGroupStore) GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error) {
groupsVar := groups{}
selectQuery := []string{"g.*"}
if opts.IncludeMemberCount {
selectQuery = append(selectQuery, "coalesce(Members.MemberCount, 0) AS MemberCount")
}
if opts.IncludeChannelMemberCount != "" {
selectQuery = append(selectQuery, "coalesce(ChannelMembers.ChannelMemberCount, 0) AS ChannelMemberCount")
if opts.IncludeTimezones {
selectQuery = append(selectQuery, "coalesce(ChannelMembers.ChannelMemberTimezonesCount, 0) AS ChannelMemberTimezonesCount")
}
}
groupsQuery := s.getQueryBuilder().Select(strings.Join(selectQuery, ", "))
if opts.IncludeMemberCount {
countQuery := s.getQueryBuilder().
Select("GroupMembers.GroupId, COUNT(DISTINCT u.Id) AS MemberCount").
From("GroupMembers").
LeftJoin("Users u ON u.Id = GroupMembers.UserId").
Where(sq.Eq{"GroupMembers.DeleteAt": 0}).
Where(sq.Eq{"u.DeleteAt": 0}).
GroupBy("GroupId")
countQuery = applyViewRestrictionsFilter(countQuery, viewRestrictions, false)
countString, params, err := countQuery.PlaceholderFormat(sq.Question).ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_groups_tosql")
}
groupsQuery = groupsQuery.
LeftJoin("("+countString+") AS Members ON Members.GroupId = g.Id", params...)
}
if opts.IncludeChannelMemberCount != "" {
selectStr := "GroupMembers.GroupId, COUNT(ChannelMembers.UserId) AS ChannelMemberCount"
joinStr := ""
if opts.IncludeTimezones {
if s.DriverName() == model.DatabaseDriverMysql {
selectStr += `,
COUNT(DISTINCT
(
CASE WHEN JSON_EXTRACT(Timezone, '$.useAutomaticTimezone') = 'true' AND LENGTH(JSON_UNQUOTE(JSON_EXTRACT(Timezone, '$.automaticTimezone'))) > 0
THEN JSON_EXTRACT(Timezone, '$.automaticTimezone')
WHEN JSON_EXTRACT(Timezone, '$.useAutomaticTimezone') = 'false' AND LENGTH(JSON_UNQUOTE(JSON_EXTRACT(Timezone, '$.manualTimezone'))) > 0
THEN JSON_EXTRACT(Timezone, '$.manualTimezone')
END
)) AS ChannelMemberTimezonesCount`
} else if s.DriverName() == model.DatabaseDriverPostgres {
selectStr += `,
COUNT(DISTINCT
(
CASE WHEN Timezone->>'useAutomaticTimezone' = 'true' AND length(Timezone->>'automaticTimezone') > 0
THEN Timezone->>'automaticTimezone'
WHEN Timezone->>'useAutomaticTimezone' = 'false' AND length(Timezone->>'manualTimezone') > 0
THEN Timezone->>'manualTimezone'
END
)) AS ChannelMemberTimezonesCount`
}
joinStr = "LEFT JOIN Users ON Users.Id = GroupMembers.UserId"
}
groupsQuery = groupsQuery.
LeftJoin("(SELECT "+selectStr+" FROM ChannelMembers LEFT JOIN GroupMembers ON GroupMembers.UserId = ChannelMembers.UserId AND GroupMembers.DeleteAt = 0 "+joinStr+" WHERE ChannelMembers.ChannelId = ? GROUP BY GroupId) AS ChannelMembers ON ChannelMembers.GroupId = g.Id", opts.IncludeChannelMemberCount)
}
if opts.FilterHasMember != "" {
groupsQuery = groupsQuery.
LeftJoin("GroupMembers ON GroupMembers.GroupId = g.Id").
Where("GroupMembers.UserId = ?", opts.FilterHasMember).
Where("GroupMembers.DeleteAt = 0")
}
groupsQuery = groupsQuery.
From("UserGroups g").
OrderBy("g.DisplayName")
if opts.Since > 0 {
groupsQuery = groupsQuery.Where(sq.Gt{
"g.UpdateAt": opts.Since,
})
} else {
groupsQuery = groupsQuery.Where("g.DeleteAt = 0")
}
if perPage != 0 {
groupsQuery = groupsQuery.
Limit(uint64(perPage)).
Offset(uint64(page * perPage))
}
if opts.FilterAllowReference {
groupsQuery = groupsQuery.Where("g.AllowReference = true")
}
if opts.Q != "" {
pattern := fmt.Sprintf("%%%s%%", sanitizeSearchTerm(opts.Q, "\\"))
operatorKeyword := "ILIKE"
if s.DriverName() == model.DatabaseDriverMysql {
operatorKeyword = "LIKE"
}
groupsQuery = groupsQuery.Where(fmt.Sprintf("(g.Name %[1]s ? OR g.DisplayName %[1]s ?)", operatorKeyword), pattern, pattern)
}
if len(opts.NotAssociatedToTeam) == 26 {
groupsQuery = groupsQuery.Where(`
g.Id NOT IN (
SELECT
Id
FROM
UserGroups
JOIN GroupTeams ON GroupTeams.GroupId = UserGroups.Id
WHERE
GroupTeams.DeleteAt = 0
AND UserGroups.DeleteAt = 0
AND GroupTeams.TeamId = ?
)
`, opts.NotAssociatedToTeam)
}
if len(opts.NotAssociatedToChannel) == 26 {
groupsQuery = groupsQuery.Where(`
g.Id NOT IN (
SELECT
Id
FROM
UserGroups
JOIN GroupChannels ON GroupChannels.GroupId = UserGroups.Id
WHERE
GroupChannels.DeleteAt = 0
AND UserGroups.DeleteAt = 0
AND GroupChannels.ChannelId = ?
)
`, opts.NotAssociatedToChannel)
}
if opts.FilterParentTeamPermitted && len(opts.NotAssociatedToChannel) == 26 {
groupsQuery = groupsQuery.Where(`
CASE
WHEN (
SELECT
Teams.GroupConstrained
FROM
Teams
JOIN Channels ON Channels.TeamId = Teams.Id
WHERE
Channels.Id = ?
) THEN g.Id IN (
SELECT
GroupId
FROM
GroupTeams
WHERE
GroupTeams.DeleteAt = 0
AND GroupTeams.TeamId = (
SELECT
TeamId
FROM
Channels
WHERE
Id = ?
)
)
ELSE TRUE
END
`, opts.NotAssociatedToChannel, opts.NotAssociatedToChannel)
}
if opts.Source != "" {
groupsQuery = groupsQuery.Where("g.Source = ?", opts.Source)
}
queryString, args, err := groupsQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_groups_tosql")
}
if err = s.GetReplicaX().Select(&groupsVar, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Groups")
}
return groupsVar.ToModel(), nil
}
func (s *SqlGroupStore) teamMembersMinusGroupMembersQuery(teamID string, groupIDs []string, isCount bool) sq.SelectBuilder {
var selectStr string
if isCount {
selectStr = "count(DISTINCT Users.Id)"
} else {
tmpl := "Users.*, coalesce(TeamMembers.SchemeGuest, false) SchemeGuest, TeamMembers.SchemeAdmin, TeamMembers.SchemeUser, %s AS GroupIDs"
if s.DriverName() == model.DatabaseDriverMysql {
selectStr = fmt.Sprintf(tmpl, "group_concat(UserGroups.Id)")
} else {
selectStr = fmt.Sprintf(tmpl, "string_agg(UserGroups.Id, ',')")
}
}
subQuery := s.getQueryBuilder().Select("GroupMembers.UserId").
From("GroupMembers").
Join("UserGroups ON UserGroups.Id = GroupMembers.GroupId").
Where("GroupMembers.DeleteAt = 0").
Where(fmt.Sprintf("GroupMembers.GroupId IN ('%s')", strings.Join(groupIDs, "', '")))
query, _ := subQuery.MustSql()
builder := s.getQueryBuilder().Select(selectStr).
From("TeamMembers").
Join("Teams ON Teams.Id = TeamMembers.TeamId").
Join("Users ON Users.Id = TeamMembers.UserId").
LeftJoin("Bots ON Bots.UserId = TeamMembers.UserId").
LeftJoin("GroupMembers ON GroupMembers.UserId = Users.Id").
LeftJoin("UserGroups ON UserGroups.Id = GroupMembers.GroupId").
Where("TeamMembers.DeleteAt = 0").
Where("Teams.DeleteAt = 0").
Where("Users.DeleteAt = 0").
Where("Bots.UserId IS NULL").
Where("Teams.Id = ?", teamID).
Where(fmt.Sprintf("Users.Id NOT IN (%s)", query))
if !isCount {
builder = builder.GroupBy("Users.Id, TeamMembers.SchemeGuest, TeamMembers.SchemeAdmin, TeamMembers.SchemeUser")
}
return builder
}
// TeamMembersMinusGroupMembers returns the set of users on the given team minus the set of users in the given
// groups.
func (s *SqlGroupStore) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, error) {
query := s.teamMembersMinusGroupMembersQuery(teamID, groupIDs, false)
query = query.OrderBy("Users.Username ASC").Limit(uint64(perPage)).Offset(uint64(page * perPage))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_members_minus_group_members")
}
users := []*model.UserWithGroups{}
if err = s.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find UserWithGroups")
}
return users, nil
}
// CountTeamMembersMinusGroupMembers returns the count of the set of users on the given team minus the set of users
// in the given groups.
func (s *SqlGroupStore) CountTeamMembersMinusGroupMembers(teamID string, groupIDs []string) (int64, error) {
queryString, args, err := s.teamMembersMinusGroupMembersQuery(teamID, groupIDs, true).ToSql()
if err != nil {
return 0, errors.Wrap(err, "count_team_members_minus_group_members_tosql")
}
var count int64
if err := s.GetReplicaX().Get(&count, queryString, args...); err != nil {
return 0, errors.Wrap(err, "failed to count TeamMembers minus GroupMembers")
}
return count, nil
}
func (s *SqlGroupStore) channelMembersMinusGroupMembersQuery(channelID string, groupIDs []string, isCount bool) sq.SelectBuilder {
var selectStr string
if isCount {
selectStr = "count(DISTINCT Users.Id)"
} else {
tmpl := "Users.*, coalesce(ChannelMembers.SchemeGuest, false) SchemeGuest, ChannelMembers.SchemeAdmin, ChannelMembers.SchemeUser, %s AS GroupIDs"
if s.DriverName() == model.DatabaseDriverMysql {
selectStr = fmt.Sprintf(tmpl, "group_concat(UserGroups.Id)")
} else {
selectStr = fmt.Sprintf(tmpl, "string_agg(UserGroups.Id, ',')")
}
}
subQuery := s.getQueryBuilder().Select("GroupMembers.UserId").
From("GroupMembers").
Join("UserGroups ON UserGroups.Id = GroupMembers.GroupId").
Where("GroupMembers.DeleteAt = 0").
Where(fmt.Sprintf("GroupMembers.GroupId IN ('%s')", strings.Join(groupIDs, "', '")))
query, _ := subQuery.MustSql()
builder := s.getQueryBuilder().Select(selectStr).
From("ChannelMembers").
Join("Channels ON Channels.Id = ChannelMembers.ChannelId").
Join("Users ON Users.Id = ChannelMembers.UserId").
LeftJoin("Bots ON Bots.UserId = ChannelMembers.UserId").
LeftJoin("GroupMembers ON GroupMembers.UserId = Users.Id").
LeftJoin("UserGroups ON UserGroups.Id = GroupMembers.GroupId").
Where("Channels.DeleteAt = 0").
Where("Users.DeleteAt = 0").
Where("Bots.UserId IS NULL").
Where("Channels.Id = ?", channelID).
Where(fmt.Sprintf("Users.Id NOT IN (%s)", query))
if !isCount {
builder = builder.GroupBy("Users.Id, ChannelMembers.SchemeGuest, ChannelMembers.SchemeAdmin, ChannelMembers.SchemeUser")
}
return builder
}
// ChannelMembersMinusGroupMembers returns the set of users in the given channel minus the set of users in the given
// groups.
func (s *SqlGroupStore) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, error) {
query := s.channelMembersMinusGroupMembersQuery(channelID, groupIDs, false)
query = query.OrderBy("Users.Username ASC").Limit(uint64(perPage)).Offset(uint64(page * perPage))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_members_minus_group_members_tosql")
}
users := []*model.UserWithGroups{}
if err = s.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find UserWithGroups")
}
return users, nil
}
// CountChannelMembersMinusGroupMembers returns the count of the set of users in the given channel minus the set of users
// in the given groups.
func (s *SqlGroupStore) CountChannelMembersMinusGroupMembers(channelID string, groupIDs []string) (int64, error) {
queryString, args, err := s.channelMembersMinusGroupMembersQuery(channelID, groupIDs, true).ToSql()
if err != nil {
return 0, errors.Wrap(err, "count_channel_members_minus_group_members_tosql")
}
var count int64
if err := s.GetReplicaX().Get(&count, queryString, args...); err != nil {
return 0, errors.Wrap(err, "failed to count ChannelMembers")
}
return count, nil
}
func (s *SqlGroupStore) AdminRoleGroupsForSyncableMember(userID, syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
var groupIds []string
query := fmt.Sprintf(`
SELECT
GroupMembers.GroupId
FROM
GroupMembers
INNER JOIN
Group%[1]ss ON Group%[1]ss.GroupId = GroupMembers.GroupId
WHERE
GroupMembers.UserId = ?
AND GroupMembers.DeleteAt = 0
AND %[1]sId = ?
AND Group%[1]ss.DeleteAt = 0
AND Group%[1]ss.SchemeAdmin = TRUE`, syncableType)
err := s.GetReplicaX().Select(&groupIds, query, userID, syncableID)
if err != nil {
return nil, errors.Wrap(err, "failed to find Group ids")
}
return groupIds, nil
}
func (s *SqlGroupStore) PermittedSyncableAdmins(syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
builder := s.getQueryBuilder().Select("UserId").
From(fmt.Sprintf("Group%ss", syncableType)).
Join(fmt.Sprintf("GroupMembers ON GroupMembers.GroupId = Group%ss.GroupId AND Group%[1]ss.SchemeAdmin = TRUE AND GroupMembers.DeleteAt = 0", syncableType.String())).Where(fmt.Sprintf("Group%[1]ss.%[1]sId = ?", syncableType.String()), syncableID)
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "permitted_syncable_admins_tosql")
}
var userIDs []string
if err = s.GetMasterX().Select(&userIDs, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find User ids")
}
return userIDs, nil
}
func (s *SqlGroupStore) GroupCount() (int64, error) {
return s.countTable("UserGroups")
}
func (s *SqlGroupStore) GroupCountBySource(source model.GroupSource) (int64, error) {
return s.countTableWithSelectAndWhere("COUNT(*)", "UserGroups", sq.Eq{"Source": source, "DeleteAt": 0})
}
func (s *SqlGroupStore) GroupTeamCount() (int64, error) {
return s.countTable("GroupTeams")
}
func (s *SqlGroupStore) GroupChannelCount() (int64, error) {
return s.countTable("GroupChannels")
}
func (s *SqlGroupStore) GroupMemberCount() (int64, error) {
return s.countTable("GroupMembers")
}
func (s *SqlGroupStore) DistinctGroupMemberCount() (int64, error) {
return s.countTableWithSelectAndWhere("COUNT(DISTINCT UserId)", "GroupMembers", nil)
}
func (s *SqlGroupStore) DistinctGroupMemberCountForSource(source model.GroupSource) (int64, error) {
builder := s.getQueryBuilder().
Select("COUNT(DISTINCT GroupMembers.UserId)").
From("GroupMembers").
Join("UserGroups ON GroupMembers.GroupId = UserGroups.Id").
Where(sq.Eq{"UserGroups.Source": source, "GroupMembers.DeleteAt": 0})
query, args, err := builder.ToSql()
if err != nil {
return 0, errors.Wrap(err, "distinct_group_member_count_for_source_tosql")
}
var count int64
if err = s.GetReplicaX().Get(&count, query, args...); err != nil {
return 0, errors.Wrapf(err, "failed to select distinct groupmember count for source %q", source)
}
return count, nil
}
func (s *SqlGroupStore) GroupCountWithAllowReference() (int64, error) {
return s.countTableWithSelectAndWhere("COUNT(*)", "UserGroups", sq.Eq{"AllowReference": true, "DeleteAt": 0})
}
func (s *SqlGroupStore) countTable(tableName string) (int64, error) {
return s.countTableWithSelectAndWhere("COUNT(*)", tableName, nil)
}
func (s *SqlGroupStore) countTableWithSelectAndWhere(selectStr, tableName string, whereStmt map[string]any) (int64, error) {
if whereStmt == nil {
whereStmt = sq.Eq{"DeleteAt": 0}
}
query := s.getQueryBuilder().Select(selectStr).From(tableName).Where(whereStmt)
sql, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "count_table_with_select_and_where_tosql")
}
var count int64
err = s.GetReplicaX().Get(&count, sql, args...)
if err != nil {
return 0, errors.Wrapf(err, "failed to count from table %s", tableName)
}
return count, nil
}
func (s *SqlGroupStore) UpsertMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
members, query, args, err := s.buildUpsertMembersQuery(groupID, userIDs)
if err != nil {
return nil, err
}
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrap(err, "failed to save GroupMember")
}
return members, err
}
func (s *SqlGroupStore) buildUpsertMembersQuery(groupID string, userIDs []string) (members []*model.GroupMember, query string, args []any, err error) {
var retrievedGroup model.Group
// Check Group exists
if err = s.GetReplicaX().Get(&retrievedGroup, "SELECT * FROM UserGroups WHERE Id = ?", groupID); err != nil {
err = errors.Wrapf(err, "failed to get UserGroup with groupId=%s", groupID)
return
}
// Check Users exist
if err = s.checkUsersExist(userIDs); err != nil {
return
}
builder := s.getQueryBuilder().
Insert("GroupMembers").
Columns("GroupId", "UserId", "CreateAt", "DeleteAt")
members = make([]*model.GroupMember, 0, len(userIDs))
createAt := model.GetMillis()
for _, userId := range userIDs {
member := &model.GroupMember{
GroupId: groupID,
UserId: userId,
CreateAt: createAt,
DeleteAt: 0,
}
builder = builder.Values(member.GroupId, member.UserId, member.CreateAt, member.DeleteAt)
members = append(members, member)
}
if s.DriverName() == model.DatabaseDriverMysql {
builder = builder.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE CreateAt = ?, DeleteAt = ?", createAt, 0))
} else if s.DriverName() == model.DatabaseDriverPostgres {
builder = builder.SuffixExpr(sq.Expr("ON CONFLICT (groupid, userid) DO UPDATE SET CreateAt = ?, DeleteAt = ?", createAt, 0))
}
query, args, err = builder.ToSql()
return
}
func (s *SqlGroupStore) DeleteMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
members, query, args, err := s.buildDeleteMembersQuery(groupID, userIDs)
if err != nil {
return nil, err
}
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrap(err, "failed to delete GroupMembers")
}
return members, err
}
func (s *SqlGroupStore) buildDeleteMembersQuery(groupID string, userIDs []string) (members []*model.GroupMember, query string, args []any, err error) {
membersSelectQuery, membersSelectArgs, err := s.getQueryBuilder().
Select("*").
From("GroupMembers").
Where(sq.And{
sq.Eq{"GroupId": groupID},
sq.Eq{"UserId": userIDs},
sq.Eq{"DeleteAt": 0},
}).
ToSql()
if err != nil {
return
}
err = s.GetReplicaX().Select(&members, membersSelectQuery, membersSelectArgs...)
if err != nil {
return
}
if len(members) != len(userIDs) {
retrievedRecords := make(map[string]bool)
for _, member := range members {
retrievedRecords[member.UserId] = true
}
for _, userID := range userIDs {
if _, ok := retrievedRecords[userID]; !ok {
err = store.NewErrNotFound("User", userID)
return
}
}
}
deleteAt := model.GetMillis()
for _, member := range members {
member.DeleteAt = deleteAt
}
builder := s.getQueryBuilder().
Update("GroupMembers").
Set("DeleteAt", deleteAt).
Where(sq.And{
sq.Eq{"GroupId": groupID},
sq.Eq{"UserId": userIDs},
})
query, args, err = builder.ToSql()
return
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
sq "github.com/mattermost/squirrel"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type relationalCheckConfig struct {
parentName string
parentIdAttr string
childName string
childIdAttr string
canParentIdBeEmpty bool
sortRecords bool
filter any
}
func getOrphanedRecords(ss *SqlStore, cfg relationalCheckConfig) ([]model.OrphanedRecord, error) {
records := []model.OrphanedRecord{}
sub := ss.getQueryBuilder().
Select("TRUE").
From(cfg.parentName + " AS PT").
Prefix("NOT EXISTS (").
Suffix(")").
Where("PT.id = CT." + cfg.parentIdAttr)
main := ss.getQueryBuilder().
Select().
Column("CT." + cfg.parentIdAttr + " AS ParentId").
From(cfg.childName + " AS CT").
Where(sub)
if cfg.childIdAttr != "" {
main = main.Column("CT." + cfg.childIdAttr + " AS ChildId")
}
if cfg.canParentIdBeEmpty {
main = main.Where(sq.NotEq{"CT." + cfg.parentIdAttr: ""})
}
if cfg.filter != nil {
main = main.Where(cfg.filter)
}
if cfg.sortRecords {
main = main.OrderBy("CT." + cfg.parentIdAttr)
}
query, args, err := main.ToSql()
if err != nil {
return nil, err
}
err = ss.GetMasterX().Select(&records, query, args...)
return records, err
}
func checkParentChildIntegrity(ss *SqlStore, config relationalCheckConfig) model.IntegrityCheckResult {
var result model.IntegrityCheckResult
var data model.RelationalIntegrityCheckData
config.sortRecords = true
data.Records, result.Err = getOrphanedRecords(ss, config)
if result.Err != nil {
mlog.Error("Error while getting orphaned records", mlog.Err(result.Err))
return result
}
data.ParentName = config.parentName
data.ChildName = config.childName
data.ParentIdAttr = config.parentIdAttr
data.ChildIdAttr = config.childIdAttr
result.Data = data
return result
}
func checkChannelsCommandWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Channels",
parentIdAttr: "ChannelId",
childName: "CommandWebhooks",
childIdAttr: "Id",
})
}
func checkChannelsChannelMemberHistoryIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Channels",
parentIdAttr: "ChannelId",
childName: "ChannelMemberHistory",
childIdAttr: "",
})
}
func checkChannelsChannelMembersIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Channels",
parentIdAttr: "ChannelId",
childName: "ChannelMembers",
childIdAttr: "",
})
}
func checkChannelsIncomingWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Channels",
parentIdAttr: "ChannelId",
childName: "IncomingWebhooks",
childIdAttr: "Id",
})
}
func checkChannelsOutgoingWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Channels",
parentIdAttr: "ChannelId",
childName: "OutgoingWebhooks",
childIdAttr: "Id",
})
}
func checkChannelsPostsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Channels",
parentIdAttr: "ChannelId",
childName: "Posts",
childIdAttr: "Id",
})
}
func checkChannelsFileInfoIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Channels",
parentIdAttr: "ChannelId",
childName: "FileInfo",
childIdAttr: "Id",
})
}
func checkCommandsCommandWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Commands",
parentIdAttr: "CommandId",
childName: "CommandWebhooks",
childIdAttr: "Id",
})
}
func checkPostsFileInfoIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Posts",
parentIdAttr: "PostId",
childName: "FileInfo",
childIdAttr: "Id",
})
}
func checkPostsPostsRootIdIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Posts",
parentIdAttr: "RootId",
childName: "Posts",
childIdAttr: "Id",
canParentIdBeEmpty: true,
})
}
func checkPostsReactionsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Posts",
parentIdAttr: "PostId",
childName: "Reactions",
childIdAttr: "",
})
}
func checkSchemesChannelsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Schemes",
parentIdAttr: "SchemeId",
childName: "Channels",
childIdAttr: "Id",
canParentIdBeEmpty: true,
})
}
func checkSchemesTeamsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Schemes",
parentIdAttr: "SchemeId",
childName: "Teams",
childIdAttr: "Id",
canParentIdBeEmpty: true,
})
}
func checkSessionsAuditsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Sessions",
parentIdAttr: "SessionId",
childName: "Audits",
childIdAttr: "Id",
canParentIdBeEmpty: true,
})
}
func checkTeamsChannelsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
res1 := checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Teams",
parentIdAttr: "TeamId",
childName: "Channels",
childIdAttr: "Id",
filter: sq.NotEq{"CT.Type": []model.ChannelType{model.ChannelTypeDirect, model.ChannelTypeGroup}},
})
res2 := checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Teams",
parentIdAttr: "TeamId",
childName: "Channels",
childIdAttr: "Id",
canParentIdBeEmpty: true,
filter: sq.Eq{"CT.Type": []model.ChannelType{model.ChannelTypeDirect, model.ChannelTypeGroup}},
})
data1 := res1.Data.(model.RelationalIntegrityCheckData)
data2 := res2.Data.(model.RelationalIntegrityCheckData)
data1.Records = append(data1.Records, data2.Records...)
res1.Data = data1
return res1
}
func checkTeamsCommandsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Teams",
parentIdAttr: "TeamId",
childName: "Commands",
childIdAttr: "Id",
})
}
func checkTeamsIncomingWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Teams",
parentIdAttr: "TeamId",
childName: "IncomingWebhooks",
childIdAttr: "Id",
})
}
func checkTeamsOutgoingWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Teams",
parentIdAttr: "TeamId",
childName: "OutgoingWebhooks",
childIdAttr: "Id",
})
}
func checkTeamsTeamMembersIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Teams",
parentIdAttr: "TeamId",
childName: "TeamMembers",
childIdAttr: "",
})
}
func checkUsersAuditsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "Audits",
childIdAttr: "Id",
canParentIdBeEmpty: true,
})
}
func checkUsersCommandWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "CommandWebhooks",
childIdAttr: "Id",
})
}
func checkUsersChannelMemberHistoryIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "ChannelMemberHistory",
childIdAttr: "",
})
}
func checkUsersChannelMembersIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "ChannelMembers",
childIdAttr: "",
})
}
func checkUsersChannelsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "CreatorId",
childName: "Channels",
childIdAttr: "Id",
canParentIdBeEmpty: true,
})
}
func checkUsersCommandsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "CreatorId",
childName: "Commands",
childIdAttr: "Id",
})
}
func checkUsersCompliancesIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "Compliances",
childIdAttr: "Id",
})
}
func checkUsersEmojiIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "CreatorId",
childName: "Emoji",
childIdAttr: "Id",
})
}
func checkUsersFileInfoIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "CreatorId",
childName: "FileInfo",
childIdAttr: "Id",
})
}
func checkUsersIncomingWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "IncomingWebhooks",
childIdAttr: "Id",
})
}
func checkUsersOAuthAccessDataIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "OAuthAccessData",
childIdAttr: "Token",
})
}
func checkUsersOAuthAppsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "CreatorId",
childName: "OAuthApps",
childIdAttr: "Id",
})
}
func checkUsersOAuthAuthDataIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "OAuthAuthData",
childIdAttr: "Code",
})
}
func checkUsersOutgoingWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "CreatorId",
childName: "OutgoingWebhooks",
childIdAttr: "Id",
})
}
func checkUsersPostsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "Posts",
childIdAttr: "Id",
})
}
func checkUsersPreferencesIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "Preferences",
childIdAttr: "",
})
}
func checkUsersReactionsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "Reactions",
childIdAttr: "",
})
}
func checkUsersSessionsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "Sessions",
childIdAttr: "Id",
})
}
func checkUsersStatusIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "Status",
childIdAttr: "",
})
}
func checkUsersTeamMembersIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "TeamMembers",
childIdAttr: "",
})
}
func checkUsersUserAccessTokensIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Users",
parentIdAttr: "UserId",
childName: "UserAccessTokens",
childIdAttr: "Id",
})
}
func checkChannelsIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
results <- checkChannelsCommandWebhooksIntegrity(ss)
results <- checkChannelsChannelMemberHistoryIntegrity(ss)
results <- checkChannelsChannelMembersIntegrity(ss)
results <- checkChannelsIncomingWebhooksIntegrity(ss)
results <- checkChannelsOutgoingWebhooksIntegrity(ss)
results <- checkChannelsPostsIntegrity(ss)
results <- checkChannelsFileInfoIntegrity(ss)
}
func checkCommandsIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
results <- checkCommandsCommandWebhooksIntegrity(ss)
}
func checkPostsIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
results <- checkPostsFileInfoIntegrity(ss)
results <- checkPostsPostsRootIdIntegrity(ss)
results <- checkPostsReactionsIntegrity(ss)
results <- checkThreadsTeamsIntegrity(ss)
}
func checkSchemesIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
results <- checkSchemesChannelsIntegrity(ss)
results <- checkSchemesTeamsIntegrity(ss)
}
func checkSessionsIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
results <- checkSessionsAuditsIntegrity(ss)
}
func checkTeamsIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
results <- checkTeamsChannelsIntegrity(ss)
results <- checkTeamsCommandsIntegrity(ss)
results <- checkTeamsIncomingWebhooksIntegrity(ss)
results <- checkTeamsOutgoingWebhooksIntegrity(ss)
results <- checkTeamsTeamMembersIntegrity(ss)
}
func checkUsersIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
results <- checkUsersAuditsIntegrity(ss)
results <- checkUsersCommandWebhooksIntegrity(ss)
results <- checkUsersChannelMemberHistoryIntegrity(ss)
results <- checkUsersChannelMembersIntegrity(ss)
results <- checkUsersChannelsIntegrity(ss)
results <- checkUsersCommandsIntegrity(ss)
results <- checkUsersCompliancesIntegrity(ss)
results <- checkUsersEmojiIntegrity(ss)
results <- checkUsersFileInfoIntegrity(ss)
results <- checkUsersIncomingWebhooksIntegrity(ss)
results <- checkUsersOAuthAccessDataIntegrity(ss)
results <- checkUsersOAuthAppsIntegrity(ss)
results <- checkUsersOAuthAuthDataIntegrity(ss)
results <- checkUsersOutgoingWebhooksIntegrity(ss)
results <- checkUsersPostsIntegrity(ss)
results <- checkUsersPreferencesIntegrity(ss)
results <- checkUsersReactionsIntegrity(ss)
results <- checkUsersSessionsIntegrity(ss)
results <- checkUsersStatusIntegrity(ss)
results <- checkUsersTeamMembersIntegrity(ss)
results <- checkUsersUserAccessTokensIntegrity(ss)
}
func checkThreadsTeamsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Teams",
parentIdAttr: "ThreadTeamId",
childName: "Threads",
childIdAttr: "PostId",
canParentIdBeEmpty: false,
})
}
func CheckRelationalIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
mlog.Info("Starting relational integrity checks...")
checkChannelsIntegrity(ss, results)
checkCommandsIntegrity(ss, results)
checkPostsIntegrity(ss, results)
checkSchemesIntegrity(ss, results)
checkSessionsIntegrity(ss, results)
checkTeamsIntegrity(ss, results)
checkUsersIntegrity(ss, results)
mlog.Info("Done with relational integrity checks")
close(results)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"encoding/json"
"fmt"
"strings"
"time"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const (
jobsCleanupDelay = 100 * time.Millisecond
)
type SqlJobStore struct {
*SqlStore
}
func newSqlJobStore(sqlStore *SqlStore) store.JobStore {
return &SqlJobStore{sqlStore}
}
func (jss SqlJobStore) Save(job *model.Job) (*model.Job, error) {
jsonData, err := json.Marshal(job.Data)
if err != nil {
return nil, errors.Wrap(err, "failed marshalling job data")
}
if jss.IsBinaryParamEnabled() {
jsonData = AppendBinaryFlag(jsonData)
}
query := jss.getQueryBuilder().
Insert("Jobs").
Columns("Id", "Type", "Priority", "CreateAt", "StartAt", "LastActivityAt", "Status", "Progress", "Data").
Values(job.Id, job.Type, job.Priority, job.CreateAt, job.StartAt, job.LastActivityAt, job.Status, job.Progress, jsonData)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "failed to generate sqlquery")
}
if _, err = jss.GetMasterX().Exec(queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to save Preference")
}
return job, nil
}
func (jss SqlJobStore) UpdateOptimistically(job *model.Job, currentStatus string) (bool, error) {
dataJSON, jsonErr := json.Marshal(job.Data)
if jsonErr != nil {
return false, errors.Wrap(jsonErr, "failed to encode job's data to JSON")
}
if jss.IsBinaryParamEnabled() {
dataJSON = AppendBinaryFlag(dataJSON)
}
query, args, err := jss.getQueryBuilder().
Update("Jobs").
Set("LastActivityAt", model.GetMillis()).
Set("Status", job.Status).
Set("Data", dataJSON).
Set("Progress", job.Progress).
Where(sq.Eq{"Id": job.Id, "Status": currentStatus}).ToSql()
if err != nil {
return false, errors.Wrap(err, "job_tosql")
}
sqlResult, err := jss.GetMasterX().Exec(query, args...)
if err != nil {
return false, errors.Wrap(err, "failed to update Job")
}
rows, err := sqlResult.RowsAffected()
if err != nil {
return false, errors.Wrap(err, "unable to get rows affected")
}
if rows != 1 {
return false, nil
}
return true, nil
}
func (jss SqlJobStore) UpdateStatus(id string, status string) (*model.Job, error) {
job := &model.Job{
Id: id,
Status: status,
LastActivityAt: model.GetMillis(),
}
if _, err := jss.GetMasterX().NamedExec(`UPDATE Jobs
SET Status=:Status, LastActivityAt=:LastActivityAt
WHERE Id=:Id`, job); err != nil {
return nil, errors.Wrapf(err, "failed to update Job with id=%s", id)
}
return job, nil
}
func (jss SqlJobStore) UpdateStatusOptimistically(id string, currentStatus string, newStatus string) (bool, error) {
builder := jss.getQueryBuilder().
Update("Jobs").
Set("LastActivityAt", model.GetMillis()).
Set("Status", newStatus).
Where(sq.Eq{"Id": id, "Status": currentStatus})
if newStatus == model.JobStatusInProgress {
builder = builder.Set("StartAt", model.GetMillis())
}
query, args, err := builder.ToSql()
if err != nil {
return false, errors.Wrap(err, "job_tosql")
}
sqlResult, err := jss.GetMasterX().Exec(query, args...)
if err != nil {
return false, errors.Wrapf(err, "failed to update Job with id=%s", id)
}
rows, err := sqlResult.RowsAffected()
if err != nil {
return false, errors.Wrap(err, "unable to get rows affected")
}
if rows != 1 {
return false, nil
}
return true, nil
}
func (jss SqlJobStore) Get(id string) (*model.Job, error) {
query, args, err := jss.getQueryBuilder().
Select("*").
From("Jobs").
Where(sq.Eq{"Id": id}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "job_tosql")
}
var status model.Job
if err = jss.GetReplicaX().Get(&status, query, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Job", id)
}
return nil, errors.Wrapf(err, "failed to get Job with id=%s", id)
}
return &status, nil
}
func (jss SqlJobStore) GetAllPage(offset int, limit int) ([]*model.Job, error) {
query, args, err := jss.getQueryBuilder().
Select("*").
From("Jobs").
OrderBy("CreateAt DESC").
Limit(uint64(limit)).
Offset(uint64(offset)).ToSql()
if err != nil {
return nil, errors.Wrap(err, "job_tosql")
}
statuses := []*model.Job{}
if err = jss.GetReplicaX().Select(&statuses, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Jobs")
}
return statuses, nil
}
func (jss SqlJobStore) GetAllByTypesPage(jobTypes []string, offset int, limit int) ([]*model.Job, error) {
query, args, err := jss.getQueryBuilder().
Select("*").
From("Jobs").
Where(sq.Eq{"Type": jobTypes}).
OrderBy("CreateAt DESC").
Limit(uint64(limit)).
Offset(uint64(offset)).ToSql()
if err != nil {
return nil, errors.Wrap(err, "job_tosql")
}
var jobs []*model.Job
if err = jss.GetReplicaX().Select(&jobs, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Jobs with types")
}
return jobs, nil
}
func (jss SqlJobStore) GetAllByType(jobType string) ([]*model.Job, error) {
query, args, err := jss.getQueryBuilder().
Select("*").
From("Jobs").
Where(sq.Eq{"Type": jobType}).
OrderBy("CreateAt DESC").ToSql()
if err != nil {
return nil, errors.Wrap(err, "job_tosql")
}
statuses := []*model.Job{}
if err = jss.GetReplicaX().Select(&statuses, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Jobs with type=%s", jobType)
}
return statuses, nil
}
func (jss SqlJobStore) GetAllByTypeAndStatus(jobType string, status string) ([]*model.Job, error) {
query, args, err := jss.getQueryBuilder().
Select("*").
From("Jobs").
Where(sq.Eq{"Type": jobType, "Status": status}).
OrderBy("CreateAt DESC").ToSql()
if err != nil {
return nil, errors.Wrap(err, "job_tosql")
}
jobs := []*model.Job{}
if err = jss.GetReplicaX().Select(&jobs, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Jobs with type=%s", jobType)
}
return jobs, nil
}
func (jss SqlJobStore) GetAllByTypePage(jobType string, offset int, limit int) ([]*model.Job, error) {
query, args, err := jss.getQueryBuilder().
Select("*").
From("Jobs").
Where(sq.Eq{"Type": jobType}).
OrderBy("CreateAt DESC").
Limit(uint64(limit)).
Offset(uint64(offset)).ToSql()
if err != nil {
return nil, errors.Wrap(err, "job_tosql")
}
statuses := []*model.Job{}
if err = jss.GetReplicaX().Select(&statuses, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Jobs with type=%s", jobType)
}
return statuses, nil
}
func (jss SqlJobStore) GetAllByStatus(status string) ([]*model.Job, error) {
statuses := []*model.Job{}
query, args, err := jss.getQueryBuilder().
Select("*").
From("Jobs").
Where(sq.Eq{"Status": status}).
OrderBy("CreateAt ASC").ToSql()
if err != nil {
return nil, errors.Wrap(err, "job_tosql")
}
if err = jss.GetReplicaX().Select(&statuses, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Jobs with status=%s", status)
}
return statuses, nil
}
func (jss SqlJobStore) GetNewestJobByStatusAndType(status string, jobType string) (*model.Job, error) {
return jss.GetNewestJobByStatusesAndType([]string{status}, jobType)
}
func (jss SqlJobStore) GetNewestJobByStatusesAndType(status []string, jobType string) (*model.Job, error) {
query, args, err := jss.getQueryBuilder().
Select("*").
From("Jobs").
Where(sq.Eq{"Status": status, "Type": jobType}).
OrderBy("CreateAt DESC").
Limit(1).ToSql()
if err != nil {
return nil, errors.Wrap(err, "job_tosql")
}
var job model.Job
if err = jss.GetReplicaX().Get(&job, query, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Job", fmt.Sprintf("<status, type>=<%s, %s>", strings.Join(status, ","), jobType))
}
return nil, errors.Wrapf(err, "failed to find Job with statuses=%s and type=%s", strings.Join(status, ","), jobType)
}
return &job, nil
}
func (jss SqlJobStore) GetCountByStatusAndType(status string, jobType string) (int64, error) {
query, args, err := jss.getQueryBuilder().
Select("COUNT(*)").
From("Jobs").
Where(sq.Eq{"Status": status, "Type": jobType}).ToSql()
if err != nil {
return 0, errors.Wrap(err, "job_tosql")
}
var count int64
err = jss.GetReplicaX().Get(&count, query, args...)
if err != nil {
return int64(0), errors.Wrapf(err, "failed to count Jobs with status=%s and type=%s", status, jobType)
}
return count, nil
}
func (jss SqlJobStore) Delete(id string) (string, error) {
query, args, err := jss.getQueryBuilder().
Delete("Jobs").
Where(sq.Eq{"Id": id}).ToSql()
if err != nil {
return "", errors.Wrap(err, "job_tosql")
}
if _, err = jss.GetMasterX().Exec(query, args...); err != nil {
return "", errors.Wrapf(err, "failed to delete Job with id=%s", id)
}
return id, nil
}
func (jss SqlJobStore) Cleanup(expiryTime int64, batchSize int) error {
var query string
if jss.DriverName() == model.DatabaseDriverPostgres {
query = "DELETE FROM Jobs WHERE Id IN (SELECT Id FROM Jobs WHERE CreateAt < ? AND (Status != ? AND Status != ?) ORDER BY CreateAt ASC LIMIT ?)"
} else {
query = "DELETE FROM Jobs WHERE CreateAt < ? AND (Status != ? AND Status != ?) ORDER BY CreateAt ASC LIMIT ?"
}
var rowsAffected int64 = 1
for rowsAffected > 0 {
sqlResult, err := jss.GetMasterX().Exec(query,
expiryTime, model.JobStatusInProgress, model.JobStatusPending, batchSize)
if err != nil {
return errors.Wrap(err, "unable to delete jobs")
}
var rowErr error
rowsAffected, rowErr = sqlResult.RowsAffected()
if rowErr != nil {
return errors.Wrap(err, "unable to delete jobs")
}
time.Sleep(jobsCleanupDelay)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
// SqlLicenseStore encapsulates the database writes and reads for
// model.LicenseRecord objects.
type SqlLicenseStore struct {
*SqlStore
}
func newSqlLicenseStore(sqlStore *SqlStore) store.LicenseStore {
return &SqlLicenseStore{sqlStore}
}
// Save validates and stores the license instance in the database. The Id
// and Bytes fields are mandatory. The Bytes field is limited to a maximum
// of 10000 bytes. If the license ID matches an existing license in the
// database it returns the license stored in the database. If not, it saves the
// new database and returns the created license with the CreateAt field
// updated.
func (ls SqlLicenseStore) Save(license *model.LicenseRecord) (*model.LicenseRecord, error) {
license.PreSave()
if err := license.IsValid(); err != nil {
return nil, err
}
query := ls.getQueryBuilder().
Select("Id, CreateAt, Bytes").
From("Licenses").
Where(sq.Eq{"Id": license.Id})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "license_tosql")
}
var storedLicense model.LicenseRecord
if err := ls.GetReplicaX().Get(&storedLicense, queryString, args...); err != nil {
// Only insert if not exists
query, args, err := ls.getQueryBuilder().
Insert("Licenses").
Columns("Id", "CreateAt", "Bytes").
Values(license.Id, license.CreateAt, license.Bytes).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "license_record_tosql")
}
if _, err := ls.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to get License with licenseId=%s", license.Id)
}
return license, nil
}
return &storedLicense, nil
}
// Get obtains the license with the provided id parameter from the database.
// If the license doesn't exist it returns a model.AppError with
// http.StatusNotFound in the StatusCode field.
func (ls SqlLicenseStore) Get(id string) (*model.LicenseRecord, error) {
query := ls.getQueryBuilder().
Select("Id, CreateAt, Bytes").
From("Licenses").
Where(sq.Eq{"Id": id})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "license_record_tosql")
}
license := &model.LicenseRecord{}
if err := ls.GetReplicaX().Get(license, queryString, args...); err != nil {
return nil, store.NewErrNotFound("License", id)
}
return license, nil
}
func (ls SqlLicenseStore) GetAll() ([]*model.LicenseRecord, error) {
query := ls.getQueryBuilder().
Select("Id, CreateAt, Bytes").
From("Licenses")
queryString, _, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "license_tosql")
}
licenses := []*model.LicenseRecord{}
if err := ls.GetReplicaX().Select(&licenses, queryString); err != nil {
return nil, errors.Wrap(err, "failed to fetch licenses")
}
return licenses, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"encoding/json"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlLinkMetadataStore struct {
*SqlStore
}
func newSqlLinkMetadataStore(sqlStore *SqlStore) store.LinkMetadataStore {
return &SqlLinkMetadataStore{sqlStore}
}
func (s SqlLinkMetadataStore) Save(metadata *model.LinkMetadata) (*model.LinkMetadata, error) {
if err := metadata.IsValid(); err != nil {
return nil, err
}
metadata.PreSave()
metadataBytes, err := json.Marshal(metadata.Data)
if err != nil {
return nil, errors.Wrap(err, "could not serialize metadataBytes to JSON")
}
if s.IsBinaryParamEnabled() {
metadataBytes = AppendBinaryFlag(metadataBytes)
}
query := s.getQueryBuilder().
Insert("LinkMetadata").
Columns("Hash", "URL", "Timestamp", "Type", "Data").
Values(metadata.Hash, metadata.URL, metadata.Timestamp, metadata.Type, metadataBytes)
if s.DriverName() == model.DatabaseDriverMysql {
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE URL = ?, Timestamp = ?, Type = ?, Data = ?", metadata.URL, metadata.Timestamp, metadata.Type, metadataBytes))
} else {
query = query.SuffixExpr(sq.Expr("ON CONFLICT (hash) DO UPDATE SET URL = ?, Timestamp = ?, Type = ?, Data = ?", metadata.URL, metadata.Timestamp, metadata.Type, metadataBytes))
}
q, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "metadata_tosql")
}
_, err = s.GetMasterX().Exec(q, args...)
if err != nil && !IsUniqueConstraintError(err, []string{"PRIMARY", "linkmetadata_pkey"}) {
return nil, errors.Wrap(err, "could not save link metadata")
}
return metadata, nil
}
func (s SqlLinkMetadataStore) Get(url string, timestamp int64) (*model.LinkMetadata, error) {
var metadata model.LinkMetadata
query, args, err := s.getQueryBuilder().
Select("*").
From("LinkMetadata").
Where(sq.Eq{"URL": url, "Timestamp": timestamp}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "could not create query with querybuilder")
}
err = s.GetReplicaX().Get(&metadata, query, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("LinkMetadata", "url="+url)
}
return nil, errors.Wrapf(err, "could not get metadata with selectone: url=%s", url)
}
err = metadata.DeserializeDataToConcreteType()
if err != nil {
return nil, errors.Wrapf(err, "could not deserialize metadata to concrete type for url=%s", url)
}
return &metadata, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"github.com/pkg/errors"
sq "github.com/mattermost/squirrel"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlNotifyAdminStore struct {
*SqlStore
}
func newSqlNotifyAdminStore(sqlStore *SqlStore) store.NotifyAdminStore {
return &SqlNotifyAdminStore{sqlStore}
}
func (s SqlNotifyAdminStore) insert(data *model.NotifyAdminData) (sql.Result, error) {
query := `INSERT INTO NotifyAdmin (UserId, CreateAt, RequiredPlan, RequiredFeature, Trial) VALUES (:UserId, :CreateAt, :RequiredPlan, :RequiredFeature, :Trial)`
return s.GetMasterX().NamedExec(query, data)
}
func (s SqlNotifyAdminStore) Save(data *model.NotifyAdminData) (*model.NotifyAdminData, error) {
if err := data.IsValid(); err != nil {
return nil, err
}
data.PreSave()
_, err := s.insert(data)
if err != nil {
return nil, errors.Wrap(err, "failed to save Notify Admin data")
}
return data, nil
}
func (s SqlNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostFeature) ([]*model.NotifyAdminData, error) {
data := []*model.NotifyAdminData{}
query, args, err := s.getQueryBuilder().
Select("*").
From("NotifyAdmin").
Where(sq.Eq{"UserId": userId, "RequiredFeature": feature}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "could not build sql query to get all notification data by user id and required feature")
}
if err := s.GetReplicaX().Select(&data, query, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("NotifyAdmin", fmt.Sprintf("user id: %s and required feature: %s", userId, feature))
}
return nil, errors.Wrapf(err, "notifcation data by user id: %s and required feature: %s", userId, feature)
}
return data, nil
}
func (s SqlNotifyAdminStore) Get(trial bool) ([]*model.NotifyAdminData, error) {
data := []*model.NotifyAdminData{}
query, args, err := s.getQueryBuilder().
Select("*").
From("NotifyAdmin").
Where(sq.Eq{"Trial": trial}).
Where("(SentAt IS NULL)").
ToSql()
if err != nil {
return nil, errors.Wrap(err, "could not build sql query to get all notifcation data")
}
if err := s.GetReplicaX().Select(&data, query, args...); err != nil {
return nil, errors.Wrap(err, "notifcation data")
}
return data, nil
}
func (s SqlNotifyAdminStore) DeleteBefore(trial bool, now int64) error {
if _, err := s.GetMasterX().Exec("DELETE FROM NotifyAdmin WHERE Trial = ? AND CreateAt < ? AND SentAt IS NULL", trial, now); err != nil {
return errors.Wrapf(err, "failed to remove all notification data with trial=%t", trial)
}
return nil
}
func (s SqlNotifyAdminStore) Update(userId string, requiredPlan string, requiredFeature model.MattermostFeature, now int64) error {
if _, err := s.GetMasterX().Exec("UPDATE NotifyAdmin SET SentAt = ? WHERE UserId = ? AND RequiredPlan = ? AND RequiredFeature = ?", now, userId, requiredPlan, requiredFeature); err != nil {
return errors.Wrapf(err, "failed to update SentAt for userId=%s and requiredPlan=%s", userId, requiredPlan)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlOAuthStore struct {
*SqlStore
}
func newSqlOAuthStore(sqlStore *SqlStore) store.OAuthStore {
return &SqlOAuthStore{sqlStore}
}
func (as SqlOAuthStore) SaveApp(app *model.OAuthApp) (*model.OAuthApp, error) {
if app.Id != "" {
return nil, store.NewErrInvalidInput("OAuthApp", "Id", app.Id)
}
app.PreSave()
if err := app.IsValid(); err != nil {
return nil, err
}
if _, err := as.GetMasterX().NamedExec(`INSERT INTO OAuthApps
(Id, CreatorId, CreateAt, UpdateAt, ClientSecret, Name, Description, IconURL, CallbackUrls, Homepage, IsTrusted, MattermostAppID)
VALUES
(:Id, :CreatorId, :CreateAt, :UpdateAt, :ClientSecret, :Name, :Description, :IconURL, :CallbackUrls, :Homepage, :IsTrusted, :MattermostAppID)`, app); err != nil {
return nil, errors.Wrap(err, "failed to save OAuthApp")
}
return app, nil
}
func (as SqlOAuthStore) UpdateApp(app *model.OAuthApp) (*model.OAuthApp, error) {
app.PreUpdate()
if err := app.IsValid(); err != nil {
return nil, err
}
var oldApp model.OAuthApp
err := as.GetMasterX().Get(&oldApp, `SELECT * FROM OAuthApps
WHERE id=?`, app.Id)
if err != nil {
return nil, errors.Wrapf(err, "failed to get OAuthApp with id=%s", app.Id)
}
if oldApp.Id == "" {
return nil, store.NewErrInvalidInput("OAuthApp", "Id", app.Id)
}
app.CreateAt = oldApp.CreateAt
app.CreatorId = oldApp.CreatorId
res, err := as.GetMasterX().NamedExec(`UPDATE OAuthApps
SET UpdateAt=:UpdateAt, ClientSecret=:ClientSecret, Name=:Name,
Description=:Description, IconURL=:IconURL, CallbackUrls=:CallbackUrls,
Homepage=:Homepage, IsTrusted=:IsTrusted, MattermostAppID=:MattermostAppID
WHERE Id=:Id`, app)
if err != nil {
return nil, errors.Wrapf(err, "failed to update OAuthApp with id=%s", app.Id)
}
count, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "error while getting rows_affected")
}
if count > 1 {
return nil, store.NewErrInvalidInput("OAuthApp", "Id", app.Id)
}
return app, nil
}
func (as SqlOAuthStore) GetApp(id string) (*model.OAuthApp, error) {
var app model.OAuthApp
if err := as.GetReplicaX().Get(&app, `SELECT * FROM OAuthApps WHERE Id=?`, id); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("OAuthApp", id)
}
return nil, errors.Wrapf(err, "failed to get OAuthApp with id=%s", id)
}
if app.Id == "" {
return nil, store.NewErrNotFound("OAuthApp", id)
}
return &app, nil
}
func (as SqlOAuthStore) GetAppByUser(userId string, offset, limit int) ([]*model.OAuthApp, error) {
apps := []*model.OAuthApp{}
if err := as.GetReplicaX().Select(&apps, "SELECT * FROM OAuthApps WHERE CreatorId = ? LIMIT ? OFFSET ?", userId, limit, offset); err != nil {
return nil, errors.Wrapf(err, "failed to find OAuthApps with userId=%s", userId)
}
return apps, nil
}
func (as SqlOAuthStore) GetApps(offset, limit int) ([]*model.OAuthApp, error) {
apps := []*model.OAuthApp{}
if err := as.GetReplicaX().Select(&apps, "SELECT * FROM OAuthApps LIMIT ? OFFSET ?", limit, offset); err != nil {
return nil, errors.Wrap(err, "failed to find OAuthApps")
}
return apps, nil
}
func (as SqlOAuthStore) GetAuthorizedApps(userId string, offset, limit int) ([]*model.OAuthApp, error) {
apps := []*model.OAuthApp{}
if err := as.GetReplicaX().Select(&apps,
`SELECT o.* FROM OAuthApps AS o INNER JOIN
Preferences AS p ON p.Name=o.Id AND p.UserId=? LIMIT ? OFFSET ?`, userId, limit, offset); err != nil {
return nil, errors.Wrapf(err, "failed to find OAuthApps with userId=%s", userId)
}
return apps, nil
}
func (as SqlOAuthStore) DeleteApp(id string) (err error) {
// wrap in a transaction so that if one fails, everything fails
transaction, err := as.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
if err := as.deleteApp(transaction, id); err != nil {
return err
}
if err := transaction.Commit(); err != nil {
// don't need to rollback here since the transaction is already closed
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (as SqlOAuthStore) SaveAccessData(accessData *model.AccessData) (*model.AccessData, error) {
if err := accessData.IsValid(); err != nil {
return nil, err
}
if _, err := as.GetMasterX().NamedExec(`INSERT INTO OAuthAccessData
(ClientId, UserId, Token, RefreshToken, RedirectUri, ExpiresAt, Scope)
VALUES
(:ClientId, :UserId, :Token, :RefreshToken, :RedirectUri, :ExpiresAt, :Scope)`, accessData); err != nil {
return nil, errors.Wrap(err, "failed to save AccessData")
}
return accessData, nil
}
func (as SqlOAuthStore) GetAccessData(token string) (*model.AccessData, error) {
accessData := model.AccessData{}
if err := as.GetReplicaX().Get(&accessData, "SELECT * FROM OAuthAccessData WHERE Token = ?", token); err != nil {
return nil, errors.Wrapf(err, "failed to get OAuthAccessData with token=%s", token)
}
return &accessData, nil
}
func (as SqlOAuthStore) GetAccessDataByUserForApp(userID, clientID string) ([]*model.AccessData, error) {
accessData := []*model.AccessData{}
if err := as.GetReplicaX().Select(&accessData,
"SELECT * FROM OAuthAccessData WHERE UserId = ? AND ClientId = ?", userID, clientID); err != nil {
return nil, errors.Wrapf(err, "failed to delete OAuthAccessData with userId=%s and clientId=%s", userID, clientID)
}
return accessData, nil
}
func (as SqlOAuthStore) GetAccessDataByRefreshToken(token string) (*model.AccessData, error) {
accessData := model.AccessData{}
if err := as.GetReplicaX().Get(&accessData, "SELECT * FROM OAuthAccessData WHERE RefreshToken = ?", token); err != nil {
return nil, errors.Wrapf(err, "failed to find OAuthAccessData with refreshToken=%s", token)
}
return &accessData, nil
}
func (as SqlOAuthStore) GetPreviousAccessData(userID, clientID string) (*model.AccessData, error) {
accessData := model.AccessData{}
if err := as.GetReplicaX().Get(&accessData, "SELECT * FROM OAuthAccessData WHERE ClientId = ? AND UserId = ?", clientID, userID); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, errors.Wrapf(err, "failed to get AccessData with clientId=%s and userId=%s", clientID, userID)
}
return &accessData, nil
}
func (as SqlOAuthStore) UpdateAccessData(accessData *model.AccessData) (*model.AccessData, error) {
if err := accessData.IsValid(); err != nil {
return nil, err
}
if _, err := as.GetMasterX().NamedExec("UPDATE OAuthAccessData SET Token = :Token, ExpiresAt = :ExpiresAt, RefreshToken = :RefreshToken WHERE ClientId = :ClientId AND UserID = :UserId", accessData); err != nil {
return nil, errors.Wrapf(err, "failed to update OAuthAccessData with userId=%s and clientId=%s", accessData.UserId, accessData.ClientId)
}
return accessData, nil
}
func (as SqlOAuthStore) RemoveAccessData(token string) error {
if _, err := as.GetMasterX().Exec("DELETE FROM OAuthAccessData WHERE Token = ?", token); err != nil {
return errors.Wrapf(err, "failed to delete OAuthAccessData with token=%s", token)
}
return nil
}
func (as SqlOAuthStore) RemoveAllAccessData() error {
if _, err := as.GetMasterX().Exec("DELETE FROM OAuthAccessData"); err != nil {
return errors.Wrap(err, "failed to delete OAuthAccessData")
}
return nil
}
func (as SqlOAuthStore) SaveAuthData(authData *model.AuthData) (*model.AuthData, error) {
authData.PreSave()
if err := authData.IsValid(); err != nil {
return nil, err
}
if _, err := as.GetMasterX().NamedExec(`INSERT INTO OAuthAuthData
(ClientId, UserId, Code, ExpiresIn, CreateAt, RedirectUri, State, Scope)
VALUES
(:ClientId, :UserId, :Code, :ExpiresIn, :CreateAt, :RedirectUri, :State, :Scope)`, authData); err != nil {
return nil, errors.Wrap(err, "failed to save AuthData")
}
return authData, nil
}
func (as SqlOAuthStore) GetAuthData(code string) (*model.AuthData, error) {
var authData model.AuthData
err := as.GetReplicaX().Get(&authData, `SELECT * FROM OAuthAuthData WHERE Code=?`, code)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("AuthData", fmt.Sprintf("code=%s", code))
}
return nil, errors.Wrapf(err, "failed to get AuthData with code=%s", code)
}
if authData.Code == "" {
return nil, store.NewErrNotFound("AuthData", fmt.Sprintf("code=%s", code))
}
return &authData, nil
}
func (as SqlOAuthStore) RemoveAuthData(code string) error {
_, err := as.GetMasterX().Exec("DELETE FROM OAuthAuthData WHERE Code = ?", code)
if err != nil {
return errors.Wrapf(err, "failed to delete AuthData with code=%s", code)
}
return nil
}
func (as SqlOAuthStore) RemoveAuthDataByClientId(clientId string, userId string) error {
_, err := as.GetMasterX().Exec("DELETE FROM OAuthAuthData WHERE ClientId = ? and UserId = ?", clientId, userId)
if err != nil {
return errors.Wrapf(err, "failed to delete AuthData with clientId=%s and userId=%s", clientId, userId)
}
return nil
}
func (as SqlOAuthStore) PermanentDeleteAuthDataByUser(userId string) error {
_, err := as.GetMasterX().Exec("DELETE FROM OAuthAccessData WHERE UserId = ?", userId)
if err != nil {
return errors.Wrapf(err, "failed to delete OAuthAccessData with userId=%s", userId)
}
return nil
}
func (as SqlOAuthStore) deleteApp(transaction *sqlxTxWrapper, clientId string) error {
if _, err := transaction.Exec("DELETE FROM OAuthApps WHERE Id = ?", clientId); err != nil {
return errors.Wrapf(err, "failed to delete OAuthApp with id=%s", clientId)
}
return as.deleteOAuthAppSessions(transaction, clientId)
}
func (as SqlOAuthStore) deleteOAuthAppSessions(transaction *sqlxTxWrapper, clientId string) error {
query := ""
if as.DriverName() == model.DatabaseDriverPostgres {
query = "DELETE FROM Sessions s USING OAuthAccessData o WHERE o.Token = s.Token AND o.ClientId = ?"
} else if as.DriverName() == model.DatabaseDriverMysql {
query = "DELETE s.* FROM Sessions s INNER JOIN OAuthAccessData o ON o.Token = s.Token WHERE o.ClientId = ?"
}
if _, err := transaction.Exec(query, clientId); err != nil {
return errors.Wrapf(err, "failed to delete Session with OAuthAccessData.Id=%s", clientId)
}
return as.deleteOAuthTokens(transaction, clientId)
}
func (as SqlOAuthStore) deleteOAuthTokens(transaction *sqlxTxWrapper, clientId string) error {
if _, err := transaction.Exec("DELETE FROM OAuthAccessData WHERE ClientId = ?", clientId); err != nil {
return errors.Wrapf(err, "failed to delete OAuthAccessData with id=%s", clientId)
}
return as.deleteAppExtras(transaction, clientId)
}
func (as SqlOAuthStore) deleteAppExtras(transaction *sqlxTxWrapper, clientId string) error {
if _, err := transaction.Exec(
`DELETE FROM
Preferences
WHERE
Category = ?
AND Name = ?`, model.PreferenceCategoryAuthorizedOAuthApp, clientId); err != nil {
return errors.Wrapf(err, "failed to delete Preferences with name=%s", clientId)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"bytes"
"database/sql"
"fmt"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const (
defaultPluginKeyFetchLimit = 10
)
type SqlPluginStore struct {
*SqlStore
}
func newSqlPluginStore(sqlStore *SqlStore) store.PluginStore {
return &SqlPluginStore{sqlStore}
}
func (ps SqlPluginStore) SaveOrUpdate(kv *model.PluginKeyValue) (*model.PluginKeyValue, error) {
if err := kv.IsValid(); err != nil {
return nil, err
}
if kv.Value == nil {
// Setting a key to nil is the same as removing it
err := ps.Delete(kv.PluginId, kv.Key)
if err != nil {
return nil, err
}
return kv, nil
}
query := ps.getQueryBuilder().
Insert("PluginKeyValueStore").
Columns("PluginId", "PKey", "PValue", "ExpireAt").
Values(kv.PluginId, kv.Key, kv.Value, kv.ExpireAt)
if ps.DriverName() == model.DatabaseDriverPostgres {
query = query.SuffixExpr(sq.Expr("ON CONFLICT (pluginid, pkey) DO UPDATE SET PValue = ?, ExpireAt = ?", kv.Value, kv.ExpireAt))
} else if ps.DriverName() == model.DatabaseDriverMysql {
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE PValue = ?, ExpireAt = ?", kv.Value, kv.ExpireAt))
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "plugin_tosql")
}
if _, err := ps.GetMasterX().Exec(queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to upsert PluginKeyValue")
}
return kv, nil
}
func (ps SqlPluginStore) CompareAndSet(kv *model.PluginKeyValue, oldValue []byte) (bool, error) {
if err := kv.IsValid(); err != nil {
return false, err
}
if kv.Value == nil {
// Setting a key to nil is the same as removing it
return ps.CompareAndDelete(kv, oldValue)
}
if oldValue == nil {
// Delete any existing, expired value.
query := ps.getQueryBuilder().
Delete("PluginKeyValueStore").
Where(sq.Eq{"PluginId": kv.PluginId}).
Where(sq.Eq{"PKey": kv.Key}).
Where(sq.NotEq{"ExpireAt": int(0)}).
Where(sq.Lt{"ExpireAt": model.GetMillis()})
queryString, args, err := query.ToSql()
if err != nil {
return false, errors.Wrap(err, "plugin_tosql")
}
if _, err = ps.GetMasterX().Exec(queryString, args...); err != nil {
return false, errors.Wrap(err, "failed to delete PluginKeyValue")
}
// Insert if oldValue is nil
queryString, args, err = ps.getQueryBuilder().
Insert("PluginKeyValueStore").
Columns("PluginId", "PKey", "PValue", "ExpireAt").
Values(kv.PluginId, kv.Key, kv.Value, kv.ExpireAt).ToSql()
if err != nil {
return false, errors.Wrap(err, "plugin_tosql")
}
if _, err := ps.GetMasterX().Exec(queryString, args...); err != nil {
// If the error is from unique constraints violation, it's the result of a
// race condition, return false and no error. Otherwise we have a real error and
// need to return it.
if IsUniqueConstraintError(err, []string{"PRIMARY", "PluginId", "Key", "PKey", "pkey"}) {
return false, nil
}
return false, errors.Wrap(err, "failed to insert PluginKeyValue")
}
} else {
currentTime := model.GetMillis()
// Update if oldValue is not nil
query := ps.getQueryBuilder().
Update("PluginKeyValueStore").
Set("PValue", kv.Value).
Set("ExpireAt", kv.ExpireAt).
Where(sq.Eq{"PluginId": kv.PluginId}).
Where(sq.Eq{"PKey": kv.Key}).
Where(sq.Eq{"PValue": oldValue}).
Where(sq.Or{
sq.Eq{"ExpireAt": int(0)},
sq.Gt{"ExpireAt": currentTime},
})
queryString, args, err := query.ToSql()
if err != nil {
return false, errors.Wrap(err, "plugin_tosql")
}
updateResult, err := ps.GetMasterX().Exec(queryString, args...)
if err != nil {
return false, errors.Wrap(err, "failed to update PluginKeyValue")
}
if rowsAffected, err := updateResult.RowsAffected(); err != nil {
// Failed to update
return false, errors.Wrap(err, "unable to get rows affected")
} else if rowsAffected == 0 {
if ps.DriverName() == model.DatabaseDriverMysql && bytes.Equal(oldValue, kv.Value) {
// ROW_COUNT on MySQL is zero even if the row existed but no changes to the row were required.
// Check if the row exists with the required value to distinguish this case. Strictly speaking,
// this isn't a good use of CompareAndSet anyway, since there's no corresponding guarantee of
// atomicity. Nevertheless, let's return results consistent with Postgres and with what might
// be expected in this case.
query := ps.getQueryBuilder().
Select("COUNT(*)").
From("PluginKeyValueStore").
Where(sq.Eq{"PluginId": kv.PluginId}).
Where(sq.Eq{"PKey": kv.Key}).
Where(sq.Eq{"PValue": kv.Value}).
Where(sq.Or{
sq.Eq{"ExpireAt": int(0)},
sq.Gt{"ExpireAt": currentTime},
})
queryString, args, err := query.ToSql()
if err != nil {
return false, errors.Wrap(err, "plugin_tosql")
}
var count int64
err = ps.GetReplicaX().Get(&count, queryString, args...)
if err != nil {
return false, errors.Wrapf(err, "failed to count PluginKeyValue with pluginId=%s and key=%s", kv.PluginId, kv.Key)
}
if count == 0 {
return false, nil
} else if count == 1 {
return true, nil
} else {
return false, errors.Wrapf(err, "got too many rows when counting PluginKeyValue with pluginId=%s, key=%s, rows=%d", kv.PluginId, kv.Key, count)
}
}
// No rows were affected by the update, where condition was not satisfied,
// return false, but no error.
return false, nil
}
}
return true, nil
}
func (ps SqlPluginStore) CompareAndDelete(kv *model.PluginKeyValue, oldValue []byte) (bool, error) {
if err := kv.IsValid(); err != nil {
return false, err
}
if oldValue == nil {
// nil can't be stored. Return showing that we didn't do anything
return false, nil
}
query := ps.getQueryBuilder().
Delete("PluginKeyValueStore").
Where(sq.Eq{"PluginId": kv.PluginId}).
Where(sq.Eq{"PKey": kv.Key}).
Where(sq.Eq{"PValue": oldValue}).
Where(sq.Or{
sq.Eq{"ExpireAt": int(0)},
sq.Gt{"ExpireAt": model.GetMillis()},
})
queryString, args, err := query.ToSql()
if err != nil {
return false, errors.Wrap(err, "plugin_tosql")
}
deleteResult, err := ps.GetMasterX().Exec(queryString, args...)
if err != nil {
return false, errors.Wrap(err, "failed to delete PluginKeyValue")
}
if rowsAffected, err := deleteResult.RowsAffected(); err != nil {
return false, errors.Wrap(err, "unable to get rows affected")
} else if rowsAffected == 0 {
return false, nil
}
return true, nil
}
func (ps SqlPluginStore) SetWithOptions(pluginId string, key string, value []byte, opt model.PluginKVSetOptions) (bool, error) {
if err := opt.IsValid(); err != nil {
return false, err
}
kv, err := model.NewPluginKeyValueFromOptions(pluginId, key, value, opt)
if err != nil {
return false, err
}
if opt.Atomic {
return ps.CompareAndSet(kv, opt.OldValue)
}
savedKv, nErr := ps.SaveOrUpdate(kv)
if nErr != nil {
return false, nErr
}
return savedKv != nil, nil
}
func (ps SqlPluginStore) Get(pluginId, key string) (*model.PluginKeyValue, error) {
currentTime := model.GetMillis()
query := ps.getQueryBuilder().Select("PluginId, PKey, PValue, ExpireAt").
From("PluginKeyValueStore").
Where(sq.Eq{"PluginId": pluginId}).
Where(sq.Eq{"PKey": key}).
Where(sq.Or{sq.Eq{"ExpireAt": 0}, sq.Gt{"ExpireAt": currentTime}})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "plugin_tosql")
}
row := ps.GetReplicaX().QueryRowx(queryString, args...)
var kv model.PluginKeyValue
if err := row.Scan(&kv.PluginId, &kv.Key, &kv.Value, &kv.ExpireAt); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("PluginKeyValue", fmt.Sprintf("pluginId=%s, key=%s", pluginId, key))
}
return nil, errors.Wrapf(err, "failed to get PluginKeyValue with pluginId=%s and key=%s", pluginId, key)
}
return &kv, nil
}
func (ps SqlPluginStore) Delete(pluginId, key string) error {
query := ps.getQueryBuilder().
Delete("PluginKeyValueStore").
Where(sq.Eq{"PluginId": pluginId}).
Where(sq.Eq{"Pkey": key})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "plugin_tosql")
}
if _, err := ps.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrapf(err, "failed to delete PluginKeyValue with pluginId=%s and key=%s", pluginId, key)
}
return nil
}
func (ps SqlPluginStore) DeleteAllForPlugin(pluginId string) error {
query := ps.getQueryBuilder().
Delete("PluginKeyValueStore").
Where(sq.Eq{"PluginId": pluginId})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "plugin_tosql")
}
if _, err := ps.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrapf(err, "failed to get all PluginKeyValues with pluginId=%s ", pluginId)
}
return nil
}
func (ps SqlPluginStore) DeleteAllExpired() error {
currentTime := model.GetMillis()
query := ps.getQueryBuilder().
Delete("PluginKeyValueStore").
Where(sq.NotEq{"ExpireAt": 0}).
Where(sq.Lt{"ExpireAt": currentTime})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "plugin_tosql")
}
if _, err := ps.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrap(err, "failed to delete all expired PluginKeyValues")
}
return nil
}
func (ps SqlPluginStore) List(pluginId string, offset int, limit int) ([]string, error) {
if limit <= 0 {
limit = defaultPluginKeyFetchLimit
}
if offset <= 0 {
offset = 0
}
query := ps.getQueryBuilder().
Select("Pkey").
From("PluginKeyValueStore").
Where(sq.Eq{"PluginId": pluginId}).
Where(sq.Or{
sq.Eq{"ExpireAt": int(0)},
sq.Gt{"ExpireAt": model.GetMillis()},
}).
OrderBy("PKey").
Limit(uint64(limit)).
Offset(uint64(offset))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "plugin_tosql")
}
keys := []string{}
err = ps.GetReplicaX().Select(&keys, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get PluginKeyValues with pluginId=%s", pluginId)
}
return keys, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlPostAcknowledgementStore struct {
*SqlStore
}
func newSqlPostAcknowledgementStore(sqlStore *SqlStore) store.PostAcknowledgementStore {
return &SqlPostAcknowledgementStore{sqlStore}
}
func (s *SqlPostAcknowledgementStore) Get(postID, userID string) (*model.PostAcknowledgement, error) {
query := s.getQueryBuilder().
Select("PostId", "UserId", "AcknowledgedAt").
From("PostAcknowledgements").
Where(sq.And{
sq.Eq{"PostId": postID},
sq.Eq{"UserId": userID},
sq.NotEq{"AcknowledgedAt": 0},
})
var acknowledgement model.PostAcknowledgement
err := s.GetReplicaX().GetBuilder(&acknowledgement, query)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("PostAcknowledgement", postID)
}
return nil, err
}
return &acknowledgement, nil
}
func (s *SqlPostAcknowledgementStore) Save(postID, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) {
if acknowledgedAt == 0 {
acknowledgedAt = model.GetMillis()
}
acknowledgement := &model.PostAcknowledgement{
UserId: userID,
PostId: postID,
AcknowledgedAt: acknowledgedAt,
}
if err := acknowledgement.IsValid(); err != nil {
return nil, err
}
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
query := s.getQueryBuilder().
Insert("PostAcknowledgements").
Columns("PostId", "UserId", "AcknowledgedAt").
Values(acknowledgement.PostId, acknowledgement.UserId, acknowledgement.AcknowledgedAt)
if s.DriverName() == model.DatabaseDriverMysql {
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE AcknowledgedAt = ?", acknowledgement.AcknowledgedAt))
} else {
query = query.SuffixExpr(sq.Expr("ON CONFLICT (postid, userid) DO UPDATE SET AcknowledgedAt = ?", acknowledgement.AcknowledgedAt))
}
_, err = transaction.ExecBuilder(query)
if err != nil {
return nil, err
}
err = updatePost(transaction, acknowledgement.PostId)
if err != nil {
return nil, err
}
err = transaction.Commit()
if err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return acknowledgement, nil
}
func (s *SqlPostAcknowledgementStore) Delete(acknowledgement *model.PostAcknowledgement) error {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
query := s.getQueryBuilder().
Update("PostAcknowledgements").
Set("AcknowledgedAt", 0).
Where(sq.And{
sq.Eq{"PostId": acknowledgement.PostId},
sq.Eq{"UserId": acknowledgement.UserId},
})
_, err = transaction.ExecBuilder(query)
if err != nil {
return err
}
err = updatePost(transaction, acknowledgement.PostId)
if err != nil {
return err
}
err = transaction.Commit()
if err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (s *SqlPostAcknowledgementStore) GetForPost(postID string) ([]*model.PostAcknowledgement, error) {
var acknowledgements []*model.PostAcknowledgement
query := s.getQueryBuilder().
Select("PostId", "UserId", "AcknowledgedAt").
From("PostAcknowledgements").
Where(sq.And{
sq.NotEq{"AcknowledgedAt": 0},
sq.Eq{"PostId": postID},
})
err := s.GetReplicaX().SelectBuilder(&acknowledgements, query)
if err != nil {
return nil, errors.Wrapf(err, "failed to get PostAcknowledgements for postID=%s", postID)
}
return acknowledgements, nil
}
func (s *SqlPostAcknowledgementStore) GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) {
var acknowledgements []*model.PostAcknowledgement
perPage := 200
for i := 0; i < len(postIds); i += perPage {
j := i + perPage
if len(postIds) < j {
j = len(postIds)
}
query := s.getQueryBuilder().
Select("PostId", "UserId", "AcknowledgedAt").
From("PostAcknowledgements").
Where(sq.And{
sq.Eq{"PostId": postIds[i:j]},
sq.NotEq{"AcknowledgedAt": 0},
})
var acknowledgementsBatch []*model.PostAcknowledgement
err := s.GetReplicaX().SelectBuilder(&acknowledgementsBatch, query)
if err != nil {
return nil, errors.Wrapf(err, "failed to get PostAcknowledgements for post list")
}
acknowledgements = append(acknowledgements, acknowledgementsBatch...)
}
return acknowledgements, nil
}
func updatePost(transaction *sqlxTxWrapper, postId string) error {
_, err := transaction.Exec(
`UPDATE
Posts
SET
UpdateAt = ?
WHERE
Id = ?`,
model.GetMillis(),
postId,
)
return err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
sq "github.com/mattermost/squirrel"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlPostPriorityStore struct {
*SqlStore
}
func newSqlPostPriorityStore(sqlStore *SqlStore) store.PostPriorityStore {
return &SqlPostPriorityStore{
SqlStore: sqlStore,
}
}
func (s *SqlPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) {
query := s.getQueryBuilder().
Select("Priority", "RequestedAck", "PersistentNotifications").
From("PostsPriority").
Where(sq.Eq{"PostId": postId})
var postPriority model.PostPriority
err := s.GetReplicaX().GetBuilder(&postPriority, query)
if err != nil {
return nil, err
}
return &postPriority, nil
}
func (s *SqlPostPriorityStore) GetForPosts(postIds []string) ([]*model.PostPriority, error) {
var priority []*model.PostPriority
perPage := 200
for i := 0; i < len(postIds); i += perPage {
j := i + perPage
if len(postIds) < j {
j = len(postIds)
}
query := s.getQueryBuilder().
Select("PostId", "Priority", "RequestedAck", "PersistentNotifications").
From("PostsPriority").
Where(sq.Eq{"PostId": postIds[i:j]})
var priorityBatch []*model.PostPriority
err := s.GetReplicaX().SelectBuilder(&priority, query)
if err != nil {
return nil, err
}
priority = append(priority, priorityBatch...)
}
return priority, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
"sync"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/searchlayer"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SqlPostStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
maxPostSizeOnce sync.Once
maxPostSizeCached int
}
type postWithExtra struct {
ThreadReplyCount int64
IsFollowing *bool
ThreadParticipants model.StringArray
model.Post
}
func (s *SqlPostStore) ClearCaches() {
}
func postSliceColumnsWithTypes() []struct {
Name string
Type reflect.Kind
} {
return []struct {
Name string
Type reflect.Kind
}{
{"Id", reflect.String},
{"CreateAt", reflect.Int64},
{"UpdateAt", reflect.Int64},
{"EditAt", reflect.Int64},
{"DeleteAt", reflect.Int64},
{"IsPinned", reflect.Bool},
{"UserId", reflect.String},
{"ChannelId", reflect.String},
{"RootId", reflect.String},
{"OriginalId", reflect.String},
{"Message", reflect.String},
{"Type", reflect.String},
{"Props", reflect.Map},
{"Hashtags", reflect.String},
{"Filenames", reflect.Slice},
{"FileIds", reflect.Slice},
{"HasReactions", reflect.Bool},
{"RemoteId", reflect.String},
}
}
func postToSlice(post *model.Post) []any {
return []any{
post.Id,
post.CreateAt,
post.UpdateAt,
post.EditAt,
post.DeleteAt,
post.IsPinned,
post.UserId,
post.ChannelId,
post.RootId,
post.OriginalId,
post.Message,
post.Type,
model.StringInterfaceToJSON(post.Props),
post.Hashtags,
model.ArrayToJSON(post.Filenames),
model.ArrayToJSON(post.FileIds),
post.HasReactions,
post.RemoteId,
}
}
func postSliceColumns() []string {
colInfos := postSliceColumnsWithTypes()
cols := make([]string, len(colInfos))
for i, colInfo := range colInfos {
cols[i] = colInfo.Name
}
return cols
}
func postSliceCoalesceQuery() string {
colInfos := postSliceColumnsWithTypes()
cols := make([]string, len(colInfos))
for i, colInfo := range colInfos {
var defaultValue string
switch colInfo.Type {
case reflect.String:
defaultValue = "''"
case reflect.Int64:
defaultValue = "0"
case reflect.Bool:
defaultValue = "false"
case reflect.Map:
defaultValue = "'{}'"
case reflect.Slice:
defaultValue = "'[]'"
}
cols[i] = "COALESCE(Posts." + colInfo.Name + "," + defaultValue + ") AS " + colInfo.Name
}
return strings.Join(cols, ",")
}
func newSqlPostStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.PostStore {
return &SqlPostStore{
SqlStore: sqlStore,
metrics: metrics,
maxPostSizeCached: model.PostMessageMaxRunesV1,
}
}
func (s *SqlPostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, int, error) {
channelNewPosts := make(map[string]int)
channelNewRootPosts := make(map[string]int)
maxDateNewPosts := make(map[string]int64)
maxDateNewRootPosts := make(map[string]int64)
rootIds := make(map[string]int)
maxDateRootIds := make(map[string]int64)
for idx, post := range posts {
if post.Id != "" && !post.IsRemote() {
return nil, idx, store.NewErrInvalidInput("Post", "id", post.Id)
}
post.PreSave()
maxPostSize := s.GetMaxPostSize()
if err := post.IsValid(maxPostSize); err != nil {
return nil, idx, err
}
if currentChannelCount, ok := channelNewPosts[post.ChannelId]; !ok {
if post.IsJoinLeaveMessage() {
channelNewPosts[post.ChannelId] = 0
} else {
channelNewPosts[post.ChannelId] = 1
}
maxDateNewPosts[post.ChannelId] = post.CreateAt
} else {
if !post.IsJoinLeaveMessage() {
channelNewPosts[post.ChannelId] = currentChannelCount + 1
}
if post.CreateAt > maxDateNewPosts[post.ChannelId] {
maxDateNewPosts[post.ChannelId] = post.CreateAt
}
}
if post.RootId == "" {
if currentChannelCount, ok := channelNewRootPosts[post.ChannelId]; !ok {
if post.IsJoinLeaveMessage() {
channelNewRootPosts[post.ChannelId] = 0
} else {
channelNewRootPosts[post.ChannelId] = 1
}
maxDateNewRootPosts[post.ChannelId] = post.CreateAt
} else {
if !post.IsJoinLeaveMessage() {
channelNewRootPosts[post.ChannelId] = currentChannelCount + 1
}
if post.CreateAt > maxDateNewRootPosts[post.ChannelId] {
maxDateNewRootPosts[post.ChannelId] = post.CreateAt
}
}
continue
}
if currentRootCount, ok := rootIds[post.RootId]; !ok {
rootIds[post.RootId] = 1
maxDateRootIds[post.RootId] = post.CreateAt
} else {
rootIds[post.RootId] = currentRootCount + 1
if post.CreateAt > maxDateRootIds[post.RootId] {
maxDateRootIds[post.RootId] = post.CreateAt
}
}
}
builder := s.getQueryBuilder().Insert("Posts").Columns(postSliceColumns()...)
for _, post := range posts {
builder = builder.Values(postToSlice(post)...)
}
query, args, err := builder.ToSql()
if err != nil {
return nil, -1, errors.Wrap(err, "post_tosql")
}
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return posts, -1, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
if _, err = transaction.Exec(query, args...); err != nil {
return nil, -1, errors.Wrap(err, "failed to save Post")
}
if err = s.updateThreadsFromPosts(transaction, posts); err != nil {
return nil, -1, errors.Wrap(err, "update thread from posts failed")
}
if err = s.savePostsPriority(transaction, posts); err != nil {
return nil, -1, errors.Wrap(err, "failed to save PostPriority")
}
if err = transaction.Commit(); err != nil {
// don't need to rollback here since the transaction is already closed
return posts, -1, errors.Wrap(err, "commit_transaction")
}
for channelId, count := range channelNewPosts {
countRoot := channelNewRootPosts[channelId]
if _, err = s.GetMasterX().NamedExec(`UPDATE Channels
SET LastPostAt = GREATEST(:lastpostat, LastPostAt),
LastRootPostAt = GREATEST(:lastrootpostat, LastRootPostAt),
TotalMsgCount = TotalMsgCount + :count,
TotalMsgCountRoot = TotalMsgCountRoot + :countroot
WHERE Id = :channelid`, map[string]any{
"lastpostat": maxDateNewPosts[channelId],
"lastrootpostat": maxDateNewRootPosts[channelId],
"channelid": channelId,
"count": count,
"countroot": countRoot,
}); err != nil {
mlog.Warn("Error updating Channel LastPostAt.", mlog.Err(err))
}
}
for rootId := range rootIds {
if _, err = s.GetMasterX().Exec("UPDATE Posts SET UpdateAt = ? WHERE Id = ?", maxDateRootIds[rootId], rootId); err != nil {
mlog.Warn("Error updating Post UpdateAt.", mlog.Err(err))
}
}
var unknownRepliesPosts []*model.Post
for _, post := range posts {
if post.RootId == "" {
count, ok := rootIds[post.Id]
if ok {
post.ReplyCount += int64(count)
}
} else {
unknownRepliesPosts = append(unknownRepliesPosts, post)
}
}
if len(unknownRepliesPosts) > 0 {
if err := s.populateReplyCount(unknownRepliesPosts); err != nil {
mlog.Warn("Unable to populate the reply count in some posts.", mlog.Err(err))
}
}
return posts, -1, nil
}
func (s *SqlPostStore) Save(post *model.Post) (*model.Post, error) {
posts, _, err := s.SaveMultiple([]*model.Post{post})
if err != nil {
return nil, err
}
return posts[0], nil
}
func (s *SqlPostStore) populateReplyCount(posts []*model.Post) error {
rootIds := []string{}
for _, post := range posts {
rootIds = append(rootIds, post.RootId)
}
countList := []struct {
RootId string
Count int64
}{}
query := s.getQueryBuilder().
Select("RootId, COUNT(Id) AS Count").
From("Posts").
Where(sq.Eq{"RootId": rootIds}).
Where(sq.Eq{"Posts.DeleteAt": 0}).
GroupBy("RootId")
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "post_tosql")
}
err = s.GetMasterX().Select(&countList, queryString, args...)
if err != nil {
return errors.Wrap(err, "failed to count Posts")
}
counts := map[string]int64{}
for _, count := range countList {
counts[count.RootId] = count.Count
}
for _, post := range posts {
count, ok := counts[post.RootId]
if !ok {
post.ReplyCount = 0
}
post.ReplyCount = count
}
return nil
}
func (s *SqlPostStore) Update(newPost *model.Post, oldPost *model.Post) (*model.Post, error) {
newPost.UpdateAt = model.GetMillis()
newPost.PreCommit()
oldPost.DeleteAt = newPost.UpdateAt
oldPost.UpdateAt = newPost.UpdateAt
oldPost.OriginalId = oldPost.Id
oldPost.Id = model.NewId()
oldPost.PreCommit()
maxPostSize := s.GetMaxPostSize()
if err := newPost.IsValid(maxPostSize); err != nil {
return nil, err
}
if _, err := s.GetMasterX().NamedExec(`UPDATE Posts
SET CreateAt=:CreateAt,
UpdateAt=:UpdateAt,
EditAt=:EditAt,
DeleteAt=:DeleteAt,
IsPinned=:IsPinned,
UserId=:UserId,
ChannelId=:ChannelId,
RootId=:RootId,
OriginalId=:OriginalId,
Message=:Message,
Type=:Type,
Props=:Props,
Hashtags=:Hashtags,
Filenames=:Filenames,
FileIds=:FileIds,
HasReactions=:HasReactions,
RemoteId=:RemoteId
WHERE
Id=:Id
`, newPost); err != nil {
return nil, errors.Wrapf(err, "failed to update Post with id=%s", newPost.Id)
}
time := model.GetMillis()
if _, err := s.GetMasterX().Exec("UPDATE Channels SET LastPostAt = ? WHERE Id = ? AND LastPostAt < ?", time, newPost.ChannelId, time); err != nil {
return nil, errors.Wrap(err, "failed to update lastpostat of channels")
}
if newPost.RootId != "" {
if _, err := s.GetMasterX().Exec("UPDATE Posts SET UpdateAt = ? WHERE Id = ? AND UpdateAt < ?", time, newPost.RootId, time); err != nil {
return nil, errors.Wrap(err, "failed to update updateAt of posts")
}
}
// mark the old post as deleted
builder := s.getQueryBuilder().
Insert("Posts").
Columns(postSliceColumns()...).
Values(postToSlice(oldPost)...)
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "post_tosql")
}
_, err = s.GetMasterX().Exec(query, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to insert the old post")
}
return newPost, nil
}
func (s *SqlPostStore) OverwriteMultiple(posts []*model.Post) (_ []*model.Post, _ int, err error) {
updateAt := model.GetMillis()
maxPostSize := s.GetMaxPostSize()
for idx, post := range posts {
post.UpdateAt = updateAt
if appErr := post.IsValid(maxPostSize); appErr != nil {
return nil, idx, appErr
}
}
tx, err := s.GetMasterX().Beginx()
if err != nil {
return nil, -1, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(tx, &err)
for idx, post := range posts {
if _, err2 := tx.NamedExec(`UPDATE Posts
SET CreateAt=:CreateAt,
UpdateAt=:UpdateAt,
EditAt=:EditAt,
DeleteAt=:DeleteAt,
IsPinned=:IsPinned,
UserId=:UserId,
ChannelId=:ChannelId,
RootId=:RootId,
OriginalId=:OriginalId,
Message=:Message,
Type=:Type,
Props=:Props,
Hashtags=:Hashtags,
Filenames=:Filenames,
FileIds=:FileIds,
HasReactions=:HasReactions,
RemoteId=:RemoteId
WHERE
Id=:Id
`, post); err2 != nil {
return nil, idx, errors.Wrapf(err2, "failed to update Post with id=%s", post.Id)
}
if post.RootId != "" {
if _, err2 := tx.Exec("UPDATE Threads SET LastReplyAt = ? WHERE PostId = ?", updateAt, post.Id); err2 != nil {
return nil, idx, errors.Wrapf(err2, "failed to update Threads with postid=%s", post.Id)
}
}
}
err = tx.Commit()
if err != nil {
return nil, -1, errors.Wrap(err, "commit_transaction")
}
return posts, -1, nil
}
func (s *SqlPostStore) Overwrite(post *model.Post) (*model.Post, error) {
posts, _, err := s.OverwriteMultiple([]*model.Post{post})
if err != nil {
return nil, err
}
return posts[0], nil
}
func (s *SqlPostStore) GetFlaggedPosts(userId string, offset int, limit int) (*model.PostList, error) {
return s.getFlaggedPosts(userId, "", "", offset, limit)
}
func (s *SqlPostStore) GetFlaggedPostsForTeam(userId, teamId string, offset int, limit int) (*model.PostList, error) {
return s.getFlaggedPosts(userId, "", teamId, offset, limit)
}
func (s *SqlPostStore) GetFlaggedPostsForChannel(userId, channelId string, offset int, limit int) (*model.PostList, error) {
return s.getFlaggedPosts(userId, channelId, "", offset, limit)
}
// TODO: convert to squirrel HW
func (s *SqlPostStore) getFlaggedPosts(userId, channelId, teamId string, offset int, limit int) (*model.PostList, error) {
pl := model.NewPostList()
posts := []*model.Post{}
query := `
SELECT
A.*, (SELECT count(*) FROM Posts WHERE Posts.RootId = (CASE WHEN A.RootId = '' THEN A.Id ELSE A.RootId END) AND Posts.DeleteAt = 0) as ReplyCount
FROM
(SELECT
*
FROM
Posts
WHERE
Id
IN
(
SELECT
Name
FROM
Preferences
WHERE
UserId = ?
AND Category = ?
)
CHANNEL_FILTER
AND Posts.DeleteAt = 0
) as A
INNER JOIN Channels as B
ON B.Id = A.ChannelId
WHERE
ChannelId IN (
SELECT
Id
FROM
Channels,
ChannelMembers
WHERE
Id = ChannelId
AND UserId = ?
)
TEAM_FILTER
ORDER BY CreateAt DESC
LIMIT ? OFFSET ?`
queryParams := []any{userId, model.PreferenceCategoryFlaggedPost}
var channelClause, teamClause string
channelClause, queryParams = s.buildFlaggedPostChannelFilterClause(channelId, queryParams)
query = strings.Replace(query, "CHANNEL_FILTER", channelClause, 1)
queryParams = append(queryParams, userId)
teamClause, queryParams = s.buildFlaggedPostTeamFilterClause(teamId, queryParams)
query = strings.Replace(query, "TEAM_FILTER", teamClause, 1)
queryParams = append(queryParams, limit, offset)
if err := s.GetReplicaX().Select(&posts, query, queryParams...); err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
for _, post := range posts {
pl.AddPost(post)
pl.AddOrder(post.Id)
}
return pl, nil
}
func (s *SqlPostStore) buildFlaggedPostTeamFilterClause(teamId string, queryParams []any) (string, []any) {
if teamId == "" {
return "", queryParams
}
return "AND B.TeamId = ? OR B.TeamId = ''", append(queryParams, teamId)
}
func (s *SqlPostStore) buildFlaggedPostChannelFilterClause(channelId string, queryParams []any) (string, []any) {
if channelId == "" {
return "", queryParams
}
return "AND ChannelId = ?", append(queryParams, channelId)
}
func (s *SqlPostStore) getPostWithCollapsedThreads(id, userID string, opts model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
if id == "" {
return nil, store.NewErrInvalidInput("Post", "id", id)
}
var columns []string
for _, c := range postSliceColumns() {
columns = append(columns, "Posts."+c)
}
columns = append(columns,
"COALESCE(Threads.ReplyCount, 0) as ThreadReplyCount",
"COALESCE(Threads.LastReplyAt, 0) as LastReplyAt",
"COALESCE(Threads.Participants, '[]') as ThreadParticipants",
"ThreadMemberships.Following as IsFollowing",
)
var post postWithExtra
postFetchQuery, args, err := s.getQueryBuilder().
Select(columns...).
From("Posts").
LeftJoin("Threads ON Threads.PostId = Id").
LeftJoin("ThreadMemberships ON ThreadMemberships.PostId = Id AND ThreadMemberships.UserId = ?", userID).
Where(sq.Eq{"Posts.DeleteAt": 0}).
Where(sq.Eq{"Posts.Id": id}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "getPostWithCollapsedThreads_ToSql2")
}
err = s.GetReplicaX().Get(&post, postFetchQuery, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Post", id)
}
return nil, errors.Wrapf(err, "failed to get Post with id=%s", id)
}
posts := []*model.Post{}
query := s.getQueryBuilder().
Select("*").
From("Posts").
Where(sq.Eq{
"Posts.RootId": id,
"Posts.DeleteAt": 0,
})
var sort string
if opts.Direction != "" {
if opts.Direction == "up" {
sort = "DESC"
} else if opts.Direction == "down" {
sort = "ASC"
}
}
if sort != "" {
query = query.OrderBy("CreateAt " + sort + ", Id " + sort)
}
if opts.FromCreateAt != 0 {
if opts.Direction == "down" {
direction := sq.Gt{"Posts.CreateAt": opts.FromCreateAt}
if opts.FromPost != "" {
query = query.Where(sq.Or{
direction,
sq.And{
sq.Eq{"Posts.CreateAt": opts.FromCreateAt},
sq.Gt{"Posts.Id": opts.FromPost},
},
})
} else {
query = query.Where(direction)
}
} else {
direction := sq.Lt{"Posts.CreateAt": opts.FromCreateAt}
if opts.FromPost != "" {
query = query.Where(sq.Or{
direction,
sq.And{
sq.Eq{"Posts.CreateAt": opts.FromCreateAt},
sq.Lt{"Posts.Id": opts.FromPost},
},
})
} else {
query = query.Where(direction)
}
}
}
if opts.PerPage != 0 {
query = query.Limit(uint64(opts.PerPage + 1))
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "getPostWithCollapsedThreads_Tosql2")
}
err = s.GetReplicaX().Select(&posts, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Posts for thread %s", id)
}
var hasNext bool
if opts.PerPage != 0 {
if len(posts) == opts.PerPage+1 {
hasNext = true
}
}
if hasNext {
// Shave off the last item.
posts = posts[:len(posts)-1]
}
list, err := s.prepareThreadedResponse([]*postWithExtra{&post}, opts.CollapsedThreadsExtended, false, sanitizeOptions)
if err != nil {
return nil, err
}
for _, p := range posts {
list.AddPost(p)
list.AddOrder(p.Id)
}
list.HasNext = hasNext
return list, nil
}
func (s *SqlPostStore) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) {
if opts.CollapsedThreads {
return s.getPostWithCollapsedThreads(id, userID, opts, sanitizeOptions)
}
pl := model.NewPostList()
if id == "" {
return nil, store.NewErrInvalidInput("Post", "id", id)
}
var post model.Post
postFetchQuery := "SELECT p.*, (SELECT count(*) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount FROM Posts p WHERE p.Id = ? AND p.DeleteAt = 0"
err := s.DBXFromContext(ctx).Get(&post, postFetchQuery, id)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Post", id)
}
return nil, errors.Wrapf(err, "failed to get Post with id=%s", id)
}
pl.AddPost(&post)
pl.AddOrder(id)
if !opts.SkipFetchThreads {
rootId := post.RootId
if rootId == "" {
rootId = post.Id
}
if rootId == "" {
return nil, errors.Wrapf(err, "invalid rootId with value=%s", rootId)
}
query := s.getQueryBuilder().
Select("p.*, (SELECT count(*) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount").
From("Posts p").
Where(sq.Or{
sq.Eq{"p.Id": rootId},
sq.Eq{"p.RootId": rootId},
}).
Where(sq.Eq{"p.DeleteAt": 0})
var sort string
if opts.Direction != "" {
if opts.Direction == "up" {
sort = "DESC"
} else if opts.Direction == "down" {
sort = "ASC"
}
}
if sort != "" {
query = query.OrderBy("CreateAt " + sort + ", Id " + sort)
}
if opts.FromCreateAt != 0 {
if opts.Direction == "down" {
direction := sq.Gt{"p.CreateAt": opts.FromCreateAt}
if opts.FromPost != "" {
query = query.Where(sq.Or{
direction,
sq.And{
sq.Eq{"p.CreateAt": opts.FromCreateAt},
sq.Gt{"p.Id": opts.FromPost},
},
})
} else {
query = query.Where(direction)
}
} else {
direction := sq.Lt{"p.CreateAt": opts.FromCreateAt}
if opts.FromPost != "" {
query = query.Where(sq.Or{
direction,
sq.And{
sq.Eq{"p.CreateAt": opts.FromCreateAt},
sq.Lt{"p.Id": opts.FromPost},
},
})
} else {
query = query.Where(direction)
}
}
}
if opts.PerPage != 0 {
query = query.Limit(uint64(opts.PerPage + 1))
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Get_Tosql")
}
posts := []*model.Post{}
err = s.GetReplicaX().Select(&posts, sql, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
var hasNext bool
if opts.PerPage != 0 {
if len(posts) == opts.PerPage+1 {
hasNext = true
}
}
if hasNext {
// Shave off the last item
posts = posts[:len(posts)-1]
}
for _, p := range posts {
if p.Id == id {
// Based on the conditions above such as sq.Or{ sq.Eq{"p.Id": rootId}, sq.Eq{"p.RootId": rootId}, }
// posts may contain the "id" post which has already been fetched and added in the "pl"
// So, skip the "id" to avoid duplicate entry of the post
continue
}
pl.AddPost(p)
pl.AddOrder(p.Id)
}
pl.HasNext = hasNext
}
return pl, nil
}
func (s *SqlPostStore) GetSingle(id string, inclDeleted bool) (*model.Post, error) {
query := s.getQueryBuilder().
Select("p.*").
From("Posts p").
Where(sq.Eq{"p.Id": id})
replyCountSubQuery := s.getQueryBuilder().
Select("COUNT(Posts.Id)").
From("Posts").
Where(sq.Expr("Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0"))
if !inclDeleted {
query = query.Where(sq.Eq{"p.DeleteAt": 0})
}
query = query.Column(sq.Alias(replyCountSubQuery, "ReplyCount"))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "getsingleincldeleted_tosql")
}
var post model.Post
err = s.GetReplicaX().Get(&post, queryString, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Post", id)
}
return nil, errors.Wrapf(err, "failed to get Post with id=%s", id)
}
return &post, nil
}
type etagPosts struct {
Id string
UpdateAt int64
}
//nolint:unparam
func (s *SqlPostStore) InvalidateLastPostTimeCache(channelId string) {
}
//nolint:unparam
func (s *SqlPostStore) GetEtag(channelId string, allowFromCache, collapsedThreads bool) string {
q := s.getQueryBuilder().Select("Id", "UpdateAt").From("Posts").Where(sq.Eq{"ChannelId": channelId}).OrderBy("UpdateAt DESC").Limit(1)
if collapsedThreads {
q.Where(sq.Eq{"RootId": ""})
}
sql, args := q.MustSql()
var et etagPosts
err := s.GetReplicaX().Get(&et, sql, args...)
var result string
if err != nil {
result = fmt.Sprintf("%v.%v", model.CurrentVersion, model.GetMillis())
} else {
result = fmt.Sprintf("%v.%v", model.CurrentVersion, et.UpdateAt)
}
return result
}
// Soft deletes a post
// and cleans up the thread if it's a comment
func (s *SqlPostStore) Delete(postID string, time int64, deleteByID string) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
id := postIds{}
// TODO: change this to later delete thread directly from postID
err = transaction.Get(&id, "SELECT RootId, UserId FROM Posts WHERE Id = ?", postID)
if err != nil {
if err == sql.ErrNoRows {
return store.NewErrNotFound("Post", postID)
}
return errors.Wrapf(err, "failed to delete Post with id=%s", postID)
}
if s.DriverName() == model.DatabaseDriverPostgres {
_, err = transaction.Exec(`UPDATE Posts
SET DeleteAt = $1,
UpdateAt = $1,
Props = jsonb_set(Props, $2, $3)
WHERE Id = $4 OR RootId = $4`, time, jsonKeyPath(model.PostPropsDeleteBy), jsonStringVal(deleteByID), postID)
} else {
// We use ORDER BY clause for MySQL
// to trigger filesort optimization in the index_merge.
// Without it, MySQL does a temporary sort.
// See: https://dev.mysql.com/doc/refman/8.0/en/order-by-optimization.html#order-by-filesort.
_, err = transaction.Exec(`UPDATE Posts
SET DeleteAt = ?,
UpdateAt = ?,
Props = JSON_SET(Props, ?, ?)
Where Id = ? OR RootId = ?
ORDER BY Id`, time, time, "$."+model.PostPropsDeleteBy, deleteByID, postID, postID)
}
if err != nil {
return errors.Wrap(err, "failed to update Posts")
}
if id.RootId == "" {
err = s.deleteThread(transaction, postID, time)
} else {
err = s.updateThreadAfterReplyDeletion(transaction, id.RootId, id.UserId)
}
if err != nil {
return errors.Wrapf(err, "failed to cleanup Thread with postid=%s", id.RootId)
}
if err = transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (s *SqlPostStore) permanentDelete(postId string) (err error) {
var post model.Post
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
err = transaction.Get(&post, "SELECT * FROM Posts WHERE Id = ?", postId)
if err != nil && err != sql.ErrNoRows {
return errors.Wrapf(err, "failed to get Post with id=%s", postId)
}
if err = s.permanentDeleteThreads(transaction, post.Id); err != nil {
return errors.Wrapf(err, "failed to cleanup threads for Post with id=%s", postId)
}
if _, err = transaction.NamedExec("DELETE FROM Posts WHERE Id = :id OR RootId = :rootid", map[string]any{"id": postId, "rootid": postId}); err != nil {
return errors.Wrapf(err, "failed to delete Post with id=%s", postId)
}
if err = transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
type postIds struct {
Id string
RootId string
UserId string
}
func (s *SqlPostStore) permanentDeleteAllCommentByUser(userId string) (err error) {
results := []postIds{}
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
err = transaction.Select(&results, "Select Id, RootId FROM Posts WHERE UserId = ? AND RootId != ''", userId)
if err != nil {
return errors.Wrapf(err, "failed to fetch Posts with userId=%s", userId)
}
_, err = transaction.Exec("DELETE FROM Posts WHERE UserId = ? AND RootId != ''", userId)
if err != nil {
return errors.Wrapf(err, "failed to delete Posts with userId=%s", userId)
}
for _, ids := range results {
if err = s.updateThreadAfterReplyDeletion(transaction, ids.RootId, userId); err != nil {
return err
}
}
if err = transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
// Permanently deletes all comments by user,
// cleans up threads (removes said user from participants and decreases reply count),
// permanent delete all root posts by user,
// and delete threads and thread memberships for those root posts
func (s *SqlPostStore) PermanentDeleteByUser(userId string) error {
// First attempt to delete all the comments for a user
if err := s.permanentDeleteAllCommentByUser(userId); err != nil {
return err
}
// Now attempt to delete all the root posts for a user. This will also
// delete all the comments for each post
found := true
count := 0
for found {
var ids []string
err := s.GetMasterX().Select(&ids, "SELECT Id FROM Posts WHERE UserId = ? LIMIT 1000", userId)
if err != nil {
return errors.Wrapf(err, "failed to find Posts with userId=%s", userId)
}
found = false
for _, id := range ids {
found = true
if err = s.permanentDelete(id); err != nil {
return err
}
}
// This is a fail safe, give up if more than 10k messages
count++
if count >= 10 {
return errors.Wrapf(err, "too many Posts to delete with userId=%s", userId)
}
}
return nil
}
// Permanent deletes all channel root posts and comments,
// deletes all threads and thread memberships
// no thread comment cleanup needed, since we are deleting threads and thread memberships
func (s *SqlPostStore) PermanentDeleteByChannel(channelId string) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
results := []postIds{}
err = transaction.Select(&results, "SELECT Id, RootId, UserId FROM Posts WHERE ChannelId = ?", channelId)
if err != nil {
return errors.Wrapf(err, "failed to fetch Posts with channelId=%s", channelId)
}
for _, ids := range results {
if err = s.permanentDeleteThreads(transaction, ids.Id); err != nil {
return err
}
}
if _, err = transaction.Exec("DELETE FROM Posts WHERE ChannelId = ?", channelId); err != nil {
return errors.Wrapf(err, "failed to delete Posts with channelId=%s", channelId)
}
if err = transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (s *SqlPostStore) prepareThreadedResponse(posts []*postWithExtra, extended, reversed bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
list := model.NewPostList()
var userIds []string
userIdMap := map[string]bool{}
for _, thread := range posts {
for _, participantId := range thread.ThreadParticipants {
if _, ok := userIdMap[participantId]; !ok {
userIdMap[participantId] = true
userIds = append(userIds, participantId)
}
}
}
// usersMap is the global profile map of all participants from all threads.
usersMap := make(map[string]*model.User, len(userIds))
if extended {
users, err := s.User().GetProfileByIds(context.Background(), userIds, &store.UserGetByIdsOpts{}, true)
if err != nil {
return nil, err
}
for _, user := range users {
user.SanitizeProfile(sanitizeOptions)
usersMap[user.Id] = user
}
} else {
for _, userId := range userIds {
usersMap[userId] = &model.User{Id: userId}
}
}
processPost := func(p *postWithExtra) error {
p.Post.ReplyCount = p.ThreadReplyCount
if p.IsFollowing != nil {
p.Post.IsFollowing = model.NewBool(*p.IsFollowing)
}
for _, userID := range p.ThreadParticipants {
participant, ok := usersMap[userID]
if !ok {
return errors.New("cannot find thread participant with id=" + userID)
}
p.Post.Participants = append(p.Post.Participants, participant)
}
return nil
}
l := len(posts)
for i := range posts {
idx := i
// We need to flip the order if we selected backwards
if reversed {
idx = l - i - 1
}
if err := processPost(posts[idx]); err != nil {
return nil, err
}
post := &posts[idx].Post
list.AddPost(post)
list.AddOrder(posts[idx].Id)
}
return list, nil
}
func (s *SqlPostStore) getPostsCollapsedThreads(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
var columns []string
for _, c := range postSliceColumns() {
columns = append(columns, "Posts."+c)
}
columns = append(columns,
"COALESCE(Threads.ReplyCount, 0) as ThreadReplyCount",
"COALESCE(Threads.LastReplyAt, 0) as LastReplyAt",
"COALESCE(Threads.Participants, '[]') as ThreadParticipants",
"ThreadMemberships.Following as IsFollowing",
)
var posts []*postWithExtra
offset := options.PerPage * options.Page
postFetchQuery, args, _ := s.getQueryBuilder().
Select(columns...).
From("Posts").
LeftJoin("Threads ON Threads.PostId = Posts.Id").
LeftJoin("ThreadMemberships ON ThreadMemberships.PostId = Posts.Id AND ThreadMemberships.UserId = ?", options.UserId).
Where(sq.Eq{"Posts.DeleteAt": 0}).
Where(sq.Eq{"Posts.ChannelId": options.ChannelId}).
Where(sq.Eq{"Posts.RootId": ""}).
Limit(uint64(options.PerPage)).
Offset(uint64(offset)).
OrderBy("Posts.CreateAt DESC").ToSql()
err := s.GetReplicaX().Select(&posts, postFetchQuery, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Posts with channelId=%s", options.ChannelId)
}
return s.prepareThreadedResponse(posts, options.CollapsedThreadsExtended, false, sanitizeOptions)
}
func (s *SqlPostStore) GetPosts(options model.GetPostsOptions, _ bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
if options.PerPage > 1000 {
return nil, store.NewErrInvalidInput("Post", "<options.PerPage>", options.PerPage)
}
if options.CollapsedThreads {
return s.getPostsCollapsedThreads(options, sanitizeOptions)
}
offset := options.PerPage * options.Page
rpc := make(chan store.StoreResult, 1)
go func() {
posts, err := s.getRootPosts(options.ChannelId, offset, options.PerPage, options.SkipFetchThreads, options.IncludeDeleted)
rpc <- store.StoreResult{Data: posts, NErr: err}
close(rpc)
}()
cpc := make(chan store.StoreResult, 1)
go func() {
posts, err := s.getParentsPosts(options.ChannelId, offset, options.PerPage, options.SkipFetchThreads, options.IncludeDeleted)
cpc <- store.StoreResult{Data: posts, NErr: err}
close(cpc)
}()
list := model.NewPostList()
rpr := <-rpc
if rpr.NErr != nil {
return nil, rpr.NErr
}
cpr := <-cpc
if cpr.NErr != nil {
return nil, cpr.NErr
}
posts := rpr.Data.([]*model.Post)
parents := cpr.Data.([]*model.Post)
for _, p := range posts {
list.AddPost(p)
list.AddOrder(p.Id)
}
for _, p := range parents {
list.AddPost(p)
}
list.MakeNonNil()
return list, nil
}
func (s *SqlPostStore) getPostsSinceCollapsedThreads(options model.GetPostsSinceOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
var columns []string
for _, c := range postSliceColumns() {
columns = append(columns, "Posts."+c)
}
columns = append(columns,
"COALESCE(Threads.ReplyCount, 0) as ThreadReplyCount",
"COALESCE(Threads.LastReplyAt, 0) as LastReplyAt",
"COALESCE(Threads.Participants, '[]') as ThreadParticipants",
"ThreadMemberships.Following as IsFollowing",
)
var posts []*postWithExtra
postFetchQuery, args, err := s.getQueryBuilder().
Select(columns...).
From("Posts").
LeftJoin("Threads ON Threads.PostId = Posts.Id").
LeftJoin("ThreadMemberships ON ThreadMemberships.PostId = Posts.Id AND ThreadMemberships.UserId = ?", options.UserId).
Where(sq.Eq{"Posts.ChannelId": options.ChannelId}).
Where(sq.Gt{"Posts.UpdateAt": options.Time}).
Where(sq.Eq{"Posts.RootId": ""}).
OrderBy("Posts.CreateAt DESC").
Limit(1000).
ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getPostsSinceCollapsedThreads_ToSql")
}
err = s.GetReplicaX().Select(&posts, postFetchQuery, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Posts with channelId=%s", options.ChannelId)
}
return s.prepareThreadedResponse(posts, options.CollapsedThreadsExtended, false, sanitizeOptions)
}
//nolint:unparam
func (s *SqlPostStore) GetPostsSince(options model.GetPostsSinceOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
if options.CollapsedThreads {
return s.getPostsSinceCollapsedThreads(options, sanitizeOptions)
}
posts := []*model.Post{}
order := "DESC"
if options.SortAscending {
order = "ASC"
}
replyCountQuery1 := ""
replyCountQuery2 := ""
if options.SkipFetchThreads {
replyCountQuery1 = `, (SELECT COUNT(*) FROM Posts WHERE Posts.RootId = (CASE WHEN p1.RootId = '' THEN p1.Id ELSE p1.RootId END) AND Posts.DeleteAt = 0) as ReplyCount`
replyCountQuery2 = `, (SELECT COUNT(*) FROM Posts WHERE Posts.RootId = (CASE WHEN cte.RootId = '' THEN cte.Id ELSE cte.RootId END) AND Posts.DeleteAt = 0) as ReplyCount`
}
var query string
var params []any
// union of IDs and then join to get full posts is faster in mysql
if s.DriverName() == model.DatabaseDriverMysql {
query = `SELECT *` + replyCountQuery1 + ` FROM Posts p1 JOIN (
(SELECT
Id
FROM
Posts p2
WHERE
(UpdateAt > ?
AND ChannelId = ?)
LIMIT 1000)
UNION
(SELECT
Id
FROM
Posts p3
WHERE
Id
IN
(SELECT * FROM (SELECT
RootId
FROM
Posts
WHERE
UpdateAt > ?
AND ChannelId = ?
LIMIT 1000) temp_tab))
) j ON p1.Id = j.Id
ORDER BY CreateAt ` + order
params = []any{options.Time, options.ChannelId, options.Time, options.ChannelId}
} else if s.DriverName() == model.DatabaseDriverPostgres {
query = `WITH cte AS (SELECT
*
FROM
Posts
WHERE
UpdateAt > ? AND ChannelId = ?
LIMIT 1000)
(SELECT *` + replyCountQuery2 + ` FROM cte)
UNION
(SELECT *` + replyCountQuery1 + ` FROM Posts p1 WHERE id in (SELECT rootid FROM cte))
ORDER BY CreateAt ` + order
params = []any{options.Time, options.ChannelId}
}
err := s.GetReplicaX().Select(&posts, query, params...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Posts with channelId=%s", options.ChannelId)
}
list := model.NewPostList()
for _, p := range posts {
list.AddPost(p)
if p.UpdateAt > options.Time {
list.AddOrder(p.Id)
}
}
return list, nil
}
func (s *SqlPostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
query := `
SELECT EXISTS (SELECT 1
FROM
Posts
WHERE
UpdateAt >= ?
AND
ChannelId = ?
AND
UserId = ?
AND
Type = ?
LIMIT 1)`
var exist bool
err := s.GetReplicaX().Get(&exist, query, options.Time, options.ChannelId, userId, model.PostTypeAutoResponder)
if err != nil {
return false, errors.Wrapf(err,
"failed to check if autoresponse posts in channelId=%s for userId=%s since %s", options.ChannelId, userId, model.GetTimeForMillis(options.Time))
}
return exist, nil
}
func (s *SqlPostStore) GetPostsSinceForSync(options model.GetPostsSinceForSyncOptions, cursor model.GetPostsSinceForSyncCursor, limit int) ([]*model.Post, model.GetPostsSinceForSyncCursor, error) {
query := s.getQueryBuilder().
Select("*").
From("Posts").
Where(sq.Or{sq.Gt{"Posts.UpdateAt": cursor.LastPostUpdateAt}, sq.And{sq.Eq{"Posts.UpdateAt": cursor.LastPostUpdateAt}, sq.Gt{"Posts.Id": cursor.LastPostId}}}).
OrderBy("Posts.UpdateAt", "Id").
Limit(uint64(limit))
if options.ChannelId != "" {
query = query.Where(sq.Eq{"Posts.ChannelId": options.ChannelId})
}
if !options.IncludeDeleted {
query = query.Where(sq.Eq{"Posts.DeleteAt": 0})
}
if options.ExcludeRemoteId != "" {
query = query.Where(sq.NotEq{"COALESCE(Posts.RemoteId,'')": options.ExcludeRemoteId})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, cursor, errors.Wrap(err, "getpostssinceforsync_tosql")
}
posts := []*model.Post{}
err = s.GetReplicaX().Select(&posts, queryString, args...)
if err != nil {
return nil, cursor, errors.Wrapf(err, "error getting Posts with channelId=%s", options.ChannelId)
}
if len(posts) != 0 {
cursor.LastPostUpdateAt = posts[len(posts)-1].UpdateAt
cursor.LastPostId = posts[len(posts)-1].Id
}
return posts, cursor, nil
}
func (s *SqlPostStore) GetPostsBefore(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
return s.getPostsAround(true, options, sanitizeOptions)
}
func (s *SqlPostStore) GetPostsAfter(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
return s.getPostsAround(false, options, sanitizeOptions)
}
func (s *SqlPostStore) GetPostsByThread(threadId string, since int64) ([]*model.Post, error) {
query := s.getQueryBuilder().
Select("*").
From("Posts").
Where(sq.Eq{"RootId": threadId}).
Where(sq.Eq{"DeleteAt": 0}).
Where(sq.GtOrEq{"CreateAt": since})
result := []*model.Post{}
err := s.GetReplicaX().SelectBuilder(&result, query)
if err != nil {
return nil, errors.Wrap(err, "failed to fetch thread posts")
}
return result, nil
}
func (s *SqlPostStore) getPostsAround(before bool, options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
if options.Page < 0 {
return nil, store.NewErrInvalidInput("Post", "<options.Page>", options.Page)
}
if options.PerPage < 0 {
return nil, store.NewErrInvalidInput("Post", "<options.PerPage>", options.PerPage)
}
offset := options.Page * options.PerPage
posts := []*postWithExtra{}
parents := []*model.Post{}
var direction string
var sort string
if before {
direction = "<"
sort = "DESC"
} else {
direction = ">"
sort = "ASC"
}
table := "Posts p"
// We force MySQL to use the right index to prevent it from accidentally
// using the index_merge_intersection optimization.
// See MM-27575.
if s.DriverName() == model.DatabaseDriverMysql {
table += " USE INDEX(idx_posts_channel_id_delete_at_create_at)"
}
columns := []string{"p.*"}
if options.CollapsedThreads {
columns = append(columns,
"COALESCE(Threads.ReplyCount, 0) as ThreadReplyCount",
"COALESCE(Threads.LastReplyAt, 0) as LastReplyAt",
"COALESCE(Threads.Participants, '[]') as ThreadParticipants",
"ThreadMemberships.Following as IsFollowing",
)
}
query := s.getQueryBuilder().Select(columns...)
replyCountSubQuery := s.getQueryBuilder().Select("COUNT(*)").From("Posts").Where(sq.Expr("Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END)"))
conditions := sq.And{
sq.Expr(`CreateAt `+direction+` (SELECT CreateAt FROM Posts WHERE Id = ?)`, options.PostId),
sq.Eq{"p.ChannelId": options.ChannelId},
}
if !options.IncludeDeleted {
replyCountSubQuery = replyCountSubQuery.Where(sq.Expr("Posts.DeleteAt = 0"))
conditions = append(conditions, sq.Eq{"p.DeleteAt": int(0)})
}
if options.CollapsedThreads {
conditions = append(conditions, sq.Eq{"RootId": ""})
query = query.LeftJoin("Threads ON Threads.PostId = p.Id").LeftJoin("ThreadMemberships ON ThreadMemberships.PostId = p.Id AND ThreadMemberships.UserId=?", options.UserId)
} else {
query = query.Column(sq.Alias(replyCountSubQuery, "ReplyCount"))
}
query = query.From(table).
Where(conditions).
// Adding ChannelId and DeleteAt order columns
// to let mysql choose the "idx_posts_channel_id_delete_at_create_at" index always.
// See MM-24170.
OrderBy("p.ChannelId", "p.DeleteAt", "p.CreateAt "+sort).
Limit(uint64(options.PerPage)).
Offset(uint64(offset))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "post_tosql")
}
err = s.GetReplicaX().Select(&posts, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Posts with channelId=%s", options.ChannelId)
}
if !options.CollapsedThreads && len(posts) > 0 {
rootIds := []string{}
for _, post := range posts {
rootIds = append(rootIds, post.Id)
if post.RootId != "" {
rootIds = append(rootIds, post.RootId)
}
}
rootQuery := s.getQueryBuilder().Select("p.*")
idQuery := sq.Or{
sq.Eq{"Id": rootIds},
}
rootQuery = rootQuery.Column(sq.Alias(replyCountSubQuery, "ReplyCount"))
if !options.SkipFetchThreads {
idQuery = append(idQuery, sq.Eq{"RootId": rootIds}) // preserve original behaviour
}
rootQuery = rootQuery.From("Posts p").
Where(sq.And{
idQuery,
sq.Eq{"p.ChannelId": options.ChannelId},
}).
OrderBy("CreateAt DESC")
if !options.IncludeDeleted {
rootQuery = rootQuery.Where(sq.Eq{"p.DeleteAt": 0})
}
rootQueryString, rootArgs, nErr := rootQuery.ToSql()
if nErr != nil {
return nil, errors.Wrap(nErr, "post_tosql")
}
nErr = s.GetReplicaX().Select(&parents, rootQueryString, rootArgs...)
if nErr != nil {
return nil, errors.Wrapf(nErr, "failed to find Posts with channelId=%s", options.ChannelId)
}
}
list, err := s.prepareThreadedResponse(posts, options.CollapsedThreadsExtended, !before, sanitizeOptions)
if err != nil {
return nil, err
}
for _, p := range parents {
list.AddPost(p)
}
return list, nil
}
func (s *SqlPostStore) GetPostIdBeforeTime(channelId string, time int64, collapsedThreads bool) (string, error) {
return s.getPostIdAroundTime(channelId, time, true, collapsedThreads)
}
func (s *SqlPostStore) GetPostIdAfterTime(channelId string, time int64, collapsedThreads bool) (string, error) {
return s.getPostIdAroundTime(channelId, time, false, collapsedThreads)
}
func (s *SqlPostStore) getPostIdAroundTime(channelId string, time int64, before bool, collapsedThreads bool) (string, error) {
var direction sq.Sqlizer
var sort string
if before {
direction = sq.Lt{"CreateAt": time}
sort = "DESC"
} else {
direction = sq.Gt{"CreateAt": time}
sort = "ASC"
}
table := "Posts"
// We force MySQL to use the right index to prevent it from accidentally
// using the index_merge_intersection optimization.
// See MM-27575.
if s.DriverName() == model.DatabaseDriverMysql {
table += " USE INDEX(idx_posts_channel_id_delete_at_create_at)"
}
conditions := sq.And{
direction,
sq.Eq{"Posts.ChannelId": channelId},
sq.Eq{"Posts.DeleteAt": int(0)},
}
if collapsedThreads {
conditions = sq.And{conditions, sq.Eq{"Posts.RootId": ""}}
}
query := s.getQueryBuilder().
Select("Id").
From(table).
Where(conditions).
// Adding ChannelId and DeleteAt order columns
// to let mysql choose the "idx_posts_channel_id_delete_at_create_at" index always.
// See MM-23369.
OrderBy("Posts.ChannelId", "Posts.DeleteAt", "Posts.CreateAt "+sort).
Limit(1)
queryString, args, err := query.ToSql()
if err != nil {
return "", errors.Wrap(err, "post_tosql")
}
var postId string
if err := s.GetMasterX().Get(&postId, queryString, args...); err != nil {
if err != sql.ErrNoRows {
return "", errors.Wrapf(err, "failed to get Post id with channelId=%s", channelId)
}
}
return postId, nil
}
func (s *SqlPostStore) GetPostAfterTime(channelId string, time int64, collapsedThreads bool) (*model.Post, error) {
table := "Posts"
// We force MySQL to use the right index to prevent it from accidentally
// using the index_merge_intersection optimization.
// See MM-27575.
if s.DriverName() == model.DatabaseDriverMysql {
table += " USE INDEX(idx_posts_channel_id_delete_at_create_at)"
}
conditions := sq.And{
sq.Gt{"Posts.CreateAt": time},
sq.Eq{"Posts.ChannelId": channelId},
sq.Eq{"Posts.DeleteAt": int(0)},
}
if collapsedThreads {
conditions = sq.And{conditions, sq.Eq{"RootId": ""}}
}
query := s.getQueryBuilder().
Select("*").
From(table).
Where(conditions).
// Adding ChannelId and DeleteAt order columns
// to let mysql choose the "idx_posts_channel_id_delete_at_create_at" index always.
// See MM-23369.
OrderBy("Posts.ChannelId", "Posts.DeleteAt", "Posts.CreateAt ASC").
Limit(1)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "post_tosql")
}
var post model.Post
if err := s.GetMasterX().Get(&post, queryString, args...); err != nil {
if err != sql.ErrNoRows {
return nil, errors.Wrapf(err, "failed to get Post with channelId=%s", channelId)
}
}
return &post, nil
}
func (s *SqlPostStore) getRootPosts(channelId string, offset int, limit int, skipFetchThreads bool, includeDeleted bool) ([]*model.Post, error) {
posts := []*model.Post{}
var fetchQuery string
if skipFetchThreads {
fetchQuery = "SELECT p.*, (SELECT COUNT(*) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END)) as ReplyCount FROM Posts p WHERE p.ChannelId = ? ORDER BY p.CreateAt DESC LIMIT ? OFFSET ?"
if !includeDeleted {
fetchQuery = "SELECT p.*, (SELECT COUNT(*) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount FROM Posts p WHERE p.ChannelId = ? AND p.DeleteAt = 0 ORDER BY p.CreateAt DESC LIMIT ? OFFSET ?"
}
} else {
fetchQuery = "SELECT * FROM Posts WHERE Posts.ChannelId = ? ORDER BY Posts.CreateAt DESC LIMIT ? OFFSET ?"
if !includeDeleted {
fetchQuery = "SELECT * FROM Posts WHERE Posts.ChannelId = ? AND Posts.DeleteAt = 0 ORDER BY Posts.CreateAt DESC LIMIT ? OFFSET ?"
}
}
err := s.GetReplicaX().Select(&posts, fetchQuery, channelId, limit, offset)
if err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
return posts, nil
}
func (s *SqlPostStore) getParentsPosts(channelId string, offset int, limit int, skipFetchThreads bool, includeDeleted bool) ([]*model.Post, error) {
if s.DriverName() == model.DatabaseDriverPostgres {
return s.getParentsPostsPostgreSQL(channelId, offset, limit, skipFetchThreads, includeDeleted)
}
deleteAtCondition := "AND DeleteAt = 0"
if includeDeleted {
deleteAtCondition = ""
}
// query parent Ids first
roots := []string{}
rootQuery := `
SELECT DISTINCT
q.RootId
FROM
(SELECT
Posts.RootId
FROM
Posts
WHERE
ChannelId = ? ` + deleteAtCondition + `
ORDER BY CreateAt DESC
LIMIT ? OFFSET ?) q
WHERE q.RootId != ''`
err := s.GetReplicaX().Select(&roots, rootQuery, channelId, limit, offset)
if err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
if len(roots) == 0 {
return nil, nil
}
cols := []string{"p.*"}
var where sq.Sqlizer
where = sq.Eq{"p.Id": roots}
if skipFetchThreads {
col := "(SELECT COUNT(*) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END)) as ReplyCount"
if !includeDeleted {
col = "(SELECT COUNT(*) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount"
}
cols = append(cols, col)
} else {
where = sq.Or{
where,
sq.Eq{"p.RootId": roots},
}
}
query := s.getQueryBuilder().
Select(cols...).
From("Posts p").
Where(sq.And{
where,
sq.Eq{"p.ChannelId": channelId},
}).
OrderBy("p.CreateAt")
if !includeDeleted {
query = query.Where(sq.Eq{"p.DeleteAt": 0})
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "ParentPosts_Tosql")
}
posts := []*model.Post{}
err = s.GetReplicaX().Select(&posts, sql, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
return posts, nil
}
func (s *SqlPostStore) getParentsPostsPostgreSQL(channelId string, offset int, limit int, skipFetchThreads bool, includeDeleted bool) ([]*model.Post, error) {
posts := []*model.Post{}
replyCountQuery := ""
onStatement := "q1.RootId = q2.Id"
if skipFetchThreads {
replyCountQuery = ` ,(SELECT COUNT(*) FROM Posts WHERE Posts.RootId = (CASE WHEN q2.RootId = '' THEN q2.Id ELSE q2.RootId END)) as ReplyCount`
if !includeDeleted {
replyCountQuery = ` ,(SELECT COUNT(*) FROM Posts WHERE Posts.RootId = (CASE WHEN q2.RootId = '' THEN q2.Id ELSE q2.RootId END) AND Posts.DeleteAt = 0) as ReplyCount`
}
} else {
onStatement += " OR q1.RootId = q2.RootId"
}
deleteAtQueryCondition := "AND q2.DeleteAt = 0"
deleteAtSubQueryCondition := "AND Posts.DeleteAt = 0"
if includeDeleted {
deleteAtQueryCondition, deleteAtSubQueryCondition = "", ""
}
err := s.GetReplicaX().Select(&posts,
`SELECT q2.*`+replyCountQuery+`
FROM
Posts q2
INNER JOIN
(SELECT DISTINCT
q3.RootId
FROM
(SELECT
Posts.RootId
FROM
Posts
WHERE
Posts.ChannelId = ? `+deleteAtSubQueryCondition+`
ORDER BY Posts.CreateAt DESC
LIMIT ? OFFSET ?) q3
WHERE q3.RootId != '') q1
ON `+onStatement+`
WHERE
q2.ChannelId = ? `+deleteAtQueryCondition+`
ORDER BY q2.CreateAt`, channelId, limit, offset, channelId)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Posts with channelId=%s", channelId)
}
return posts, nil
}
// GetNthRecentPostTime returns the CreateAt time of the nth most recent post.
func (s *SqlPostStore) GetNthRecentPostTime(n int64) (int64, error) {
if n <= 0 {
return 0, errors.New("n can't be less than 1")
}
builder := s.getQueryBuilder().
Select("CreateAt").
From("Posts p").
// Consider users posts only for cloud limit
Where(sq.And{
sq.Eq{"p.Type": ""},
sq.Expr("p.UserId NOT IN (SELECT UserId FROM Bots)"),
}).
OrderBy("p.CreateAt DESC").
Limit(1).
Offset(uint64(n - 1))
query, queryArgs, err := builder.ToSql()
if err != nil {
return 0, errors.Wrap(err, "GetNthRecentPostTime_tosql")
}
var createAt int64
if err := s.GetMasterX().Get(&createAt, query, queryArgs...); err != nil {
if err == sql.ErrNoRows {
return 0, store.NewErrNotFound("Post", "none")
}
return 0, errors.Wrapf(err, "failed to get the Nth Post=%d", n)
}
return createAt, nil
}
func (s *SqlPostStore) buildCreateDateFilterClause(params *model.SearchParams, builder sq.SelectBuilder) sq.SelectBuilder {
// handle after: before: on: filters
if params.OnDate != "" {
onDateStart, onDateEnd := params.GetOnDateMillis()
// between `on date` start of day and end of day
builder = builder.Where("CreateAt BETWEEN ? AND ?", onDateStart, onDateEnd)
return builder
}
if params.ExcludedDate != "" {
excludedDateStart, excludedDateEnd := params.GetExcludedDateMillis()
builder = builder.Where("CreateAt NOT BETWEEN ? AND ?", excludedDateStart, excludedDateEnd)
}
if params.AfterDate != "" {
afterDate := params.GetAfterDateMillis()
// greater than `after date`
builder = builder.Where("CreateAt >= ?", afterDate)
}
if params.BeforeDate != "" {
beforeDate := params.GetBeforeDateMillis()
// less than `before date`
builder = builder.Where("CreateAt <= ?", beforeDate)
}
if params.ExcludedAfterDate != "" {
afterDate := params.GetExcludedAfterDateMillis()
builder = builder.Where("CreateAt < ?", afterDate)
}
if params.ExcludedBeforeDate != "" {
beforeDate := params.GetExcludedBeforeDateMillis()
builder = builder.Where("CreateAt > ?", beforeDate)
}
return builder
}
func (s *SqlPostStore) buildSearchTeamFilterClause(teamId string, builder sq.SelectBuilder) sq.SelectBuilder {
if teamId == "" {
return builder
}
return builder.Where(sq.Or{
sq.Eq{"TeamId": teamId},
sq.Eq{"TeamId": ""},
})
}
func (s *SqlPostStore) buildSearchChannelFilterClause(channels []string, exclusion bool, byName bool, builder sq.SelectBuilder) sq.SelectBuilder {
if len(channels) == 0 {
return builder
}
if byName {
if exclusion {
return builder.Where(sq.NotEq{"Name": channels})
}
return builder.Where(sq.Eq{"Name": channels})
}
if exclusion {
return builder.Where(sq.NotEq{"Id": channels})
}
return builder.Where(sq.Eq{"Id": channels})
}
func (s *SqlPostStore) buildSearchUserFilterClause(users []string, exclusion bool, byUsername bool, builder sq.SelectBuilder) sq.SelectBuilder {
if len(users) == 0 {
return builder
}
if byUsername {
if exclusion {
return builder.Where(sq.NotEq{"Username": users})
}
return builder.Where(sq.Eq{"Username": users})
}
if exclusion {
return builder.Where(sq.NotEq{"Id": users})
}
return builder.Where(sq.Eq{"Id": users})
}
func (s *SqlPostStore) buildSearchPostFilterClause(teamID string, fromUsers []string, excludedUsers []string, userByUsername bool, builder sq.SelectBuilder) (sq.SelectBuilder, error) {
if len(fromUsers) == 0 && len(excludedUsers) == 0 {
return builder, nil
}
// Sub-query builder.
sb := s.getSubQueryBuilder().
Select("Id").
From("Users, TeamMembers").
Where(sq.Expr("Users.Id = TeamMembers.UserId"))
if teamID != "" {
sb = sb.Where(sq.Eq{"TeamMembers.TeamId": teamID})
}
sb = s.buildSearchUserFilterClause(fromUsers, false, userByUsername, sb)
sb = s.buildSearchUserFilterClause(excludedUsers, true, userByUsername, sb)
subQuery, subQueryArgs, err := sb.ToSql()
if err != nil {
return sq.SelectBuilder{}, err
}
/*
* Squirrel does not support a sub-query in the WHERE condition.
* https://github.com/Masterminds/squirrel/issues/299
*/
return builder.Where("UserId IN ("+subQuery+")", subQueryArgs...), nil
}
func (s *SqlPostStore) Search(teamId string, userId string, params *model.SearchParams) (*model.PostList, error) {
return s.search(teamId, userId, params, true, true)
}
func (s *SqlPostStore) search(teamId string, userId string, params *model.SearchParams, channelsByName bool, userByUsername bool) (*model.PostList, error) {
list := model.NewPostList()
if params.Terms == "" && params.ExcludedTerms == "" &&
len(params.InChannels) == 0 && len(params.ExcludedChannels) == 0 &&
len(params.FromUsers) == 0 && len(params.ExcludedUsers) == 0 &&
params.OnDate == "" && params.AfterDate == "" && params.BeforeDate == "" {
return list, nil
}
baseQuery := s.getQueryBuilder().Select(
"*",
"(SELECT COUNT(*) FROM Posts WHERE Posts.RootId = (CASE WHEN q2.RootId = '' THEN q2.Id ELSE q2.RootId END) AND Posts.DeleteAt = 0) as ReplyCount",
).From("Posts q2").
Where("q2.DeleteAt = 0").
Where(fmt.Sprintf("q2.Type NOT LIKE '%s%%'", model.PostSystemMessagePrefix)).
OrderByClause("q2.CreateAt DESC").
Limit(100)
var err error
baseQuery, err = s.buildSearchPostFilterClause(teamId, params.FromUsers, params.ExcludedUsers, userByUsername, baseQuery)
if err != nil {
return nil, errors.Wrap(err, "failed to build search post filter clause")
}
baseQuery = s.buildCreateDateFilterClause(params, baseQuery)
termMap := map[string]bool{}
terms := params.Terms
excludedTerms := params.ExcludedTerms
searchType := "Message"
if params.IsHashtag {
searchType = "Hashtags"
for _, term := range strings.Split(terms, " ") {
termMap[strings.ToUpper(term)] = true
}
}
for _, c := range s.specialSearchChars() {
terms = strings.Replace(terms, c, " ", -1)
excludedTerms = strings.Replace(excludedTerms, c, " ", -1)
}
if terms == "" && excludedTerms == "" {
// we've already confirmed that we have a channel or user to search for
} else if s.DriverName() == model.DatabaseDriverPostgres {
// Parse text for wildcards
var wildcard *regexp.Regexp
if wildcard, err = regexp.Compile(`\*($| )`); err == nil {
terms = wildcard.ReplaceAllLiteralString(terms, ":* ")
excludedTerms = wildcard.ReplaceAllLiteralString(excludedTerms, ":* ")
}
excludeClause := ""
if excludedTerms != "" {
excludeClause = " & !(" + strings.Join(strings.Fields(excludedTerms), " | ") + ")"
}
var termsClause string
if params.OrTerms {
termsClause = "(" + strings.Join(strings.Fields(terms), " | ") + ")" + excludeClause
} else if strings.HasPrefix(terms, `"`) && strings.HasSuffix(terms, `"`) {
termsClause = "(" + strings.Join(strings.Fields(terms), " <-> ") + ")" + excludeClause
} else {
termsClause = "(" + strings.Join(strings.Fields(terms), " & ") + ")" + excludeClause
}
searchClause := fmt.Sprintf("to_tsvector('%[1]s', %[2]s) @@ to_tsquery('%[1]s', ?)", s.pgDefaultTextSearchConfig, searchType)
baseQuery = baseQuery.Where(searchClause, termsClause)
} else if s.DriverName() == model.DatabaseDriverMysql {
if searchType == "Message" {
terms, err = removeMysqlStopWordsFromTerms(terms)
if err != nil {
return nil, errors.Wrap(err, "failed to remove Mysql stop-words from terms")
}
if terms == "" {
return list, nil
}
}
excludeClause := ""
if excludedTerms != "" {
excludeClause = " -(" + excludedTerms + ")"
}
var termsClause string
if params.OrTerms {
termsClause = terms + excludeClause
} else {
splitTerms := []string{}
for _, t := range strings.Fields(terms) {
splitTerms = append(splitTerms, "+"+t)
}
termsClause = strings.Join(splitTerms, " ") + excludeClause
}
searchClause := fmt.Sprintf("MATCH (%s) AGAINST (? IN BOOLEAN MODE)", searchType)
baseQuery = baseQuery.Where(searchClause, termsClause)
}
inQuery := s.getSubQueryBuilder().Select("Id").
From("Channels, ChannelMembers").
Where("Id = ChannelId")
if !params.IncludeDeletedChannels {
inQuery = inQuery.Where("Channels.DeleteAt = 0")
}
if !params.SearchWithoutUserId {
inQuery = inQuery.Where("ChannelMembers.UserId = ?", userId)
}
inQuery = s.buildSearchTeamFilterClause(teamId, inQuery)
inQuery = s.buildSearchChannelFilterClause(params.InChannels, false, channelsByName, inQuery)
inQuery = s.buildSearchChannelFilterClause(params.ExcludedChannels, true, channelsByName, inQuery)
inQueryClause, inQueryClauseArgs, err := inQuery.ToSql()
if err != nil {
return nil, err
}
baseQuery = baseQuery.Where(fmt.Sprintf("ChannelId IN (%s)", inQueryClause), inQueryClauseArgs...)
searchQuery, searchQueryArgs, err := baseQuery.ToSql()
if err != nil {
return nil, err
}
var posts []*model.Post
if err := s.GetSearchReplicaX().Select(&posts, searchQuery, searchQueryArgs...); err != nil {
mlog.Warn("Query error searching posts.", mlog.Err(err))
// Don't return the error to the caller as it is of no use to the user. Instead return an empty set of search results.
} else {
for _, p := range posts {
if searchType == "Hashtags" {
exactMatch := false
for _, tag := range strings.Split(p.Hashtags, " ") {
if termMap[strings.ToUpper(tag)] {
exactMatch = true
break
}
}
if !exactMatch {
continue
}
}
list.AddPost(p)
list.AddOrder(p.Id)
}
}
list.MakeNonNil()
return list, nil
}
func removeMysqlStopWordsFromTerms(terms string) (string, error) {
stopWords := make([]string, len(searchlayer.MySQLStopWords))
copy(stopWords, searchlayer.MySQLStopWords)
re, err := regexp.Compile(fmt.Sprintf(`^(%s)$`, strings.Join(stopWords, "|")))
if err != nil {
return "", err
}
newTerms := make([]string, 0)
separatedTerms := strings.Fields(terms)
for _, term := range separatedTerms {
term = strings.TrimSpace(term)
if term = re.ReplaceAllString(term, ""); term != "" {
newTerms = append(newTerms, term)
}
}
return strings.Join(newTerms, " "), nil
}
// TODO: convert to squirrel HW
func (s *SqlPostStore) AnalyticsUserCountsWithPostsByDay(teamId string) (model.AnalyticsRows, error) {
var args []any
query :=
`SELECT DISTINCT
DATE(FROM_UNIXTIME(Posts.CreateAt / 1000)) AS Name,
COUNT(DISTINCT Posts.UserId) AS Value
FROM Posts`
if teamId != "" {
query += " INNER JOIN Channels ON Posts.ChannelId = Channels.Id AND Channels.TeamId = ? AND"
args = []any{teamId}
} else {
query += " WHERE"
}
query += ` Posts.CreateAt >= ? AND Posts.CreateAt <= ?
GROUP BY DATE(FROM_UNIXTIME(Posts.CreateAt / 1000))
ORDER BY Name DESC
LIMIT 30`
if s.DriverName() == model.DatabaseDriverPostgres {
query =
`SELECT
TO_CHAR(DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)), 'YYYY-MM-DD') AS Name, COUNT(DISTINCT Posts.UserId) AS Value
FROM Posts`
if teamId != "" {
query += " INNER JOIN Channels ON Posts.ChannelId = Channels.Id AND Channels.TeamId = ? AND"
args = []any{teamId}
} else {
query += " WHERE"
}
query += ` Posts.CreateAt >= ? AND Posts.CreateAt <= ?
GROUP BY DATE(TO_TIMESTAMP(Posts.CreateAt / 1000))
ORDER BY Name DESC
LIMIT 30`
}
end := utils.MillisFromTime(utils.EndOfDay(utils.Yesterday()))
start := utils.MillisFromTime(utils.StartOfDay(utils.Yesterday().AddDate(0, 0, -31)))
args = append(args, start, end)
rows := model.AnalyticsRows{}
err := s.GetReplicaX().Select(
&rows,
query,
args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Posts with teamId=%s", teamId)
}
return rows, nil
}
// TODO: convert to squirrel HW
func (s *SqlPostStore) AnalyticsPostCountsByDay(options *model.AnalyticsPostCountsOptions) (model.AnalyticsRows, error) {
var args []any
query :=
`SELECT
DATE(FROM_UNIXTIME(Posts.CreateAt / 1000)) AS Name,
COUNT(Posts.Id) AS Value
FROM Posts`
if options.BotsOnly {
query += " INNER JOIN Bots ON Posts.UserId = Bots.Userid"
}
if options.TeamId != "" {
query += " INNER JOIN Channels ON Posts.ChannelId = Channels.Id AND Channels.TeamId = ? AND"
args = []any{options.TeamId}
} else {
query += " WHERE"
}
query += ` Posts.CreateAt <= ?
AND Posts.CreateAt >= ?
GROUP BY DATE(FROM_UNIXTIME(Posts.CreateAt / 1000))
ORDER BY Name DESC
LIMIT 30`
if s.DriverName() == model.DatabaseDriverPostgres {
query =
`SELECT
TO_CHAR(DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)), 'YYYY-MM-DD') AS Name, Count(Posts.Id) AS Value
FROM Posts`
if options.BotsOnly {
query += " INNER JOIN Bots ON Posts.UserId = Bots.Userid"
}
if options.TeamId != "" {
query += " INNER JOIN Channels ON Posts.ChannelId = Channels.Id AND Channels.TeamId = ? AND"
args = []any{options.TeamId}
} else {
query += " WHERE"
}
query += ` Posts.CreateAt <= ?
AND Posts.CreateAt >= ?
GROUP BY DATE(TO_TIMESTAMP(Posts.CreateAt / 1000))
ORDER BY Name DESC
LIMIT 30`
}
end := utils.MillisFromTime(utils.EndOfDay(utils.Yesterday()))
start := utils.MillisFromTime(utils.StartOfDay(utils.Yesterday().AddDate(0, 0, -31)))
if options.YesterdayOnly {
start = utils.MillisFromTime(utils.StartOfDay(utils.Yesterday().AddDate(0, 0, -1)))
}
args = append(args, end, start)
rows := model.AnalyticsRows{}
err := s.GetReplicaX().Select(
&rows,
query,
args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Posts with teamId=%s", options.TeamId)
}
return rows, nil
}
func (s *SqlPostStore) AnalyticsPostCount(options *model.PostCountOptions) (int64, error) {
query := s.getQueryBuilder().
Select("COUNT(*) AS Value").
From("Posts p")
if options.TeamId != "" {
query = query.
Join("Channels c ON (c.Id = p.ChannelId)").
Where(sq.Eq{"c.TeamId": options.TeamId})
}
if options.UsersPostsOnly {
query = query.Where(sq.And{
sq.Eq{"p.Type": ""},
sq.Expr("p.UserId NOT IN (SELECT UserId FROM Bots)"),
})
}
if options.MustHaveFile {
query = query.Where(sq.Or{sq.NotEq{"p.FileIds": "[]"}, sq.NotEq{"p.Filenames": "[]"}})
}
if options.MustHaveHashtag {
query = query.Where(sq.NotEq{"p.Hashtags": ""})
}
if options.ExcludeDeleted {
query = query.Where(sq.Eq{"p.DeleteAt": 0})
}
if options.ExcludeSystemPosts {
query = query.Where("p.Type NOT LIKE 'system_%'")
}
if options.SinceUpdateAt > 0 {
query = query.Where(sq.Or{
sq.Gt{"p.UpdateAt": options.SinceUpdateAt},
sq.And{
sq.Eq{"p.UpdateAt": options.SinceUpdateAt},
sq.Gt{"p.Id": options.SincePostID},
},
})
}
queryString, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "post_tosql")
}
var v int64
err = s.GetReplicaX().Get(&v, queryString, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to count Posts")
}
return v, nil
}
func (s *SqlPostStore) GetPostsCreatedAt(channelId string, time int64) ([]*model.Post, error) {
query := `SELECT * FROM Posts WHERE CreateAt = ? AND ChannelId = ?`
posts := []*model.Post{}
err := s.GetReplicaX().Select(&posts, query, time, channelId)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Posts with channelId=%s", channelId)
}
return posts, nil
}
func (s *SqlPostStore) GetPostsByIds(postIds []string) ([]*model.Post, error) {
baseQuery := s.getQueryBuilder().Select("p.*, (SELECT count(*) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount").
From("Posts p").
Where(sq.Eq{"p.Id": postIds}).
OrderBy("CreateAt DESC")
query, args, err := baseQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "getPostsByIds_tosql")
}
posts := []*model.Post{}
err = s.GetReplicaX().Select(&posts, query, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
if len(posts) == 0 {
return nil, store.NewErrNotFound("Post", fmt.Sprintf("postIds=%v", postIds))
}
return posts, nil
}
func (s *SqlPostStore) GetEditHistoryForPost(postId string) ([]*model.Post, error) {
builder := s.getQueryBuilder().
Select("*").
From("Posts").
Where(sq.Eq{"Posts.OriginalId": postId}).
OrderBy("Posts.EditAt DESC")
queryString, args, err := builder.ToSql()
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Post", postId)
}
return nil, errors.Wrap(err, "failed to find post history")
}
posts := []*model.Post{}
err = s.GetReplicaX().Select(&posts, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "error getting posts edit history with postId=%s", postId)
}
if len(posts) == 0 {
return nil, store.NewErrNotFound("failed to find post history", postId)
}
return posts, nil
}
func (s *SqlPostStore) GetPostsBatchForIndexing(startTime int64, startPostID string, limit int) ([]*model.PostForIndexing, error) {
posts := []*model.PostForIndexing{}
table := "Posts"
// We force this index to avoid any chances of index merge intersection.
if s.DriverName() == model.DatabaseDriverMysql {
table += " USE INDEX(idx_posts_create_at_id)"
}
query := `SELECT
Posts.*, Channels.TeamId
FROM ` + table + `
LEFT JOIN
Channels
ON
Posts.ChannelId = Channels.Id
WHERE
Posts.CreateAt > ?
OR
(Posts.CreateAt = ? AND Posts.Id > ?)
ORDER BY
Posts.CreateAt ASC, Posts.Id ASC
LIMIT
?`
err := s.GetSearchReplicaX().Select(&posts, query, startTime, startTime, startPostID, limit)
if err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
return posts, nil
}
// PermanentDeleteBatchForRetentionPolicies deletes a batch of records which are affected by
// the global or a granular retention policy.
// See `genericPermanentDeleteBatchForRetentionPolicies` for details.
func (s *SqlPostStore) PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
builder := s.getQueryBuilder().
Select("Posts.Id").
From("Posts")
return genericPermanentDeleteBatchForRetentionPolicies(RetentionPolicyBatchDeletionInfo{
BaseBuilder: builder,
Table: "Posts",
TimeColumn: "CreateAt",
PrimaryKeys: []string{"Id"},
ChannelIDTable: "Posts",
NowMillis: now,
GlobalPolicyEndTime: globalPolicyEndTime,
Limit: limit,
}, s.SqlStore, cursor)
}
// DeleteOrphanedRows removes entries from Posts when a corresponding channel no longer exists.
func (s *SqlPostStore) DeleteOrphanedRows(limit int) (deleted int64, err error) {
var query string
// We need the extra level of nesting to deal with MySQL's locking
if s.DriverName() == model.DatabaseDriverMysql {
// MySQL fails to do a proper antijoin if the selecting column
// and the joining column are different. In that case, doing a subquery
// leads to a faster plan because MySQL materializes the sub-query
// and does a covering index scan on Posts table. More details on the PR with
// this commit.
query = `
DELETE FROM Posts WHERE Id IN (
SELECT * FROM (
SELECT Posts.Id FROM Posts
WHERE Posts.ChannelId NOT IN (SELECT Id FROM Channels USE INDEX (PRIMARY))
LIMIT ?
) AS A
)`
} else {
query = `
DELETE FROM Posts WHERE Id IN (
SELECT * FROM (
SELECT Posts.Id FROM Posts
LEFT JOIN Channels ON Posts.ChannelId = Channels.Id
WHERE Channels.Id IS NULL
LIMIT ?
) AS A
)`
}
result, err := s.GetMasterX().Exec(query, limit)
if err != nil {
return
}
deleted, err = result.RowsAffected()
return
}
func (s *SqlPostStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
var query string
if s.DriverName() == "postgres" {
query = "DELETE from Posts WHERE Id = any (array (SELECT Id FROM Posts WHERE CreateAt < ? LIMIT ?))"
} else {
query = "DELETE from Posts WHERE CreateAt < ? LIMIT ?"
}
sqlResult, err := s.GetMasterX().Exec(query, endTime, limit)
if err != nil {
return 0, errors.Wrap(err, "failed to delete Posts")
}
rowsAffected, err := sqlResult.RowsAffected()
if err != nil {
return 0, errors.Wrap(err, "failed to delete Posts")
}
return rowsAffected, nil
}
func (s *SqlPostStore) GetOldest() (*model.Post, error) {
var post model.Post
err := s.GetReplicaX().Get(&post, "SELECT * FROM Posts ORDER BY CreateAt LIMIT 1")
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Post", "none")
}
return nil, errors.Wrap(err, "failed to get oldest Post")
}
return &post, nil
}
func (s *SqlPostStore) determineMaxPostSize() int {
var maxPostSizeBytes int32
if s.DriverName() == model.DatabaseDriverPostgres {
// The Post.Message column in Postgres has historically been VARCHAR(4000), but
// may be manually enlarged to support longer posts.
if err := s.GetReplicaX().Get(&maxPostSizeBytes, `
SELECT
COALESCE(character_maximum_length, 0)
FROM
information_schema.columns
WHERE
table_name = 'posts'
AND column_name = 'message'
`); err != nil {
mlog.Warn("Unable to determine the maximum supported post size", mlog.Err(err))
}
} else if s.DriverName() == model.DatabaseDriverMysql {
// The Post.Message column in MySQL has historically been TEXT, with a maximum
// limit of 65535.
if err := s.GetReplicaX().Get(&maxPostSizeBytes, `
SELECT
COALESCE(CHARACTER_MAXIMUM_LENGTH, 0)
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
table_schema = DATABASE()
AND table_name = 'Posts'
AND column_name = 'Message'
LIMIT 0, 1
`); err != nil {
mlog.Warn("Unable to determine the maximum supported post size", mlog.Err(err))
}
} else {
mlog.Warn("No implementation found to determine the maximum supported post size")
}
// Assume a worst-case representation of four bytes per rune.
maxPostSize := int(maxPostSizeBytes) / 4
// To maintain backwards compatibility, don't yield a maximum post
// size smaller than the previous limit, even though it wasn't
// actually possible to store 4000 runes in all cases.
if maxPostSize < model.PostMessageMaxRunesV1 {
maxPostSize = model.PostMessageMaxRunesV1
}
mlog.Info("Post.Message has size restrictions", mlog.Int("max_characters", maxPostSize), mlog.Int32("max_bytes", maxPostSizeBytes))
return maxPostSize
}
// GetMaxPostSize returns the maximum number of runes that may be stored in a post.
func (s *SqlPostStore) GetMaxPostSize() int {
s.maxPostSizeOnce.Do(func() {
s.maxPostSizeCached = s.determineMaxPostSize()
})
return s.maxPostSizeCached
}
func (s *SqlPostStore) GetParentsForExportAfter(limit int, afterId string) ([]*model.PostForExport, error) {
for {
rootIds := []string{}
err := s.GetReplicaX().Select(&rootIds,
`SELECT
Id
FROM
Posts
WHERE
Posts.Id > ?
AND Posts.RootId = ''
AND Posts.DeleteAt = 0
ORDER BY Posts.Id
LIMIT ?`,
afterId, limit)
if err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
postsForExport := []*model.PostForExport{}
if len(rootIds) == 0 {
return postsForExport, nil
}
builder := s.getQueryBuilder().
Select("p1.*, Users.Username as Username, Teams.Name as TeamName, Channels.Name as ChannelName").
FromSelect(sq.Select("*").From("Posts").Where(sq.Eq{"Posts.Id": rootIds}), "p1").
InnerJoin("Channels ON p1.ChannelId = Channels.Id").
InnerJoin("Teams ON Channels.TeamId = Teams.Id").
InnerJoin("Users ON p1.UserId = Users.Id").
Where(sq.And{
sq.Eq{"Channels.DeleteAt": 0},
sq.Eq{"Teams.DeleteAt": 0},
}).
OrderBy("p1.Id")
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "postsForExport_toSql")
}
err = s.GetSearchReplicaX().Select(&postsForExport, query, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
if len(postsForExport) == 0 {
// All of the posts were in channels or teams that were deleted.
// Update the afterId and try again.
afterId = rootIds[len(rootIds)-1]
continue
}
return postsForExport, nil
}
}
func (s *SqlPostStore) GetRepliesForExport(rootId string) ([]*model.ReplyForExport, error) {
posts := []*model.ReplyForExport{}
err := s.GetSearchReplicaX().Select(&posts, `
SELECT
Posts.*,
Users.Username as Username
FROM
Posts
INNER JOIN
Users ON Posts.UserId = Users.Id
WHERE
Posts.RootId = ?
AND Posts.DeleteAt = 0
ORDER BY
Posts.Id`, rootId)
if err != nil {
return nil, errors.Wrap(err, "failed to find Posts")
}
return posts, nil
}
func (s *SqlPostStore) GetDirectPostParentsForExportAfter(limit int, afterId string) ([]*model.DirectPostForExport, error) {
query := s.getQueryBuilder().
Select("p.*", "Users.Username as User").
From("Posts p").
Join("Channels ON p.ChannelId = Channels.Id").
Join("Users ON p.UserId = Users.Id").
Where(sq.And{
sq.Gt{"p.Id": afterId},
sq.Eq{"p.RootId": ""},
sq.Eq{"p.DeleteAt": 0},
sq.Eq{"Channels.DeleteAt": 0},
sq.Eq{"Users.DeleteAt": 0},
sq.Eq{"Channels.Type": []model.ChannelType{model.ChannelTypeDirect, model.ChannelTypeGroup}},
}).
OrderBy("p.Id").
Limit(uint64(limit))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "post_tosql")
}
posts := []*model.DirectPostForExport{}
if err2 := s.GetReplicaX().Select(&posts, queryString, args...); err2 != nil {
return nil, errors.Wrap(err2, "failed to find Posts")
}
var channelIds []string
for _, post := range posts {
channelIds = append(channelIds, post.ChannelId)
}
query = s.getQueryBuilder().
Select("u.Username as Username, ChannelId, UserId, cm.Roles as Roles, LastViewedAt, MsgCount, MentionCount, MentionCountRoot, cm.NotifyProps as NotifyProps, LastUpdateAt, SchemeUser, SchemeAdmin, (SchemeGuest IS NOT NULL AND SchemeGuest) as SchemeGuest").
From("ChannelMembers cm").
Join("Users u ON ( u.Id = cm.UserId )").
Where(sq.Eq{
"cm.ChannelId": channelIds,
})
queryString, args, err = query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "post_tosql")
}
channelMembers := []*model.ChannelMemberForExport{}
if err := s.GetReplicaX().Select(&channelMembers, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find ChannelMembers")
}
// Build a map of channels and their posts
postsChannelMap := make(map[string][]*model.DirectPostForExport)
for _, post := range posts {
post.ChannelMembers = &[]string{}
postsChannelMap[post.ChannelId] = append(postsChannelMap[post.ChannelId], post)
}
// Build a map of channels and their members
channelMembersMap := make(map[string][]string)
for _, member := range channelMembers {
channelMembersMap[member.ChannelId] = append(channelMembersMap[member.ChannelId], member.Username)
}
// Populate each post ChannelMembers extracting it from the channelMembersMap
for channelId := range channelMembersMap {
for _, post := range postsChannelMap[channelId] {
*post.ChannelMembers = channelMembersMap[channelId]
}
}
return posts, nil
}
//nolint:unparam
func (s *SqlPostStore) SearchPostsForUser(paramsList []*model.SearchParams, userID, teamId string, page, perPage int) (*model.PostSearchResults, error) {
// Since we don't support paging for DB search, we just return nothing for later pages
if page > 0 {
return model.MakePostSearchResults(model.NewPostList(), nil), nil
}
if err := model.IsSearchParamsListValid(paramsList); err != nil {
return nil, err
}
now := model.GetMillis()
pchan := make(chan store.StoreResult, len(paramsList))
var wg sync.WaitGroup
for _, params := range paramsList {
// Deliberately keeping non-alphanumeric characters to
// prevent surprises in UI.
buf, err := json.Marshal(params)
if err != nil {
return nil, err
}
err = s.LogRecentSearch(userID, buf, now)
if err != nil {
return nil, err
}
// remove any unquoted term that contains only non-alphanumeric chars
// ex: abcd "**" && abc >> abcd "**" abc
params.Terms = removeNonAlphaNumericUnquotedTerms(params.Terms, " ")
wg.Add(1)
go func(params *model.SearchParams) {
defer wg.Done()
postList, err := s.search(teamId, userID, params, false, false)
pchan <- store.StoreResult{Data: postList, NErr: err}
}(params)
}
wg.Wait()
close(pchan)
posts := model.NewPostList()
for result := range pchan {
if result.NErr != nil {
return nil, result.NErr
}
data := result.Data.(*model.PostList)
posts.Extend(data)
}
posts.SortByCreateAt()
return model.MakePostSearchResults(posts, nil), nil
}
const lastSearchesLimit = 5
func (s *SqlPostStore) LogRecentSearch(userID string, searchQuery []byte, createAt int64) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
var lastSearchPointer int
var queryStr string
// get search_pointer
// We coalesce to -1 because we want to start from 0
if s.DriverName() == model.DatabaseDriverPostgres {
queryStr = `SELECT COALESCE((props->>'last_search_pointer')::integer, -1)
FROM Users
WHERE Id=?`
} else {
queryStr = `SELECT COALESCE(CAST(JSON_EXTRACT(Props, '$.last_search_pointer') as unsigned), -1)
FROM Users
WHERE Id=?`
}
err = transaction.Get(&lastSearchPointer, queryStr, userID)
if err != nil {
return errors.Wrapf(err, "failed to find last_search_pointer for user=%s", userID)
}
// (ptr+1)%lastSearchesLimit
lastSearchPointer = (lastSearchPointer + 1) % lastSearchesLimit
if s.IsBinaryParamEnabled() {
searchQuery = AppendBinaryFlag(searchQuery)
}
// insert at pointer
query := s.getQueryBuilder().
Insert("RecentSearches").
Columns("UserId", "SearchPointer", "Query", "CreateAt").
Values(userID, lastSearchPointer, searchQuery, createAt)
if s.DriverName() == model.DatabaseDriverPostgres {
query = query.SuffixExpr(sq.Expr("ON CONFLICT (userid, searchpointer) DO UPDATE SET Query = ?, CreateAt = ?", searchQuery, createAt))
} else {
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE Query = ?, CreateAt = ?", searchQuery, createAt))
}
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "log_recent_search_tosql")
}
if _, err2 := transaction.Exec(queryString, args...); err2 != nil {
return errors.Wrapf(err2, "failed to upsert recent_search for user=%s", userID)
}
// write ptr on users prop
if s.DriverName() == model.DatabaseDriverPostgres {
_, err = transaction.Exec(`UPDATE Users
SET Props = jsonb_set(Props, $1, $2)
WHERE Id = $3`, jsonKeyPath("last_search_pointer"), jsonStringVal(strconv.Itoa(lastSearchPointer)), userID)
} else {
_, err = transaction.Exec(`UPDATE Users
SET Props = JSON_SET(Props, ?, ?)
WHERE Id = ?`, "$.last_search_pointer", strconv.Itoa(lastSearchPointer), userID)
}
if err != nil {
return errors.Wrapf(err, "failed to update last_search_pointer for user=%s", userID)
}
if err2 := transaction.Commit(); err2 != nil {
return errors.Wrap(err2, "commit_transaction")
}
return nil
}
func (s *SqlPostStore) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) {
params := [][]byte{}
err := s.GetReplicaX().Select(¶ms, `SELECT query
FROM RecentSearches
WHERE UserId=?
ORDER BY CreateAt DESC`, userID)
if err != nil {
return nil, errors.Wrapf(err, "failed to get recent searches for user=%s", userID)
}
res := make([]*model.SearchParams, len(params))
for i, param := range params {
err = json.Unmarshal(param, &res[i])
if err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal recent search query for user=%s", userID)
}
}
return res, nil
}
func (s *SqlPostStore) GetOldestEntityCreationTime() (int64, error) {
query := s.getQueryBuilder().Select("MIN(min_createat) min_createat").
Suffix(`FROM (
(SELECT MIN(createat) min_createat FROM Posts)
UNION
(SELECT MIN(createat) min_createat FROM Users)
UNION
(SELECT MIN(createat) min_createat FROM Channels)
) entities`)
queryString, args, err := query.ToSql()
if err != nil {
return -1, errors.Wrap(err, "post_tosql")
}
var oldest int64
err = s.GetReplicaX().Get(&oldest, queryString, args...)
if err != nil {
return -1, errors.Wrap(err, "unable to scan oldest entity creation time")
}
return oldest, nil
}
// Deletes a thread and a thread membership if the postId is a root post
func (s *SqlPostStore) permanentDeleteThreads(transaction *sqlxTxWrapper, postId string) error {
if _, err := transaction.Exec("DELETE FROM Threads WHERE PostId = ?", postId); err != nil {
return errors.Wrap(err, "failed to delete Threads")
}
if _, err := transaction.Exec("DELETE FROM ThreadMemberships WHERE PostId = ?", postId); err != nil {
return errors.Wrap(err, "failed to delete ThreadMemberships")
}
return nil
}
// deleteThread marks a thread as deleted at the given time.
func (s *SqlPostStore) deleteThread(transaction *sqlxTxWrapper, postId string, deleteAtTime int64) error {
queryString, args, err := s.getQueryBuilder().
Update("Threads").
Set("ThreadDeleteAt", deleteAtTime).
Where(sq.Eq{"PostId": postId}).
ToSql()
if err != nil {
return errors.Wrapf(err, "failed to create SQL query to mark thread for root post %s as deleted", postId)
}
_, err = transaction.Exec(queryString, args...)
if err != nil {
return errors.Wrapf(err, "failed to mark thread for root post %s as deleted", postId)
}
return nil
}
// updateThreadAfterReplyDeletion decrements the thread reply count and adjusts the participants
// list as necessary.
func (s *SqlPostStore) updateThreadAfterReplyDeletion(transaction *sqlxTxWrapper, rootId string, userId string) error {
if rootId != "" {
queryString, args, err := s.getQueryBuilder().
Select("COUNT(Posts.Id)").
From("Posts").
Where(sq.And{
sq.Eq{"Posts.RootId": rootId},
sq.Eq{"Posts.UserId": userId},
sq.Eq{"Posts.DeleteAt": 0},
}).
ToSql()
if err != nil {
return errors.Wrap(err, "failed to create SQL query to count user's posts")
}
var count int64
err = transaction.Get(&count, queryString, args...)
if err != nil {
return errors.Wrap(err, "failed to count user's posts in thread")
}
// Updating replyCount, and reducing participants if this was the last post in the thread for the user
updateQuery := s.getQueryBuilder().Update("Threads")
if count == 0 {
if s.DriverName() == model.DatabaseDriverPostgres {
updateQuery = updateQuery.Set("Participants", sq.Expr("Participants - ?", userId))
} else {
updateQuery = updateQuery.
Set("Participants", sq.Expr(
`IFNULL(JSON_REMOVE(Participants, JSON_UNQUOTE(JSON_SEARCH(Participants, 'one', ?))), Participants)`, userId,
))
}
}
lastReplyAtSubquery := sq.Select("COALESCE(MAX(CreateAt), 0)").
From("Posts").
Where(sq.Eq{
"RootId": rootId,
"DeleteAt": 0,
})
lastReplyCountSubquery := sq.Select("Count(*)").
From("Posts").
Where(sq.Eq{
"RootId": rootId,
"DeleteAt": 0,
})
updateQueryString, updateArgs, err := updateQuery.
Set("LastReplyAt", lastReplyAtSubquery).
Set("ReplyCount", lastReplyCountSubquery).
Where(sq.And{
sq.Eq{"PostId": rootId},
sq.Gt{"ReplyCount": 0},
}).
ToSql()
if err != nil {
return errors.Wrap(err, "failed to create SQL query to update thread")
}
_, err = transaction.Exec(updateQueryString, updateArgs...)
if err != nil {
return errors.Wrap(err, "failed to update Threads")
}
}
return nil
}
func (s *SqlPostStore) savePostsPriority(transaction *sqlxTxWrapper, posts []*model.Post) error {
for _, post := range posts {
if post.GetPriority() != nil {
postPriority := &model.PostPriority{
PostId: post.Id,
ChannelId: post.ChannelId,
Priority: post.Metadata.Priority.Priority,
RequestedAck: post.Metadata.Priority.RequestedAck,
PersistentNotifications: post.Metadata.Priority.PersistentNotifications,
}
if _, err := transaction.NamedExec(`INSERT INTO PostsPriority (PostId, ChannelId, Priority, RequestedAck, PersistentNotifications) VALUES (:PostId, :ChannelId, :Priority, :RequestedAck, :PersistentNotifications)`, postPriority); err != nil {
return err
}
}
}
return nil
}
func (s *SqlPostStore) updateThreadsFromPosts(transaction *sqlxTxWrapper, posts []*model.Post) error {
postsByRoot := map[string][]*model.Post{}
var rootIds []string
for _, post := range posts {
// skip if post is not a part of a thread
if post.RootId == "" {
continue
}
rootIds = append(rootIds, post.RootId)
postsByRoot[post.RootId] = append(postsByRoot[post.RootId], post)
}
if len(rootIds) == 0 {
return nil
}
threadsByRootsSql, threadsByRootsArgs, err := s.getQueryBuilder().
Select(
"Threads.PostId",
"Threads.ChannelId",
"Threads.ReplyCount",
"Threads.LastReplyAt",
"Threads.Participants",
"COALESCE(Threads.ThreadDeleteAt, 0) AS DeleteAt",
).
From("Threads").
Where(sq.Eq{"Threads.PostId": rootIds}).
ToSql()
if err != nil {
return errors.Wrap(err, "updateThreadsFromPosts_ToSql")
}
threadsByRoots := []*model.Thread{}
err = transaction.Select(&threadsByRoots, threadsByRootsSql, threadsByRootsArgs...)
if err != nil {
return err
}
threadByRoot := map[string]*model.Thread{}
for _, thread := range threadsByRoots {
threadByRoot[thread.PostId] = thread
}
teamIdByChannelId := map[string]string{}
for rootId, posts := range postsByRoot {
if thread, found := threadByRoot[rootId]; !found {
data := []struct {
UserId string
RepliedAt int64
}{}
// calculate participants
if err := transaction.Select(&data, "SELECT Posts.UserId, MAX(Posts.CreateAt) as RepliedAt FROM Posts WHERE Posts.RootId=? AND Posts.DeleteAt=0 GROUP BY Posts.UserId ORDER BY RepliedAt ASC", rootId); err != nil {
return err
}
var participants model.StringArray
for _, item := range data {
participants = append(participants, item.UserId)
}
// calculate reply count
var count int64
err := transaction.Get(&count, "SELECT COUNT(Posts.Id) FROM Posts WHERE Posts.RootId=? And Posts.DeleteAt=0", rootId)
if err != nil {
return err
}
// calculate last reply at
var lastReplyAt int64
err = transaction.Get(&lastReplyAt, "SELECT COALESCE(MAX(Posts.CreateAt), 0) FROM Posts WHERE Posts.RootID=? and Posts.DeleteAt=0", rootId)
if err != nil {
return err
}
channelId := posts[0].ChannelId
teamId, ok := teamIdByChannelId[channelId]
if !ok {
// get teamId for channel
err = transaction.Get(&teamId, "SELECT COALESCE(Channels.TeamId, '') FROM Channels WHERE Channels.Id=?", channelId)
if err != nil {
return err
}
// store teamId for channel for efficiency
teamIdByChannelId[channelId] = teamId
}
// no metadata entry, create one
if _, err := transaction.NamedExec(`INSERT INTO Threads
(PostId, ChannelId, ReplyCount, LastReplyAt, Participants, ThreadTeamId)
VALUES
(:PostId, :ChannelId, :ReplyCount, :LastReplyAt, :Participants, :TeamId)`, &model.Thread{
PostId: rootId,
ChannelId: channelId,
ReplyCount: count,
LastReplyAt: lastReplyAt,
Participants: participants,
TeamId: teamId,
}); err != nil {
return err
}
} else {
// metadata exists, update it
for _, post := range posts {
thread.ReplyCount += 1
if thread.Participants.Contains(post.UserId) {
thread.Participants = thread.Participants.Remove(post.UserId)
}
thread.Participants = append(thread.Participants, post.UserId)
if post.CreateAt > thread.LastReplyAt {
thread.LastReplyAt = post.CreateAt
}
}
if _, err := transaction.NamedExec(`UPDATE Threads
SET ChannelId = :ChannelId,
ReplyCount = :ReplyCount,
LastReplyAt = :LastReplyAt,
Participants = :Participants
WHERE PostId=:PostId`, thread); err != nil {
return err
}
}
}
return nil
}
func (s *SqlPostStore) GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error) {
var botsFilterExpr string
/*
Channel.Name is of the format userId1__userId2.
Using this, self dms, and bot dms can be filtered.
*/
if s.DriverName() == model.DatabaseDriverPostgres {
botsFilterExpr = `SPLIT_PART(Channels.Name, '__', 1) NOT IN (SELECT UserId FROM Bots)
AND SPLIT_PART(Channels.Name, '__', 2) NOT IN (SELECT UserId FROM Bots)
`
} else if s.DriverName() == model.DatabaseDriverMysql {
botsFilterExpr = `SUBSTRING_INDEX(Channels.Name, '__', 1) NOT IN (SELECT UserId FROM Bots)
AND SUBSTRING_INDEX(Channels.Name, '__', -1) NOT IN (SELECT UserId FROM Bots)
`
}
channelSelector := s.getQueryBuilder().Select("Id", "TotalMsgCount").From("Channels").Join("ChannelMembers as cm on cm.ChannelId = Channels.Id").
Where(sq.And{
sq.Expr("Channels.Type = 'D'"),
sq.Eq{"cm.UserId": userID},
sq.NotEq{"Channels.Name": fmt.Sprintf("%s__%s", userID, userID)},
sq.Expr(botsFilterExpr),
})
var aggregator string
if s.DriverName() == model.DatabaseDriverMysql {
aggregator = "group_concat(distinct cm.UserId) as Participants"
} else {
aggregator = "string_agg(distinct cm.UserId, ',') as Participants"
}
topDMsBuilder := s.getQueryBuilder().Select("count(p.Id) as MessageCount", aggregator, "vch.Id as ChannelId").FromSelect(channelSelector, "vch").
Join("ChannelMembers as cm on cm.ChannelId = vch.Id").
Join("Posts as p on p.ChannelId = vch.Id").
Where(sq.And{
sq.Gt{
"p.UpdateAt": since,
},
sq.Eq{
"p.DeleteAt": 0,
},
}).GroupBy("vch.id")
// following where clause filters out all archived DMs with "deleted" users, that has only 1 user-id in Participants column.
archivedDMsFilter := s.getQueryBuilder().Select("MessageCount", "Participants", "ChannelId").FromSelect(topDMsBuilder, "top_dms").
Where(sq.Expr("POSITION(',' IN Participants) > 0"))
archivedDMsFilter = archivedDMsFilter.OrderBy("MessageCount DESC").Limit(uint64(limit + 1)).Offset(uint64(offset))
topDMs := make([]*model.TopDM, 0)
sql, args, err := archivedDMsFilter.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetTopDMsForUserSince_ToSql")
}
err = s.GetReplicaX().Select(&topDMs, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find top DMs for user-id: %s", userID)
}
// fill SecondParticipant column
topDMs, err = postProcessTopDMs(s, userID, topDMs, since)
if err != nil {
return nil, err
}
return model.GetTopDMListWithPagination(topDMs, limit), nil
}
func postProcessTopDMs(s *SqlPostStore, userID string, topDMs []*model.TopDM, since int64) ([]*model.TopDM, error) {
var topDMsFiltered = []*model.TopDM{}
var secondParticipantIds []string
var channelIds []string
// identify second participant in a list of participants
for _, topDM := range topDMs {
participants := strings.Split(topDM.Participants, ",")
var secondParticipantId string
// divide message count by 2, because it's counted twice due to channel memberships being 2 for dms.
topDM.MessageCount = topDM.MessageCount / 2
if participants[0] == userID {
secondParticipantId = participants[1]
} else {
secondParticipantId = participants[0]
}
secondParticipantIds = append(secondParticipantIds, secondParticipantId)
channelIds = append(channelIds, topDM.ChannelId)
}
// get user profiles
users, err := s.User().GetProfileByIds(context.Background(), secondParticipantIds, &store.UserGetByIdsOpts{}, true)
if err != nil {
return nil, errors.Wrapf(err, "failed to get second participants' information")
}
// get outgoing message count for userId
outgoingMessagesQuery := s.getQueryBuilder().Select("ch.Id as ChannelId, count(p.Id) as MessageCount").From("Channels as ch").
Join("Posts as p on p.ChannelId=ch.Id").Where(
sq.And{
sq.Gt{
"p.UpdateAt": since,
},
sq.Eq{
"p.DeleteAt": 0,
},
sq.Eq{
"ch.Id": channelIds,
},
sq.Eq{
"p.UserId": userID,
},
}).GroupBy("ch.Id")
outgoingMessages := make([]*model.OutgoingMessageQueryResult, 0)
sql, args, err := outgoingMessagesQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetTopDMsForUserSince_outgoingMessagesQuery_ToSql")
}
err = s.GetReplicaX().Select(&outgoingMessages, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find top DMs for user-id: %s", userID)
}
// create map of channelId -> MessageCount
outgoingMessagesMap := make(map[string]int)
for _, outgoingMessage := range outgoingMessages {
outgoingMessagesMap[outgoingMessage.ChannelId] = outgoingMessage.MessageCount
}
// create map of userId -> User
usersMap := make(map[string]*model.User)
for _, user := range users {
usersMap[user.Id] = user
}
for index, topDM := range topDMs {
if secondParticipantIds[index] == "-1" {
return nil, errors.Wrapf(err, "failed to find second user for topDM: %s", userID)
}
user := usersMap[secondParticipantIds[index]]
topDM.SecondParticipant = &model.TopDMInsightUserInformation{
InsightUserInformation: model.InsightUserInformation{
Id: user.Id,
LastPictureUpdate: user.LastPictureUpdate,
FirstName: user.FirstName,
LastName: user.LastName,
Username: user.Username,
NickName: user.Nickname,
},
Position: user.Position,
}
topDM.OutgoingMessageCount = int64(outgoingMessagesMap[topDM.ChannelId])
topDMsFiltered = append(topDMsFiltered, topDM)
}
return topDMsFiltered, nil
}
func (s *SqlPostStore) SetPostReminder(reminder *model.PostReminder) error {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
sql := `SELECT EXISTS (SELECT 1 FROM Posts WHERE Id=?)`
var exist bool
err = transaction.Get(&exist, sql, reminder.PostId)
if err != nil {
return errors.Wrap(err, "failed to check for post")
}
if !exist {
return store.NewErrNotFound("Post", reminder.PostId)
}
query := s.getQueryBuilder().
Insert("PostReminders").
Columns("PostId", "UserId", "TargetTime").
Values(reminder.PostId, reminder.UserId, reminder.TargetTime)
if s.DriverName() == model.DatabaseDriverMysql {
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE TargetTime = ?", reminder.TargetTime))
} else {
query = query.SuffixExpr(sq.Expr("ON CONFLICT (postid, userid) DO UPDATE SET TargetTime = ?", reminder.TargetTime))
}
sql, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "setPostReminder_tosql")
}
if _, err2 := transaction.Exec(sql, args...); err2 != nil {
return errors.Wrap(err2, "failed to insert post reminder")
}
if err = transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (s *SqlPostStore) GetPostReminders(now int64) (_ []*model.PostReminder, err error) {
reminders := []*model.PostReminder{}
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
err = transaction.Select(&reminders, `SELECT PostId, UserId
FROM PostReminders
WHERE TargetTime < ?`, now)
if err != nil && err != sql.ErrNoRows {
return nil, errors.Wrap(err, "failed to get post reminders")
}
if err == sql.ErrNoRows {
// No need to execute delete statement if there's nothing to delete.
return reminders, nil
}
// Postgres supports RETURNING * in a DELETE statement, but MySQL doesn't.
// So we are stuck with 2 queries. Not taking separate paths for Postgres
// and MySQL for simplicity.
_, err = transaction.Exec(`DELETE from PostReminders WHERE TargetTime < ?`, now)
if err != nil {
return nil, errors.Wrap(err, "failed to delete post reminders")
}
if err = transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return reminders, nil
}
func (s *SqlPostStore) GetPostReminderMetadata(postID string) (*store.PostReminderMetadata, error) {
meta := &store.PostReminderMetadata{}
err := s.GetReplicaX().Get(meta, `SELECT c.id as ChannelId,
COALESCE(t.name, '') as TeamName,
u.locale as UserLocale, u.username as Username
FROM Posts p
JOIN Channels c ON p.ChannelId=c.Id
LEFT JOIN Teams t ON c.TeamId=t.Id
JOIN Users u ON p.UserId=u.Id
AND p.Id=?`, postID)
if err != nil {
return nil, errors.Wrapf(err, "failed to get post reminder metadata: postId %s", postID)
}
return meta, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SqlPreferenceStore struct {
*SqlStore
}
func newSqlPreferenceStore(sqlStore *SqlStore) store.PreferenceStore {
s := &SqlPreferenceStore{sqlStore}
return s
}
func (s SqlPreferenceStore) deleteUnusedFeatures() {
mlog.Debug("Deleting any unused pre-release features")
sql, args, err := s.getQueryBuilder().
Delete("Preferences").
Where(sq.Eq{"Category": model.PreferenceCategoryAdvancedSettings}).
Where(sq.Eq{"Value": "false"}).
Where(sq.Like{"Name": store.FeatureTogglePrefix + "%"}).ToSql()
if err != nil {
mlog.Warn("Could not build sql query to delete unused features", mlog.Err(err))
}
if _, err = s.GetMasterX().Exec(sql, args...); err != nil {
mlog.Warn("Failed to delete unused features", mlog.Err(err))
}
}
func (s SqlPreferenceStore) Save(preferences model.Preferences) (err error) {
// wrap in a transaction so that if one fails, everything fails
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
for _, preference := range preferences {
preference := preference
if upsertErr := s.saveTx(transaction, &preference); upsertErr != nil {
return upsertErr
}
}
if err := transaction.Commit(); err != nil {
// don't need to rollback here since the transaction is already closed
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (s SqlPreferenceStore) save(transaction *sqlxTxWrapper, preference *model.Preference) error {
preference.PreUpdate()
if err := preference.IsValid(); err != nil {
return err
}
query := s.getQueryBuilder().
Insert("Preferences").
Columns("UserId", "Category", "Name", "Value").
Values(preference.UserId, preference.Category, preference.Name, preference.Value)
if s.DriverName() == model.DatabaseDriverMysql {
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE Value = ?", preference.Value))
} else if s.DriverName() == model.DatabaseDriverPostgres {
query = query.SuffixExpr(sq.Expr("ON CONFLICT (userid, category, name) DO UPDATE SET Value = ?", preference.Value))
} else {
return store.NewErrNotImplemented("failed to update preference because of missing driver")
}
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "failed to generate sqlquery")
}
if _, err = transaction.Exec(queryString, args...); err != nil {
return errors.Wrap(err, "failed to save Preference")
}
return nil
}
func (s SqlPreferenceStore) saveTx(transaction *sqlxTxWrapper, preference *model.Preference) error {
preference.PreUpdate()
if err := preference.IsValid(); err != nil {
return err
}
query := s.getQueryBuilder().
Insert("Preferences").
Columns("UserId", "Category", "Name", "Value").
Values(preference.UserId, preference.Category, preference.Name, preference.Value)
if s.DriverName() == model.DatabaseDriverMysql {
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE Value = ?", preference.Value))
} else if s.DriverName() == model.DatabaseDriverPostgres {
query = query.SuffixExpr(sq.Expr("ON CONFLICT (userid, category, name) DO UPDATE SET Value = ?", preference.Value))
} else {
return store.NewErrNotImplemented("failed to update preference because of missing driver")
}
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "failed to generate sqlquery")
}
if _, err = transaction.Exec(queryString, args...); err != nil {
return errors.Wrap(err, "failed to save Preference")
}
return nil
}
func (s SqlPreferenceStore) Get(userId string, category string, name string) (*model.Preference, error) {
var preference model.Preference
query, args, err := s.getQueryBuilder().
Select("*").
From("Preferences").
Where(sq.Eq{"UserId": userId}).
Where(sq.Eq{"Category": category}).
Where(sq.Eq{"Name": name}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "could not build sql query to get preference")
}
if err = s.GetReplicaX().Get(&preference, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Preference with userId=%s, category=%s, name=%s", userId, category, name)
}
return &preference, nil
}
func (s SqlPreferenceStore) GetCategoryAndName(category string, name string) (model.Preferences, error) {
var preferences model.Preferences
query, args, err := s.getQueryBuilder().
Select("*").
From("Preferences").
Where(sq.Eq{"Category": category}).
Where(sq.Eq{"Name": name}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "could not build sql query to get preference")
}
if err = s.GetReplicaX().Select(&preferences, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Preference with category=%s, name=%s", category, name)
}
return preferences, nil
}
func (s SqlPreferenceStore) GetCategory(userId string, category string) (model.Preferences, error) {
var preferences model.Preferences
query, args, err := s.getQueryBuilder().
Select("*").
From("Preferences").
Where(sq.Eq{"UserId": userId}).
Where(sq.Eq{"Category": category}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "could not build sql query to get preference")
}
if err = s.GetReplicaX().Select(&preferences, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Preference with userId=%s, category=%s", userId, category)
}
return preferences, nil
}
func (s SqlPreferenceStore) GetAll(userId string) (model.Preferences, error) {
var preferences model.Preferences
query, args, err := s.getQueryBuilder().
Select("*").
From("Preferences").
Where(sq.Eq{"UserId": userId}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "could not build sql query to get preference")
}
if err = s.GetReplicaX().Select(&preferences, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Preference with userId=%s", userId)
}
return preferences, nil
}
func (s SqlPreferenceStore) PermanentDeleteByUser(userId string) error {
sql, args, err := s.getQueryBuilder().
Delete("Preferences").
Where(sq.Eq{"UserId": userId}).ToSql()
if err != nil {
return errors.Wrap(err, "could not build sql query to get delete preference by user")
}
if _, err := s.GetMasterX().Exec(sql, args...); err != nil {
return errors.Wrapf(err, "failed to delete Preference with userId=%s", userId)
}
return nil
}
func (s SqlPreferenceStore) Delete(userId, category, name string) error {
sql, args, err := s.getQueryBuilder().
Delete("Preferences").
Where(sq.Eq{"UserId": userId}).
Where(sq.Eq{"Category": category}).
Where(sq.Eq{"Name": name}).ToSql()
if err != nil {
return errors.Wrap(err, "could not build sql query to get delete preference")
}
if _, err = s.GetMasterX().Exec(sql, args...); err != nil {
return errors.Wrapf(err, "failed to delete Preference with userId=%s, category=%s and name=%s", userId, category, name)
}
return nil
}
func (s SqlPreferenceStore) DeleteCategory(userId string, category string) error {
sql, args, err := s.getQueryBuilder().
Delete("Preferences").
Where(sq.Eq{"UserId": userId}).
Where(sq.Eq{"Category": category}).ToSql()
if err != nil {
return errors.Wrap(err, "could not build sql query to get delete preference by category")
}
if _, err = s.GetMasterX().Exec(sql, args...); err != nil {
return errors.Wrapf(err, "failed to delete Preference with userId=%s and category=%s", userId, category)
}
return nil
}
func (s SqlPreferenceStore) DeleteCategoryAndName(category string, name string) error {
sql, args, err := s.getQueryBuilder().
Delete("Preferences").
Where(sq.Eq{"Name": name}).
Where(sq.Eq{"Category": category}).ToSql()
if err != nil {
return errors.Wrap(err, "could not build sql query to get delete preference by category and name")
}
if _, err = s.GetMasterX().Exec(sql, args...); err != nil {
return errors.Wrapf(err, "failed to delete Preference with category=%s and name=%s", category, name)
}
return nil
}
// DeleteOrphanedRows removes entries from Preferences (flagged post) when a
// corresponding post no longer exists.
func (s *SqlPreferenceStore) DeleteOrphanedRows(limit int) (deleted int64, err error) {
// We need the extra level of nesting to deal with MySQL's locking
const query = `
DELETE FROM Preferences WHERE Name IN (
SELECT * FROM (
SELECT Preferences.Name FROM Preferences
LEFT JOIN Posts ON Preferences.Name = Posts.Id
WHERE Posts.Id IS NULL AND Category = ?
LIMIT ?
) AS A
)`
result, err := s.GetMasterX().Exec(query, model.PreferenceCategoryFlaggedPost, limit)
if err != nil {
return
}
deleted, err = result.RowsAffected()
return
}
func (s SqlPreferenceStore) CleanupFlagsBatch(limit int64) (int64, error) {
if limit < 0 {
// uint64 does not throw an error, it overflows if it is negative.
// it is better to manually check here, or change the function type to uint64
return int64(0), errors.Errorf("Received a negative limit")
}
nameInQ, nameInArgs, err := sq.Select("*").
FromSelect(
sq.Select("Preferences.Name").
From("Preferences").
LeftJoin("Posts ON Preferences.Name = Posts.Id").
Where(sq.Eq{"Preferences.Category": model.PreferenceCategoryFlaggedPost}).
Where(sq.Eq{"Posts.Id": nil}).
Limit(uint64(limit)),
"t").
ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "could not build nested sql query to delete preference")
}
query, args, err := s.getQueryBuilder().Delete("Preferences").
Where(sq.Eq{"Category": model.PreferenceCategoryFlaggedPost}).
Where(sq.Expr("name IN ("+nameInQ+")", nameInArgs...)).
ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "could not build sql query to delete preference")
}
sqlResult, err := s.GetMasterX().Exec(query, args...)
if err != nil {
return int64(0), errors.Wrap(err, "failed to delete Preference")
}
rowsAffected, err := sqlResult.RowsAffected()
if err != nil {
return int64(0), errors.Wrap(err, "unable to get rows affected")
}
return rowsAffected, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"time"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlProductNoticesStore struct {
*SqlStore
}
func newSqlProductNoticesStore(sqlStore *SqlStore) store.ProductNoticesStore {
return &SqlProductNoticesStore{sqlStore}
}
func (s SqlProductNoticesStore) Clear(notices []string) error {
sql, args, err := s.getQueryBuilder().Delete("ProductNoticeViewState").Where(sq.Eq{"NoticeId": notices}).ToSql()
if err != nil {
return errors.Wrap(err, "product_notice_view_state_tosql")
}
if _, err := s.GetMasterX().Exec(sql, args...); err != nil {
return errors.Wrap(err, "failed to delete records from ProductNoticeViewState")
}
return nil
}
func (s SqlProductNoticesStore) ClearOldNotices(currentNotices model.ProductNotices) error {
var notices []string
for _, currentNotice := range currentNotices {
notices = append(notices, currentNotice.ID)
}
sql, args, err := s.getQueryBuilder().Delete("ProductNoticeViewState").Where(sq.NotEq{"NoticeId": notices}).ToSql()
if err != nil {
return errors.Wrap(err, "product_notice_view_state_tosql")
}
if _, err := s.GetMasterX().Exec(sql, args...); err != nil {
return errors.Wrapf(err, "failed to delete records from ProductNoticeViewState")
}
return nil
}
func (s SqlProductNoticesStore) View(userId string, notices []string) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
noticeStates := []model.ProductNoticeViewState{}
sql, args, err := s.getQueryBuilder().
Select("*").
From("ProductNoticeViewState").
Where(sq.And{sq.Eq{"UserId": userId}, sq.Eq{"NoticeId": notices}}).
ToSql()
if err != nil {
return errors.Wrap(err, "View_ToSql")
}
if err := transaction.Select(¬iceStates, sql, args...); err != nil {
return errors.Wrapf(err, "failed to get ProductNoticeViewState with userId=%s", userId)
}
now := time.Now().UTC().Unix()
// update existing records
for i := range noticeStates {
noticeStates[i].Viewed += 1
noticeStates[i].Timestamp = now
if _, err := transaction.NamedExec(`UPDATE ProductNoticeViewState
SET Viewed=:Viewed, Timestamp=:Timestamp WHERE UserId=:UserId AND NoticeId=:NoticeId`, ¬iceStates[i]); err != nil {
return errors.Wrapf(err, "failed to update ProductNoticeViewState")
}
}
// add new ones
haveNoticeState := func(n string) bool {
for _, ns := range noticeStates {
if ns.NoticeId == n {
return true
}
}
return false
}
for _, noticeId := range notices {
if !haveNoticeState(noticeId) {
productNoticeViewState := &model.ProductNoticeViewState{
UserId: userId,
NoticeId: noticeId,
Viewed: 1,
Timestamp: now,
}
if _, err := transaction.NamedExec(`INSERT INTO ProductNoticeViewState (UserId, NoticeId, Viewed, Timestamp)
VALUES (:UserId, :NoticeId, :Viewed, :Timestamp)`, productNoticeViewState); err != nil {
return errors.Wrapf(err, "failed to insert ProductNoticeViewState")
}
}
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (s SqlProductNoticesStore) GetViews(userId string) ([]model.ProductNoticeViewState, error) {
noticeStates := []model.ProductNoticeViewState{}
sql, args, err := s.getQueryBuilder().Select("*").From("ProductNoticeViewState").Where(sq.Eq{"UserId": userId}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "product_notice_view_state_tosql")
}
if err := s.GetReplicaX().Select(¬iceStates, sql, args...); err != nil {
return nil, errors.Wrapf(err, "failed to get ProductNoticeViewState with userId=%s", userId)
}
return noticeStates, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
sq "github.com/mattermost/squirrel"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/pkg/errors"
)
type SqlReactionStore struct {
*SqlStore
}
func newSqlReactionStore(sqlStore *SqlStore) store.ReactionStore {
return &SqlReactionStore{sqlStore}
}
func (s *SqlReactionStore) Save(reaction *model.Reaction) (re *model.Reaction, err error) {
reaction.PreSave()
if err := reaction.IsValid(); err != nil {
return nil, err
}
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
if reaction.ChannelId == "" {
// get channelId, if not already populated
var channelIds []string
var args []interface{}
query := "SELECT ChannelId from Posts where Id = ?"
args = append(args, reaction.PostId)
err = transaction.Select(&channelIds, query, args...)
if err != nil {
return nil, errors.Wrap(err, "failed while getting channelId from Posts")
}
reaction.ChannelId = channelIds[0]
}
err = s.saveReactionAndUpdatePost(transaction, reaction)
if err != nil {
// We don't consider duplicated save calls as an error
if !IsUniqueConstraintError(err, []string{"reactions_pkey", "PRIMARY"}) {
return nil, errors.Wrap(err, "failed while saving reaction or updating post")
}
} else {
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
}
return reaction, nil
}
func (s *SqlReactionStore) Delete(reaction *model.Reaction) (re *model.Reaction, err error) {
reaction.PreUpdate()
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
if err := deleteReactionAndUpdatePost(transaction, reaction); err != nil {
return nil, errors.Wrap(err, "deleteReactionAndUpdatePost")
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return reaction, nil
}
// GetForPost returns all reactions associated with `postId` that are not deleted.
func (s *SqlReactionStore) GetForPost(postId string, allowFromCache bool) ([]*model.Reaction, error) {
queryString, args, err := s.getQueryBuilder().
Select("UserId", "PostId", "EmojiName", "CreateAt", "COALESCE(UpdateAt, CreateAt) As UpdateAt",
"COALESCE(DeleteAt, 0) As DeleteAt", "RemoteId", "ChannelId").
From("Reactions").
Where(sq.Eq{"PostId": postId}).
Where(sq.Eq{"COALESCE(DeleteAt, 0)": 0}).
OrderBy("CreateAt").
ToSql()
if err != nil {
return nil, errors.Wrap(err, "reactions_getforpost_tosql")
}
var reactions []*model.Reaction
if err := s.GetReplicaX().Select(&reactions, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to get Reactions with postId=%s", postId)
}
return reactions, nil
}
// GetForPostSince returns all reactions associated with `postId` updated after `since`.
func (s *SqlReactionStore) GetForPostSince(postId string, since int64, excludeRemoteId string, inclDeleted bool) ([]*model.Reaction, error) {
query := s.getQueryBuilder().
Select("UserId", "PostId", "EmojiName", "CreateAt", "COALESCE(UpdateAt, CreateAt) As UpdateAt",
"COALESCE(DeleteAt, 0) As DeleteAt", "RemoteId").
From("Reactions").
Where(sq.Eq{"PostId": postId}).
Where(sq.Gt{"UpdateAt": since})
if excludeRemoteId != "" {
query = query.Where(sq.NotEq{"COALESCE(RemoteId, '')": excludeRemoteId})
}
if !inclDeleted {
query = query.Where(sq.Eq{"COALESCE(DeleteAt, 0)": 0})
}
query.OrderBy("CreateAt")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "reactions_getforpostsince_tosql")
}
var reactions []*model.Reaction
if err := s.GetReplicaX().Select(&reactions, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find reactions")
}
return reactions, nil
}
func (s *SqlReactionStore) BulkGetForPosts(postIds []string) ([]*model.Reaction, error) {
placeholder, values := constructArrayArgs(postIds)
var reactions []*model.Reaction
if err := s.GetReplicaX().Select(&reactions,
`SELECT
UserId,
PostId,
EmojiName,
CreateAt,
COALESCE(UpdateAt, CreateAt) As UpdateAt,
COALESCE(DeleteAt, 0) As DeleteAt,
RemoteId,
ChannelId
FROM
Reactions
WHERE
PostId IN `+placeholder+` AND COALESCE(DeleteAt, 0) = 0
ORDER BY
CreateAt`, values...); err != nil {
return nil, errors.Wrap(err, "failed to get Reactions")
}
return reactions, nil
}
func (s *SqlReactionStore) DeleteAllWithEmojiName(emojiName string) error {
var reactions []*model.Reaction
now := model.GetMillis()
if err := s.GetReplicaX().Select(&reactions,
`SELECT
UserId,
PostId,
EmojiName,
CreateAt,
COALESCE(UpdateAt, CreateAt) As UpdateAt,
COALESCE(DeleteAt, 0) As DeleteAt,
RemoteId
FROM
Reactions
WHERE
EmojiName = ? AND COALESCE(DeleteAt, 0) = 0`, emojiName); err != nil {
return errors.Wrapf(err, "failed to get Reactions with emojiName=%s", emojiName)
}
_, err := s.GetMasterX().Exec(
`UPDATE
Reactions
SET
UpdateAt = ?, DeleteAt = ?
WHERE
EmojiName = ? AND COALESCE(DeleteAt, 0) = 0`, now, now, emojiName)
if err != nil {
return errors.Wrapf(err, "failed to delete Reactions with emojiName=%s", emojiName)
}
for _, reaction := range reactions {
reaction := reaction
_, err := s.GetMasterX().Exec(UpdatePostHasReactionsOnDeleteQuery, now, reaction.PostId, reaction.PostId)
if err != nil {
mlog.Warn("Unable to update Post.HasReactions while removing reactions",
mlog.String("post_id", reaction.PostId),
mlog.Err(err))
}
}
return nil
}
// DeleteOrphanedRows removes entries from Reactions when a corresponding post no longer exists.
func (s *SqlReactionStore) DeleteOrphanedRows(limit int) (deleted int64, err error) {
// We need the extra level of nesting to deal with MySQL's locking
const query = `
DELETE FROM Reactions WHERE PostId IN (
SELECT * FROM (
SELECT PostId FROM Reactions
LEFT JOIN Posts ON Reactions.PostId = Posts.Id
WHERE Posts.Id IS NULL
LIMIT ?
) AS A
)`
result, err := s.GetMasterX().Exec(query, limit)
if err != nil {
return
}
deleted, err = result.RowsAffected()
return
}
func (s *SqlReactionStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
var query string
if s.DriverName() == "postgres" {
query = "DELETE from Reactions WHERE CreateAt = any (array (SELECT CreateAt FROM Reactions WHERE CreateAt < ? LIMIT ?))"
} else {
query = "DELETE from Reactions WHERE CreateAt < ? LIMIT ?"
}
sqlResult, err := s.GetMasterX().Exec(query, endTime, limit)
if err != nil {
return 0, errors.Wrap(err, "failed to delete Reactions")
}
rowsAffected, err := sqlResult.RowsAffected()
if err != nil {
return 0, errors.Wrap(err, "unable to get rows affected for deleted Reactions")
}
return rowsAffected, nil
}
// GetTopForTeamSince returns the instance counts of the following Reactions sets:
// a) those created by anyone in private channels in the given user's membership graph on the given team, and
// b) those created by anyone in public channels on the given team.
func (s *SqlReactionStore) GetTopForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
reactions := make([]*model.TopReaction, 0)
query := `
SELECT
EmojiName,
sum(EmojiCount) AS Count
FROM ((
SELECT
EmojiName,
count(EmojiName) AS EmojiCount,
Reactions.DeleteAt AS DeleteAt,
Reactions.CreateAt AS CreateAt
FROM
ChannelMembers
INNER JOIN Channels ON ChannelMembers.ChannelId = Channels.Id
INNER JOIN Reactions ON Channels.Id = Reactions.ChannelId
WHERE
ChannelMembers.UserId = ?
AND Channels.Type = 'P'
AND Channels.TeamId = ?
GROUP BY
Reactions.EmojiName,
Reactions.DeleteAt,
Reactions.CreateAt)
UNION ALL (
SELECT
EmojiName,
count(EmojiName) AS EmojiCount,
Reactions.DeleteAt AS DeleteAt,
Reactions.CreateAt AS CreateAt
FROM
Reactions
INNER JOIN PublicChannels ON Reactions.ChannelId = PublicChannels.Id
WHERE
PublicChannels.TeamId = ?
GROUP BY
Reactions.EmojiName,
Reactions.DeleteAt,
Reactions.CreateAt)) AS A
WHERE
DeleteAt = 0
AND CreateAt > ?
GROUP BY
EmojiName
ORDER BY
Count DESC,
EmojiName ASC
LIMIT ?
OFFSET ?`
if err := s.GetReplicaX().Select(&reactions, query, userID, teamID, teamID, since, limit+1, offset); err != nil {
return nil, errors.Wrap(err, "failed to get top Reactions")
}
return model.GetTopReactionListWithPagination(reactions, limit), nil
}
// GetTopForUserSince returns the instance counts of the following Reactions sets:
// a) those created by the given user in any channel type on the given team (across the workspace if no team is given), and
// b) those created by the given user in DM or group channels.
func (s *SqlReactionStore) GetTopForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
reactions := make([]*model.TopReaction, 0)
var args []any
var query string
if teamID != "" {
query = `
SELECT
EmojiName,
count(EmojiName) AS Count
FROM
Reactions
INNER JOIN Channels ON Channels.Id = Reactions.ChannelId
WHERE
Reactions.DeleteAt = 0
AND Reactions.UserId = ?
AND (Channels.TeamId = ? OR Channels.Type = 'D' OR Channels.Type = 'G')
AND Reactions.CreateAt > ?
GROUP BY
EmojiName
ORDER BY
Count DESC,
EmojiName ASC
LIMIT ?
OFFSET ?`
args = []any{userID, teamID, since, limit + 1, offset}
} else {
query = `
SELECT
EmojiName,
count(EmojiName) AS Count
FROM
Reactions
WHERE
Reactions.DeleteAt = 0
AND Reactions.UserId = ?
AND Reactions.CreateAt > ?
GROUP BY
Reactions.EmojiName
ORDER BY
Count DESC,
EmojiName ASC
LIMIT ?
OFFSET ?`
args = []any{userID, since, limit + 1, offset}
}
if err := s.GetReplicaX().Select(&reactions, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to get top Reactions")
}
return model.GetTopReactionListWithPagination(reactions, limit), nil
}
func (s *SqlReactionStore) saveReactionAndUpdatePost(transaction *sqlxTxWrapper, reaction *model.Reaction) error {
reaction.DeleteAt = 0
if s.DriverName() == model.DatabaseDriverMysql {
if _, err := transaction.NamedExec(
`INSERT INTO
Reactions
(UserId, PostId, EmojiName, CreateAt, UpdateAt, DeleteAt, RemoteId, ChannelId)
VALUES
(:UserId, :PostId, :EmojiName, :CreateAt, :UpdateAt, :DeleteAt, :RemoteId, :ChannelId)
ON DUPLICATE KEY UPDATE
UpdateAt = :UpdateAt, DeleteAt = :DeleteAt, RemoteId = :RemoteId, ChannelId = :ChannelId`, reaction); err != nil {
return err
}
} else if s.DriverName() == model.DatabaseDriverPostgres {
if _, err := transaction.NamedExec(
`INSERT INTO
Reactions
(UserId, PostId, EmojiName, CreateAt, UpdateAt, DeleteAt, RemoteId, ChannelId)
VALUES
(:UserId, :PostId, :EmojiName, :CreateAt, :UpdateAt, :DeleteAt, :RemoteId, :ChannelId)
ON CONFLICT (UserId, PostId, EmojiName)
DO UPDATE SET UpdateAt = :UpdateAt, DeleteAt = :DeleteAt, RemoteId = :RemoteId, ChannelId = :ChannelId`, reaction); err != nil {
return err
}
}
return updatePostForReactionsOnInsert(transaction, reaction.PostId)
}
func deleteReactionAndUpdatePost(transaction *sqlxTxWrapper, reaction *model.Reaction) error {
if _, err := transaction.Exec(
`UPDATE
Reactions
SET
UpdateAt = ?, DeleteAt = ?, RemoteId = ?
WHERE
PostId = ? AND
UserId = ? AND
EmojiName = ?`, reaction.UpdateAt, reaction.UpdateAt, reaction.RemoteId, reaction.PostId, reaction.UserId, reaction.EmojiName); err != nil {
return err
}
return updatePostForReactionsOnDelete(transaction, reaction.PostId)
}
const (
UpdatePostHasReactionsOnDeleteQuery = `UPDATE
Posts
SET
UpdateAt = ?,
HasReactions = (SELECT count(0) > 0 FROM Reactions WHERE PostId = ? AND COALESCE(DeleteAt, 0) = 0)
WHERE
Id = ?`
)
func updatePostForReactionsOnDelete(transaction *sqlxTxWrapper, postId string) error {
updateAt := model.GetMillis()
_, err := transaction.Exec(UpdatePostHasReactionsOnDeleteQuery, updateAt, postId, postId)
return err
}
func updatePostForReactionsOnInsert(transaction *sqlxTxWrapper, postId string) error {
_, err := transaction.Exec(
`UPDATE
Posts
SET
HasReactions = True,
UpdateAt = ?
WHERE
Id = ?`,
model.GetMillis(),
postId,
)
return err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"fmt"
"strings"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type sqlRemoteClusterStore struct {
*SqlStore
}
func newSqlRemoteClusterStore(sqlStore *SqlStore) store.RemoteClusterStore {
return &sqlRemoteClusterStore{sqlStore}
}
func (s sqlRemoteClusterStore) Save(remoteCluster *model.RemoteCluster) (*model.RemoteCluster, error) {
remoteCluster.PreSave()
if err := remoteCluster.IsValid(); err != nil {
return nil, err
}
query := `INSERT INTO RemoteClusters
(RemoteId, RemoteTeamId, Name, DisplayName, SiteURL, CreateAt,
LastPingAt, Token, RemoteToken, Topics, CreatorId)
VALUES
(:RemoteId, :RemoteTeamId, :Name, :DisplayName, :SiteURL, :CreateAt,
:LastPingAt, :Token, :RemoteToken, :Topics, :CreatorId)`
if _, err := s.GetMasterX().NamedExec(query, remoteCluster); err != nil {
return nil, errors.Wrap(err, "failed to save RemoteCluster")
}
return remoteCluster, nil
}
func (s sqlRemoteClusterStore) Update(remoteCluster *model.RemoteCluster) (*model.RemoteCluster, error) {
remoteCluster.PreUpdate()
if err := remoteCluster.IsValid(); err != nil {
return nil, err
}
query := `UPDATE RemoteClusters
SET Token = :Token,
RemoteTeamId = :RemoteTeamId,
CreateAt = :CreateAt,
LastPingAt = :LastPingAt,
RemoteToken = :RemoteToken,
CreatorId = :CreatorId,
DisplayName = :DisplayName,
SiteURL = :SiteURL,
Topics = :Topics
WHERE RemoteId = :RemoteId AND Name = :Name`
if _, err := s.GetMasterX().NamedExec(query, remoteCluster); err != nil {
return nil, errors.Wrap(err, "failed to update RemoteCluster")
}
return remoteCluster, nil
}
func (s sqlRemoteClusterStore) Delete(remoteId string) (bool, error) {
squery, args, err := s.getQueryBuilder().
Delete("RemoteClusters").
Where(sq.Eq{"RemoteId": remoteId}).
ToSql()
if err != nil {
return false, errors.Wrap(err, "delete_remote_cluster_tosql")
}
result, err := s.GetMasterX().Exec(squery, args...)
if err != nil {
return false, errors.Wrap(err, "failed to delete RemoteCluster")
}
count, err := result.RowsAffected()
if err != nil {
return false, errors.Wrap(err, "failed to determine rows affected")
}
return count > 0, nil
}
func (s sqlRemoteClusterStore) Get(remoteId string) (*model.RemoteCluster, error) {
query := s.getQueryBuilder().
Select("*").
From("RemoteClusters").
Where(sq.Eq{"RemoteId": remoteId})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "remote_cluster_get_tosql")
}
var rc model.RemoteCluster
if err := s.GetReplicaX().Get(&rc, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find RemoteCluster")
}
return &rc, nil
}
func (s sqlRemoteClusterStore) GetAll(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
query := s.getQueryBuilder().
Select("rc.*").
From("RemoteClusters rc")
if filter.InChannel != "" {
query = query.Where("rc.RemoteId IN (SELECT scr.RemoteId FROM SharedChannelRemotes scr WHERE scr.ChannelId = ?)", filter.InChannel)
}
if filter.NotInChannel != "" {
query = query.Where("rc.RemoteId NOT IN (SELECT scr.RemoteId FROM SharedChannelRemotes scr WHERE scr.ChannelId = ?)", filter.NotInChannel)
}
if filter.ExcludeOffline {
query = query.Where(sq.Gt{"rc.LastPingAt": model.GetMillis() - model.RemoteOfflineAfterMillis})
}
if filter.CreatorId != "" {
query = query.Where(sq.Eq{"rc.CreatorId": filter.CreatorId})
}
if filter.OnlyConfirmed {
query = query.Where(sq.NotEq{"rc.SiteURL": ""})
}
if filter.Topic != "" {
trimmed := strings.TrimSpace(filter.Topic)
if trimmed == "" || trimmed == "*" {
return nil, errors.New("invalid topic")
}
queryTopic := fmt.Sprintf("%% %s %%", trimmed)
query = query.Where(sq.Or{sq.Like{"rc.Topics": queryTopic}, sq.Eq{"rc.Topics": "*"}})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "remote_cluster_getall_tosql")
}
list := []*model.RemoteCluster{}
if err := s.GetReplicaX().Select(&list, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find RemoteClusters")
}
return list, nil
}
func (s sqlRemoteClusterStore) UpdateTopics(remoteClusterid string, topics string) (*model.RemoteCluster, error) {
rc, err := s.Get(remoteClusterid)
if err != nil {
return nil, err
}
rc.Topics = topics
rc.PreUpdate()
query := `UPDATE RemoteClusters
SET Topics = :Topics
WHERE RemoteId = :RemoteId`
if _, err = s.GetMasterX().NamedExec(query, rc); err != nil {
return nil, err
}
return rc, nil
}
func (s sqlRemoteClusterStore) SetLastPingAt(remoteClusterId string) error {
query := s.getQueryBuilder().
Update("RemoteClusters").
Set("LastPingAt", model.GetMillis()).
Where(sq.Eq{"RemoteId": remoteClusterId})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "remote_cluster_tosql")
}
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrap(err, "failed to update RemoteCluster")
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"strconv"
"strings"
"github.com/go-sql-driver/mysql"
"github.com/lib/pq"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlRetentionPolicyStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
}
func newSqlRetentionPolicyStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.RetentionPolicyStore {
return &SqlRetentionPolicyStore{
SqlStore: sqlStore,
metrics: metrics,
}
}
// executePossiblyEmptyQuery only executes the query if it is non-empty. This helps avoid
// having to check for MySQL, which, unlike Postgres, does not allow empty queries.
func executePossiblyEmptyQuery(txn *sqlxTxWrapper, query string, args ...any) (sql.Result, error) {
if query == "" {
return nil, nil
}
return txn.Exec(query, args...)
}
func (s *SqlRetentionPolicyStore) Save(policy *model.RetentionPolicyWithTeamAndChannelIDs) (_ *model.RetentionPolicyWithTeamAndChannelCounts, err error) {
// Strategy:
// 1. Insert new policy
// 2. Insert new channels into policy
// 3. Insert new teams into policy
if err = s.checkTeamsExist(policy.TeamIDs); err != nil {
return nil, err
}
if err = s.checkChannelsExist(policy.ChannelIDs); err != nil {
return nil, err
}
policy.ID = model.NewId()
policyInsertQuery, policyInsertArgs, err := s.getQueryBuilder().
Insert("RetentionPolicies").
Columns("Id", "DisplayName", "PostDuration").
Values(policy.ID, policy.DisplayName, policy.PostDurationDays).
ToSql()
if err != nil {
return nil, err
}
channelsInsertQuery, channelsInsertArgs, err := s.buildInsertRetentionPoliciesChannelsQuery(policy.ID, policy.ChannelIDs)
if err != nil {
return nil, err
}
teamsInsertQuery, teamsInsertArgs, err := s.buildInsertRetentionPoliciesTeamsQuery(policy.ID, policy.TeamIDs)
if err != nil {
return nil, err
}
queryString, args, err := s.buildGetPolicyQuery(policy.ID)
if err != nil {
return nil, err
}
txn, err := s.GetMasterX().Beginx()
if err != nil {
return nil, err
}
defer finalizeTransactionX(txn, &err)
// Create a new policy in RetentionPolicies
if _, err = txn.Exec(policyInsertQuery, policyInsertArgs...); err != nil {
return nil, err
}
// Insert the channel IDs into RetentionPoliciesChannels
if _, err = executePossiblyEmptyQuery(txn, channelsInsertQuery, channelsInsertArgs...); err != nil {
return nil, err
}
// Insert the team IDs into RetentionPoliciesTeams
if _, err = executePossiblyEmptyQuery(txn, teamsInsertQuery, teamsInsertArgs...); err != nil {
return nil, err
}
// Select the new policy (with team/channel counts) which we just created
var newPolicy model.RetentionPolicyWithTeamAndChannelCounts
if err = txn.Get(&newPolicy, queryString, args...); err != nil {
return nil, err
}
if err = txn.Commit(); err != nil {
return nil, err
}
return &newPolicy, nil
}
func (s *SqlRetentionPolicyStore) checkTeamsExist(teamIDs []string) error {
if len(teamIDs) > 0 {
teamsSelectQuery, teamsSelectArgs, err := s.getQueryBuilder().
Select("Id").
From("Teams").
Where(sq.Eq{"Id": teamIDs}).
ToSql()
if err != nil {
return err
}
rows := []*string{}
err = s.GetReplicaX().Select(&rows, teamsSelectQuery, teamsSelectArgs...)
if err != nil {
return err
}
if len(rows) == len(teamIDs) {
return nil
}
retrievedIDs := make(map[string]bool)
for _, teamID := range rows {
retrievedIDs[*teamID] = true
}
for _, teamID := range teamIDs {
if _, ok := retrievedIDs[teamID]; !ok {
return store.NewErrNotFound("Team", teamID)
}
}
}
return nil
}
func (s *SqlRetentionPolicyStore) checkChannelsExist(channelIDs []string) error {
if len(channelIDs) > 0 {
channelsSelectQuery, channelsSelectArgs, err := s.getQueryBuilder().
Select("Id").
From("Channels").
Where(sq.Eq{"Id": channelIDs}).
ToSql()
if err != nil {
return err
}
rows := []*string{}
err = s.GetReplicaX().Select(&rows, channelsSelectQuery, channelsSelectArgs...)
if err != nil {
return err
}
if len(rows) == len(channelIDs) {
return nil
}
retrievedIDs := make(map[string]bool)
for _, channelID := range rows {
retrievedIDs[*channelID] = true
}
for _, channelID := range channelIDs {
if _, ok := retrievedIDs[channelID]; !ok {
return store.NewErrNotFound("Channel", channelID)
}
}
}
return nil
}
func (s *SqlRetentionPolicyStore) buildInsertRetentionPoliciesChannelsQuery(policyID string, channelIDs []string) (query string, args []any, err error) {
if len(channelIDs) > 0 {
builder := s.getQueryBuilder().
Insert("RetentionPoliciesChannels").
Columns("PolicyId", "ChannelId")
for _, channelID := range channelIDs {
builder = builder.Values(policyID, channelID)
}
query, args, err = builder.ToSql()
}
return
}
func (s *SqlRetentionPolicyStore) buildInsertRetentionPoliciesTeamsQuery(policyID string, teamIDs []string) (query string, args []any, err error) {
if len(teamIDs) > 0 {
builder := s.getQueryBuilder().
Insert("RetentionPoliciesTeams").
Columns("PolicyId", "TeamId")
for _, teamID := range teamIDs {
builder = builder.Values(policyID, teamID)
}
query, args, err = builder.ToSql()
}
return
}
func (s *SqlRetentionPolicyStore) Patch(patch *model.RetentionPolicyWithTeamAndChannelIDs) (_ *model.RetentionPolicyWithTeamAndChannelCounts, err error) {
// Strategy:
// 1. Update policy attributes
// 2. Delete existing channels from policy
// 3. Insert new channels into policy
// 4. Delete existing teams from policy
// 5. Insert new teams into policy
// 6. Read new policy
if err = s.checkTeamsExist(patch.TeamIDs); err != nil {
return nil, err
}
if err = s.checkChannelsExist(patch.ChannelIDs); err != nil {
return nil, err
}
policyUpdateQuery := ""
policyUpdateArgs := []any{}
if patch.DisplayName != "" || patch.PostDurationDays != nil {
builder := s.getQueryBuilder().Update("RetentionPolicies")
if patch.DisplayName != "" {
builder = builder.Set("DisplayName", patch.DisplayName)
}
if patch.PostDurationDays != nil {
builder = builder.Set("PostDuration", *patch.PostDurationDays)
}
policyUpdateQuery, policyUpdateArgs, err = builder.
Where(sq.Eq{"Id": patch.ID}).
ToSql()
if err != nil {
return nil, err
}
}
channelsDeleteQuery := ""
channelsDeleteArgs := []any{}
channelsInsertQuery := ""
channelsInsertArgs := []any{}
if patch.ChannelIDs != nil {
channelsDeleteQuery, channelsDeleteArgs, err = s.getQueryBuilder().
Delete("RetentionPoliciesChannels").
Where(sq.Eq{"PolicyId": patch.ID}).
ToSql()
if err != nil {
return nil, err
}
channelsInsertQuery, channelsInsertArgs, err = s.buildInsertRetentionPoliciesChannelsQuery(patch.ID, patch.ChannelIDs)
if err != nil {
return nil, err
}
}
teamsDeleteQuery := ""
teamsDeleteArgs := []any{}
teamsInsertQuery := ""
teamsInsertArgs := []any{}
if patch.TeamIDs != nil {
teamsDeleteQuery, teamsDeleteArgs, err = s.getQueryBuilder().
Delete("RetentionPoliciesTeams").
Where(sq.Eq{"PolicyId": patch.ID}).
ToSql()
if err != nil {
return nil, err
}
teamsInsertQuery, teamsInsertArgs, err = s.buildInsertRetentionPoliciesTeamsQuery(patch.ID, patch.TeamIDs)
if err != nil {
return nil, err
}
}
queryString, args, err := s.buildGetPolicyQuery(patch.ID)
if err != nil {
return nil, err
}
txn, err := s.GetMasterX().Beginx()
if err != nil {
return nil, err
}
defer finalizeTransactionX(txn, &err)
// Update the fields of the policy in RetentionPolicies
if _, err = executePossiblyEmptyQuery(txn, policyUpdateQuery, policyUpdateArgs...); err != nil {
return nil, err
}
// Remove all channels from the policy in RetentionPoliciesChannels
if _, err = executePossiblyEmptyQuery(txn, channelsDeleteQuery, channelsDeleteArgs...); err != nil {
return nil, err
}
// Insert the new channels for the policy in RetentionPoliciesChannels
if _, err = executePossiblyEmptyQuery(txn, channelsInsertQuery, channelsInsertArgs...); err != nil {
return nil, err
}
// Remove all teams from the policy in RetentionPoliciesTeams
if _, err = executePossiblyEmptyQuery(txn, teamsDeleteQuery, teamsDeleteArgs...); err != nil {
return nil, err
}
// Insert the new teams for the policy in RetentionPoliciesTeams
if _, err = executePossiblyEmptyQuery(txn, teamsInsertQuery, teamsInsertArgs...); err != nil {
return nil, err
}
// Select the policy which we just updated
var newPolicy model.RetentionPolicyWithTeamAndChannelCounts
if err = txn.Get(&newPolicy, queryString, args...); err != nil {
return nil, err
}
if err = txn.Commit(); err != nil {
return nil, err
}
return &newPolicy, nil
}
func (s *SqlRetentionPolicyStore) buildGetPolicyQuery(id string) (string, []any, error) {
return s.buildGetPoliciesQuery(id, 0, 1)
}
// buildGetPoliciesQuery builds a query to select information for the policy with the specified
// ID, or, if `id` is the empty string, from all policies. The results returned will be sorted by
// policy display name and ID.
func (s *SqlRetentionPolicyStore) buildGetPoliciesQuery(id string, offset, limit int) (string, []any, error) {
rpcSubQuery := s.getQueryBuilder().
Select("RetentionPolicies.Id, COUNT(RetentionPoliciesChannels.ChannelId) AS Count").
From("RetentionPolicies").
LeftJoin("RetentionPoliciesChannels ON RetentionPolicies.Id = RetentionPoliciesChannels.PolicyId").
GroupBy("RetentionPolicies.Id").
OrderBy("RetentionPolicies.DisplayName, RetentionPolicies.Id").
Limit(uint64(limit)).
Offset(uint64(offset))
if id != "" {
rpcSubQuery = rpcSubQuery.Where(sq.Eq{"RetentionPolicies.Id": id})
}
rpcSubQueryString, args, err := rpcSubQuery.ToSql()
if err != nil {
return "", nil, errors.Wrap(err, "retention_policies_tosql")
}
rptSubQuery := s.getQueryBuilder().
Select("RetentionPolicies.Id, COUNT(RetentionPoliciesTeams.TeamId) AS Count").
From("RetentionPolicies").
LeftJoin("RetentionPoliciesTeams ON RetentionPolicies.Id = RetentionPoliciesTeams.PolicyId").
GroupBy("RetentionPolicies.Id").
OrderBy("RetentionPolicies.DisplayName, RetentionPolicies.Id").
Limit(uint64(limit)).
Offset(uint64(offset))
if id != "" {
rptSubQuery = rptSubQuery.Where(sq.Eq{"RetentionPolicies.Id": id})
}
rptSubQueryString, _, err := rptSubQuery.ToSql()
if err != nil {
return "", nil, errors.Wrap(err, "retention_policies_tosql")
}
query := s.getQueryBuilder().
Select(`
RetentionPolicies.Id as "Id",
RetentionPolicies.DisplayName,
RetentionPolicies.PostDuration as "PostDuration",
A.Count AS ChannelCount,
B.Count AS TeamCount
`).
From("RetentionPolicies").
InnerJoin(`(` + rpcSubQueryString + `) AS A ON RetentionPolicies.Id = A.Id`).
InnerJoin(`(` + rptSubQueryString + `) AS B ON RetentionPolicies.Id = B.Id`).
OrderBy("RetentionPolicies.DisplayName, RetentionPolicies.Id")
queryString, _, err := query.ToSql()
if err != nil {
return "", nil, errors.Wrap(err, "retention_policies_tosql")
}
// MySQL does not support positional params, so we add one param for each WHERE clause.
if s.DriverName() == model.DatabaseDriverMysql {
args = append(args, args...)
}
return queryString, args, nil
}
func (s *SqlRetentionPolicyStore) Get(id string) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
queryString, args, err := s.buildGetPolicyQuery(id)
if err != nil {
return nil, err
}
var policy model.RetentionPolicyWithTeamAndChannelCounts
if err := s.GetReplicaX().Get(&policy, queryString, args...); err != nil {
return nil, err
}
return &policy, nil
}
func (s *SqlRetentionPolicyStore) GetAll(offset, limit int) ([]*model.RetentionPolicyWithTeamAndChannelCounts, error) {
policies := []*model.RetentionPolicyWithTeamAndChannelCounts{}
queryString, args, err := s.buildGetPoliciesQuery("", offset, limit)
if err != nil {
return policies, err
}
err = s.GetReplicaX().Select(&policies, queryString, args...)
return policies, err
}
func (s *SqlRetentionPolicyStore) GetCount() (int64, error) {
var count int64
err := s.GetReplicaX().Get(&count, "SELECT COUNT(*) FROM RetentionPolicies")
if err != nil {
return count, err
}
return count, nil
}
func (s *SqlRetentionPolicyStore) Delete(id string) error {
query := s.getQueryBuilder().
Delete("RetentionPolicies").
Where(sq.Eq{"Id": id})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "retention_policies_tosql")
}
sqlResult, err := s.GetMasterX().Exec(queryString, args...)
if err != nil {
return errors.Wrapf(err, "failed to permanent delete retention policy with id=%s", id)
}
numRowsAffected, err := sqlResult.RowsAffected()
if err != nil {
return errors.Wrap(err, "unable to get rows affected")
} else if numRowsAffected == 0 {
return errors.New("policy not found")
}
return nil
}
func (s *SqlRetentionPolicyStore) GetChannels(policyId string, offset, limit int) (model.ChannelListWithTeamData, error) {
query := s.getQueryBuilder().Select(`Channels.*, Teams.DisplayName AS TeamDisplayName,
Teams.Name AS TeamName,Teams.UpdateAt AS TeamUpdateAt`).
From("RetentionPoliciesChannels").
InnerJoin("Channels ON RetentionPoliciesChannels.ChannelId = Channels.Id").
InnerJoin("Teams ON Channels.TeamId = Teams.Id").
Where(sq.Eq{"RetentionPoliciesChannels.PolicyId": policyId}).
OrderBy("Channels.DisplayName, Channels.Id").
Limit(uint64(limit)).
Offset(uint64(offset))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "retention_policies_channels_tosql")
}
channels := model.ChannelListWithTeamData{}
if err := s.GetReplicaX().Select(&channels, queryString, args...); err != nil {
return channels, errors.Wrap(err, "failed to find RetentionPoliciesChannels")
}
for _, channel := range channels {
channel.PolicyID = model.NewString(policyId)
}
return channels, nil
}
func (s *SqlRetentionPolicyStore) GetChannelsCount(policyId string) (int64, error) {
query := s.getQueryBuilder().
Select("Count(*)").
From("RetentionPolicies").
InnerJoin("RetentionPoliciesChannels ON RetentionPolicies.Id = RetentionPoliciesChannels.PolicyId").
Where(sq.Eq{"RetentionPolicies.Id": policyId})
queryString, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "retention_policies_tosql")
}
var count int64
if err := s.GetReplicaX().Get(&count, queryString, args...); err != nil {
return 0, errors.Wrap(err, "failed to count RetentionPolicies")
}
return count, nil
}
func (s *SqlRetentionPolicyStore) AddChannels(policyId string, channelIds []string) error {
if len(channelIds) == 0 {
return nil
}
if err := s.checkChannelsExist(channelIds); err != nil {
return err
}
query := s.getQueryBuilder().
Insert("RetentionPoliciesChannels").
Columns("policyId", "channelId")
for _, channelId := range channelIds {
query = query.Values(policyId, channelId)
}
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "retention_policies_channels_tosql")
}
_, err = s.GetMasterX().Exec(queryString, args...)
if err != nil {
switch dbErr := err.(type) {
case *pq.Error:
if dbErr.Code == PGForeignKeyViolationErrorCode {
return store.NewErrNotFound("RetentionPolicy", policyId)
}
case *mysql.MySQLError:
if dbErr.Number == MySQLForeignKeyViolationErrorCode {
return store.NewErrNotFound("RetentionPolicy", policyId)
}
}
}
return nil
}
func (s *SqlRetentionPolicyStore) RemoveChannels(policyId string, channelIds []string) error {
if len(channelIds) == 0 {
return nil
}
query := s.getQueryBuilder().
Delete("RetentionPoliciesChannels").
Where(sq.And{
sq.Eq{"PolicyId": policyId},
sq.Eq{"ChannelId": channelIds},
})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "retention_policies_channels_tosql")
}
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrapf(err, "failed to permanent delete retention policy channels with policyid=%s", policyId)
}
return nil
}
func (s *SqlRetentionPolicyStore) GetTeams(policyId string, offset, limit int) ([]*model.Team, error) {
query := s.getQueryBuilder().
Select("Teams.*").
From("RetentionPoliciesTeams").
InnerJoin("Teams ON RetentionPoliciesTeams.TeamId = Teams.Id").
Where(sq.Eq{"RetentionPoliciesTeams.PolicyId": policyId}).
OrderBy("Teams.DisplayName, Teams.Id").
Limit(uint64(limit)).
Offset(uint64(offset))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "retention_policies_teams_tosql")
}
teams := []*model.Team{}
if err = s.GetReplicaX().Select(&teams, queryString, args...); err != nil {
return teams, errors.Wrap(err, "failed to find Teams")
}
return teams, nil
}
func (s *SqlRetentionPolicyStore) GetTeamsCount(policyId string) (int64, error) {
query := s.getQueryBuilder().
Select("Count(*)").
From("RetentionPolicies").
InnerJoin("RetentionPoliciesTeams ON RetentionPolicies.Id = RetentionPoliciesTeams.PolicyId").
Where(sq.Eq{"RetentionPolicies.Id": policyId})
queryString, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "retention_policies_tosql")
}
var count int64
if err := s.GetReplicaX().Get(&count, queryString, args...); err != nil {
return 0, errors.Wrap(err, "failed to count RetentionPolicies")
}
return count, nil
}
func (s *SqlRetentionPolicyStore) AddTeams(policyId string, teamIds []string) error {
if len(teamIds) == 0 {
return nil
}
if err := s.checkTeamsExist(teamIds); err != nil {
return err
}
query := s.getQueryBuilder().
Insert("RetentionPoliciesTeams").
Columns("PolicyId", "TeamId")
for _, teamId := range teamIds {
query = query.Values(policyId, teamId)
}
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "retention_policies_teams_tosql")
}
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrap(err, "failed to insert retention policies teams")
}
return nil
}
func (s *SqlRetentionPolicyStore) RemoveTeams(policyId string, teamIds []string) error {
if len(teamIds) == 0 {
return nil
}
query := s.getQueryBuilder().
Delete("RetentionPoliciesTeams").
Where(sq.And{
sq.Eq{"PolicyId": policyId},
sq.Eq{"TeamId": teamIds},
})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "retention_policies_teams_tosql")
}
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrapf(err, "unable to permanent delete retention policies teams with policyid=%s", policyId)
}
return nil
}
func subQueryIN(property string, query sq.SelectBuilder) sq.Sqlizer {
queryString, args := query.MustSql()
subQuery := fmt.Sprintf("%s IN (SELECT * FROM (%s) AS A)", property, queryString)
return sq.Expr(subQuery, args...)
}
// DeleteOrphanedRows removes entries from RetentionPoliciesChannels and RetentionPoliciesTeams
// where a channel or team no longer exists.
func (s *SqlRetentionPolicyStore) DeleteOrphanedRows(limit int) (deleted int64, err error) {
// We need the extra level of nesting to deal with MySQL's locking
rpcSubQuery := sq.Select("ChannelId").
From("RetentionPoliciesChannels").
LeftJoin("Channels ON RetentionPoliciesChannels.ChannelId = Channels.Id").
Where("Channels.Id IS NULL").
Limit(uint64(limit))
rpcDeleteQuery, rpcArgs, err := s.getQueryBuilder().
Delete("RetentionPoliciesChannels").
Where(subQueryIN("ChannelId", rpcSubQuery)).
ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "retention_policies_channels_tosql")
}
rptSubQuery := sq.Select("TeamId").
From("RetentionPoliciesTeams").
LeftJoin("Teams ON RetentionPoliciesTeams.TeamId = Teams.Id").
Where("Teams.Id IS NULL").
Limit(uint64(limit))
rptDeleteQuery, rptArgs, err := s.getQueryBuilder().
Delete("RetentionPoliciesTeams").
Where(subQueryIN("TeamId", rptSubQuery)).
ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "retention_policies_teams_tosql")
}
result, err := s.GetMasterX().Exec(rpcDeleteQuery, rpcArgs...)
if err != nil {
return
}
rpcDeleted, err := result.RowsAffected()
if err != nil {
return
}
result, err = s.GetMasterX().Exec(rptDeleteQuery, rptArgs...)
if err != nil {
return
}
rptDeleted, err := result.RowsAffected()
if err != nil {
return
}
deleted = rpcDeleted + rptDeleted
return
}
func (s *SqlRetentionPolicyStore) GetTeamPoliciesForUser(userID string, offset, limit int) ([]*model.RetentionPolicyForTeam, error) {
query := s.getQueryBuilder().
Select(`Teams.Id AS "Id", RetentionPolicies.PostDuration AS "PostDuration"`).
From("Users").
InnerJoin("TeamMembers ON Users.Id = TeamMembers.UserId").
InnerJoin("Teams ON TeamMembers.TeamId = Teams.Id").
InnerJoin("RetentionPoliciesTeams ON Teams.Id = RetentionPoliciesTeams.TeamId").
InnerJoin("RetentionPolicies ON RetentionPoliciesTeams.PolicyId = RetentionPolicies.Id").
Where(
sq.And{
sq.Eq{"Users.Id": userID},
sq.Eq{"TeamMembers.DeleteAt": 0},
sq.Eq{"Teams.DeleteAt": 0},
},
).
OrderBy("Teams.Id").
Limit(uint64(limit)).
Offset(uint64(offset))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_policies_for_user_tosql")
}
policies := []*model.RetentionPolicyForTeam{}
if err := s.GetReplicaX().Select(&policies, queryString, args...); err != nil {
return policies, errors.Wrap(err, "failed to find Users")
}
return policies, nil
}
func (s *SqlRetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) {
query := s.getQueryBuilder().
Select("Count(*)").
From("Users").
InnerJoin("TeamMembers ON Users.Id = TeamMembers.UserId").
InnerJoin("Teams ON TeamMembers.TeamId = Teams.Id").
InnerJoin("RetentionPoliciesTeams ON Teams.Id = RetentionPoliciesTeams.TeamId").
InnerJoin("RetentionPolicies ON RetentionPoliciesTeams.PolicyId = RetentionPolicies.Id").
Where(
sq.And{
sq.Eq{"Users.Id": userID},
sq.Eq{"TeamMembers.DeleteAt": 0},
sq.Eq{"Teams.DeleteAt": 0},
},
)
queryString, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "team_policies_count_for_user_tosql")
}
var count int64
if err := s.GetReplicaX().Get(&count, queryString, args...); err != nil {
return 0, errors.Wrap(err, "failed to count TeamPoliciesCountForUser")
}
return count, nil
}
func (s *SqlRetentionPolicyStore) GetChannelPoliciesForUser(userID string, offset, limit int) ([]*model.RetentionPolicyForChannel, error) {
query := s.getQueryBuilder().
Select(`Channels.Id as "Id", RetentionPolicies.PostDuration as "PostDuration"`).
From("Users").
InnerJoin("ChannelMembers ON Users.Id = ChannelMembers.UserId").
InnerJoin("Channels ON ChannelMembers.ChannelId = Channels.Id").
InnerJoin("RetentionPoliciesChannels ON Channels.Id = RetentionPoliciesChannels.ChannelId").
InnerJoin("RetentionPolicies ON RetentionPoliciesChannels.PolicyId = RetentionPolicies.Id").
Where(
sq.And{
sq.Eq{"Users.Id": userID},
sq.Eq{"Channels.DeleteAt": 0},
},
).
OrderBy("Channels.Id").
Limit(uint64(limit)).
Offset(uint64(offset))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_policies_for_user_tosql")
}
policies := []*model.RetentionPolicyForChannel{}
if err := s.GetReplicaX().Select(&policies, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
return policies, nil
}
func (s *SqlRetentionPolicyStore) GetChannelPoliciesCountForUser(userID string) (int64, error) {
query := s.getQueryBuilder().
Select("Count(*)").
From("Users").
InnerJoin("ChannelMembers ON Users.Id = ChannelMembers.UserId").
InnerJoin("Channels ON ChannelMembers.ChannelId = Channels.Id").
InnerJoin("RetentionPoliciesChannels ON Channels.Id = RetentionPoliciesChannels.ChannelId").
InnerJoin("RetentionPolicies ON RetentionPoliciesChannels.PolicyId = RetentionPolicies.Id").
Where(
sq.And{
sq.Eq{"Users.Id": userID},
sq.Eq{"Channels.DeleteAt": 0},
},
)
queryString, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "channel_policies_count_users_tosql")
}
var count int64
if err := s.GetReplicaX().Get(&count, queryString, args...); err != nil {
return 0, errors.Wrap(err, "failed to count ChannelPoliciesCountForUser")
}
return count, nil
}
// RetentionPolicyBatchDeletionInfo gives information on how to delete records
// under a retention policy; see `genericPermanentDeleteBatchForRetentionPolicies`.
//
// `BaseBuilder` should already have selected the primary key(s) for the main table
// and should be joined to a table with a ChannelId column, which will be used to join
// on the Channels table.
// `Table` is the name of the table from which records are being deleted.
// `TimeColumn` is the name of the column which contains the timestamp of the record.
// `PrimaryKeys` contains the primary keys of `table`. It should be the same as the
// `From` clause in `baseBuilder`.
// `ChannelIDTable` is the table which contains the ChannelId column, it may be the
// same as `table`, or will be different if a join was used.
// `NowMillis` must be a Unix timestamp in milliseconds and is used by the granular
// policies; if `nowMillis - timestamp(record)` is greater than
// the post duration of a granular policy, than the record will be deleted.
// `GlobalPolicyEndTime` is used by the global policy; any record older than this time
// will be deleted by the global policy if it does not fall under a granular policy.
// To disable the granular policies, set `NowMillis` to 0.
// To disable the global policy, set `GlobalPolicyEndTime` to 0.
type RetentionPolicyBatchDeletionInfo struct {
BaseBuilder sq.SelectBuilder
Table string
TimeColumn string
PrimaryKeys []string
ChannelIDTable string
NowMillis int64
GlobalPolicyEndTime int64
Limit int64
}
// genericPermanentDeleteBatchForRetentionPolicies is a helper function for tables
// which need to delete records for granular and global policies.
func genericPermanentDeleteBatchForRetentionPolicies(
r RetentionPolicyBatchDeletionInfo,
s *SqlStore,
cursor model.RetentionPolicyCursor,
) (int64, model.RetentionPolicyCursor, error) {
baseBuilder := r.BaseBuilder.InnerJoin("Channels ON " + r.ChannelIDTable + ".ChannelId = Channels.Id")
scopedTimeColumn := r.Table + "." + r.TimeColumn
nowStr := strconv.FormatInt(r.NowMillis, 10)
// A record falls under the scope of a granular retention policy if:
// 1. The policy's post duration is >= 0
// 2. The record's lifespan has not exceeded the policy's post duration
const millisecondsInADay = 24 * 60 * 60 * 1000
fallsUnderGranularPolicy := sq.And{
sq.GtOrEq{"RetentionPolicies.PostDuration": 0},
sq.Expr(nowStr + " - " + scopedTimeColumn + " > RetentionPolicies.PostDuration * " + strconv.FormatInt(millisecondsInADay, 10)),
}
// If the caller wants to disable the global policy from running
if r.GlobalPolicyEndTime <= 0 {
cursor.GlobalPoliciesDone = true
}
// If the caller wants to disable the granular policies from running
if r.NowMillis <= 0 {
cursor.ChannelPoliciesDone = true
cursor.TeamPoliciesDone = true
}
var totalRowsAffected int64
// First, delete all of the records which fall under the scope of a channel-specific policy
if !cursor.ChannelPoliciesDone {
channelPoliciesBuilder := baseBuilder.
InnerJoin("RetentionPoliciesChannels ON " + r.ChannelIDTable + ".ChannelId = RetentionPoliciesChannels.ChannelId").
InnerJoin("RetentionPolicies ON RetentionPoliciesChannels.PolicyId = RetentionPolicies.Id").
Where(fallsUnderGranularPolicy).
Limit(uint64(r.Limit))
rowsAffected, err := genericRetentionPoliciesDeletion(channelPoliciesBuilder, r, s)
if err != nil {
return 0, cursor, err
}
if rowsAffected < r.Limit {
cursor.ChannelPoliciesDone = true
}
totalRowsAffected += rowsAffected
r.Limit -= rowsAffected
}
// Next, delete all of the records which fall under the scope of a team-specific policy
if cursor.ChannelPoliciesDone && !cursor.TeamPoliciesDone {
// Channel-specific policies override team-specific policies.
teamPoliciesBuilder := baseBuilder.
LeftJoin("RetentionPoliciesChannels ON " + r.ChannelIDTable + ".ChannelId = RetentionPoliciesChannels.ChannelId").
InnerJoin("RetentionPoliciesTeams ON Channels.TeamId = RetentionPoliciesTeams.TeamId").
InnerJoin("RetentionPolicies ON RetentionPoliciesTeams.PolicyId = RetentionPolicies.Id").
Where(sq.And{
sq.Eq{"RetentionPoliciesChannels.PolicyId": nil},
sq.Expr("RetentionPoliciesTeams.PolicyId = RetentionPolicies.Id"),
}).
Where(fallsUnderGranularPolicy).
Limit(uint64(r.Limit))
rowsAffected, err := genericRetentionPoliciesDeletion(teamPoliciesBuilder, r, s)
if err != nil {
return 0, cursor, err
}
if rowsAffected < r.Limit {
cursor.TeamPoliciesDone = true
}
totalRowsAffected += rowsAffected
r.Limit -= rowsAffected
}
// Finally, delete all of the records which fall under the scope of the global policy
if cursor.ChannelPoliciesDone && cursor.TeamPoliciesDone && !cursor.GlobalPoliciesDone {
// Granular policies override the global policy.
globalPolicyBuilder := baseBuilder.
LeftJoin("RetentionPoliciesChannels ON " + r.ChannelIDTable + ".ChannelId = RetentionPoliciesChannels.ChannelId").
LeftJoin("RetentionPoliciesTeams ON Channels.TeamId = RetentionPoliciesTeams.TeamId").
LeftJoin("RetentionPolicies ON RetentionPoliciesChannels.PolicyId = RetentionPolicies.Id").
Where(sq.And{
sq.Eq{"RetentionPoliciesChannels.PolicyId": nil},
sq.Eq{"RetentionPoliciesTeams.PolicyId": nil},
}).
Where(sq.Lt{scopedTimeColumn: r.GlobalPolicyEndTime}).
Limit(uint64(r.Limit))
rowsAffected, err := genericRetentionPoliciesDeletion(globalPolicyBuilder, r, s)
if err != nil {
return 0, cursor, err
}
if rowsAffected < r.Limit {
cursor.GlobalPoliciesDone = true
}
totalRowsAffected += rowsAffected
}
return totalRowsAffected, cursor, nil
}
// genericRetentionPoliciesDeletion actually executes the DELETE query using a sq.SelectBuilder
// which selects the rows to delete.
func genericRetentionPoliciesDeletion(
builder sq.SelectBuilder,
r RetentionPolicyBatchDeletionInfo,
s *SqlStore,
) (rowsAffected int64, err error) {
query, args, err := builder.ToSql()
if err != nil {
return 0, errors.Wrap(err, r.Table+"_tosql")
}
if s.DriverName() == model.DatabaseDriverPostgres {
primaryKeysStr := "(" + strings.Join(r.PrimaryKeys, ",") + ")"
query = `
DELETE FROM ` + r.Table + ` WHERE ` + primaryKeysStr + ` IN (
` + query + `
)`
} else {
// MySQL does not support the LIMIT clause in a subquery with IN
clauses := make([]string, len(r.PrimaryKeys))
for i, key := range r.PrimaryKeys {
clauses[i] = r.Table + "." + key + " = A." + key
}
joinClause := strings.Join(clauses, " AND ")
query = `
DELETE ` + r.Table + ` FROM ` + r.Table + ` INNER JOIN (
` + query + `
) AS A ON ` + joinClause
}
result, err := s.GetMasterX().Exec(query, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to delete "+r.Table)
}
rowsAffected, err = result.RowsAffected()
if err != nil {
return 0, errors.Wrap(err, "failed to get rows affected for "+r.Table)
}
return
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
"fmt"
"strings"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlRoleStore struct {
*SqlStore
}
type Role struct {
Id string
Name string
DisplayName string
Description string
CreateAt int64
UpdateAt int64
DeleteAt int64
Permissions string
SchemeManaged bool
BuiltIn bool
}
type channelRolesPermissions struct {
GuestRoleName string
UserRoleName string
AdminRoleName string
HigherScopedGuestPermissions string
HigherScopedUserPermissions string
HigherScopedAdminPermissions string
}
func NewRoleFromModel(role *model.Role) *Role {
permissionsMap := make(map[string]bool)
permissions := ""
for _, permission := range role.Permissions {
if !permissionsMap[permission] {
permissions += fmt.Sprintf(" %v", permission)
permissionsMap[permission] = true
}
}
return &Role{
Id: role.Id,
Name: role.Name,
DisplayName: role.DisplayName,
Description: role.Description,
CreateAt: role.CreateAt,
UpdateAt: role.UpdateAt,
DeleteAt: role.DeleteAt,
Permissions: permissions,
SchemeManaged: role.SchemeManaged,
BuiltIn: role.BuiltIn,
}
}
func (role Role) ToModel() *model.Role {
return &model.Role{
Id: role.Id,
Name: role.Name,
DisplayName: role.DisplayName,
Description: role.Description,
CreateAt: role.CreateAt,
UpdateAt: role.UpdateAt,
DeleteAt: role.DeleteAt,
Permissions: strings.Fields(role.Permissions),
SchemeManaged: role.SchemeManaged,
BuiltIn: role.BuiltIn,
}
}
func newSqlRoleStore(sqlStore *SqlStore) store.RoleStore {
return &SqlRoleStore{sqlStore}
}
func (s *SqlRoleStore) Save(role *model.Role) (_ *model.Role, err error) {
// Check the role is valid before proceeding.
if !role.IsValidWithoutId() {
return nil, store.NewErrInvalidInput("Role", "<any>", fmt.Sprintf("%v", role))
}
if role.Id == "" {
transaction, terr := s.GetMasterX().Beginx()
if terr != nil {
return nil, errors.Wrap(terr, "begin_transaction")
}
defer finalizeTransactionX(transaction, &terr)
createdRole, terr := s.createRole(role, transaction)
if terr != nil {
return nil, errors.Wrap(terr, "unable to create Role")
} else if terr = transaction.Commit(); terr != nil {
return nil, errors.Wrap(terr, "commit_transaction")
}
return createdRole, nil
}
dbRole := NewRoleFromModel(role)
dbRole.UpdateAt = model.GetMillis()
res, err := s.GetMasterX().NamedExec(`UPDATE Roles
SET UpdateAt=:UpdateAt, DeleteAt=:DeleteAt, CreateAt=:CreateAt, Name=:Name, DisplayName=:DisplayName,
Description=:Description, Permissions=:Permissions, SchemeManaged=:SchemeManaged, BuiltIn=:BuiltIn
WHERE Id=:Id`, &dbRole)
if err != nil {
return nil, errors.Wrap(err, "failed to update Role")
}
rowsChanged, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "error while getting rows_affected")
}
if rowsChanged != 1 {
return nil, fmt.Errorf("invalid number of updated rows, expected 1 but got %d", rowsChanged)
}
return dbRole.ToModel(), nil
}
func (s *SqlRoleStore) createRole(role *model.Role, transaction *sqlxTxWrapper) (*model.Role, error) {
// Check the role is valid before proceeding.
if !role.IsValidWithoutId() {
return nil, store.NewErrInvalidInput("Role", "<any>", fmt.Sprintf("%v", role))
}
dbRole := NewRoleFromModel(role)
dbRole.Id = model.NewId()
dbRole.CreateAt = model.GetMillis()
dbRole.UpdateAt = dbRole.CreateAt
if _, err := transaction.NamedExec(`INSERT INTO Roles
(Id, Name, DisplayName, Description, Permissions, CreateAt, UpdateAt, DeleteAt, SchemeManaged, BuiltIn)
VALUES
(:Id, :Name, :DisplayName, :Description, :Permissions, :CreateAt, :UpdateAt, :DeleteAt, :SchemeManaged, :BuiltIn)`, dbRole); err != nil {
return nil, errors.Wrap(err, "failed to save Role")
}
return dbRole.ToModel(), nil
}
func (s *SqlRoleStore) Get(roleId string) (*model.Role, error) {
dbRole := Role{}
if err := s.GetReplicaX().Get(&dbRole, "SELECT * from Roles WHERE Id = ?", roleId); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Role", roleId)
}
return nil, errors.Wrap(err, "failed to get Role")
}
return dbRole.ToModel(), nil
}
func (s *SqlRoleStore) GetAll() ([]*model.Role, error) {
dbRoles := []Role{}
if err := s.GetReplicaX().Select(&dbRoles, "SELECT * from Roles"); err != nil {
return nil, errors.Wrap(err, "failed to find Roles")
}
roles := []*model.Role{}
for _, dbRole := range dbRoles {
roles = append(roles, dbRole.ToModel())
}
return roles, nil
}
func (s *SqlRoleStore) GetByName(ctx context.Context, name string) (*model.Role, error) {
dbRole := Role{}
if err := s.DBXFromContext(ctx).Get(&dbRole, "SELECT * from Roles WHERE Name = ?", name); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Role", fmt.Sprintf("name=%s", name))
}
return nil, errors.Wrapf(err, "failed to find Roles with name=%s", name)
}
return dbRole.ToModel(), nil
}
func (s *SqlRoleStore) GetByNames(names []string) ([]*model.Role, error) {
if len(names) == 0 {
return []*model.Role{}, nil
}
query := s.getQueryBuilder().
Select("Id, Name, DisplayName, Description, CreateAt, UpdateAt, DeleteAt, Permissions, SchemeManaged, BuiltIn").
From("Roles").
Where(sq.Eq{"Name": names})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "role_tosql")
}
rows, err := s.GetReplicaX().DB.Query(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Roles")
}
roles := []*model.Role{}
defer rows.Close()
for rows.Next() {
var role Role
err = rows.Scan(
&role.Id, &role.Name, &role.DisplayName, &role.Description,
&role.CreateAt, &role.UpdateAt, &role.DeleteAt, &role.Permissions,
&role.SchemeManaged, &role.BuiltIn)
if err != nil {
return nil, errors.Wrap(err, "failed to scan values")
}
roles = append(roles, role.ToModel())
}
if err = rows.Err(); err != nil {
return nil, errors.Wrap(err, "unable to iterate over rows")
}
return roles, nil
}
func (s *SqlRoleStore) Delete(roleId string) (*model.Role, error) {
// Get the role.
var role Role
if err := s.GetReplicaX().Get(&role, "SELECT * from Roles WHERE Id = ?", roleId); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Role", roleId)
}
return nil, errors.Wrapf(err, "failed to get Role with id=%s", roleId)
}
time := model.GetMillis()
role.DeleteAt = time
role.UpdateAt = time
res, err := s.GetMasterX().NamedExec(`UPDATE Roles
SET UpdateAt=:UpdateAt, DeleteAt=:DeleteAt, CreateAt=:CreateAt, Name=:Name, DisplayName=:DisplayName,
Description=:Description, Permissions=:Permissions, SchemeManaged=:SchemeManaged, BuiltIn=:BuiltIn
WHERE Id=:Id`, &role)
if err != nil {
return nil, errors.Wrap(err, "failed to update Role")
}
rowsChanged, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "error while getting rows_affected")
}
if rowsChanged != 1 {
return nil, fmt.Errorf("invalid number of updated rows, expected 1 but got %d", rowsChanged)
}
return role.ToModel(), nil
}
func (s *SqlRoleStore) PermanentDeleteAll() error {
if _, err := s.GetMasterX().Exec("DELETE FROM Roles"); err != nil {
return errors.Wrap(err, "failed to delete Roles")
}
return nil
}
func (s *SqlRoleStore) channelHigherScopedPermissionsQuery(roleNames []string) string {
sqlTmpl := `
SELECT
'' AS GuestRoleName,
RoleSchemes.DefaultChannelUserRole AS UserRoleName,
RoleSchemes.DefaultChannelAdminRole AS AdminRoleName,
'' AS HigherScopedGuestPermissions,
UserRoles.Permissions AS HigherScopedUserPermissions,
AdminRoles.Permissions AS HigherScopedAdminPermissions
FROM
Schemes AS RoleSchemes
JOIN Channels ON Channels.SchemeId = RoleSchemes.Id
JOIN Teams ON Teams.Id = Channels.TeamId
JOIN Schemes ON Schemes.Id = Teams.SchemeId
RIGHT JOIN Roles AS UserRoles ON UserRoles.Name = Schemes.DefaultChannelUserRole
RIGHT JOIN Roles AS AdminRoles ON AdminRoles.Name = Schemes.DefaultChannelAdminRole
WHERE
RoleSchemes.DefaultChannelUserRole IN ('%[1]s')
OR RoleSchemes.DefaultChannelAdminRole IN ('%[1]s')
UNION
SELECT
RoleSchemes.DefaultChannelGuestRole AS GuestRoleName,
'' AS UserRoleName,
'' AS AdminRoleName,
GuestRoles.Permissions AS HigherScopedGuestPermissions,
'' AS HigherScopedUserPermissions,
'' AS HigherScopedAdminPermissions
FROM
Schemes AS RoleSchemes
JOIN Channels ON Channels.SchemeId = RoleSchemes.Id
JOIN Teams ON Teams.Id = Channels.TeamId
JOIN Schemes ON Schemes.Id = Teams.SchemeId
RIGHT JOIN Roles AS GuestRoles ON GuestRoles.Name = Schemes.DefaultChannelGuestRole
WHERE
RoleSchemes.DefaultChannelGuestRole IN ('%[1]s')
UNION
SELECT
Schemes.DefaultChannelGuestRole AS GuestRoleName,
Schemes.DefaultChannelUserRole AS UserRoleName,
Schemes.DefaultChannelAdminRole AS AdminRoleName,
GuestRoles.Permissions AS HigherScopedGuestPermissions,
UserRoles.Permissions AS HigherScopedUserPermissions,
AdminRoles.Permissions AS HigherScopedAdminPermissions
FROM
Schemes
JOIN Channels ON Channels.SchemeId = Schemes.Id
JOIN Teams ON Teams.Id = Channels.TeamId
JOIN Roles AS GuestRoles ON GuestRoles.Name = '%[2]s'
JOIN Roles AS UserRoles ON UserRoles.Name = '%[3]s'
JOIN Roles AS AdminRoles ON AdminRoles.Name = '%[4]s'
WHERE
(Schemes.DefaultChannelGuestRole IN ('%[1]s')
OR Schemes.DefaultChannelUserRole IN ('%[1]s')
OR Schemes.DefaultChannelAdminRole IN ('%[1]s'))
AND (Teams.SchemeId = ''
OR Teams.SchemeId IS NULL)
`
// The below three channel role names are referenced by their name value because there is no system scheme
// record that ships with Mattermost, otherwise the system scheme would be referenced by name and the channel
// roles would be referenced by their column names.
return fmt.Sprintf(
sqlTmpl,
strings.Join(roleNames, "', '"),
model.ChannelGuestRoleId,
model.ChannelUserRoleId,
model.ChannelAdminRoleId,
)
}
func (s *SqlRoleStore) ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, error) {
query := s.channelHigherScopedPermissionsQuery(roleNames)
rolesPermissions := []*channelRolesPermissions{}
if err := s.GetReplicaX().Select(&rolesPermissions, query); err != nil {
return nil, errors.Wrap(err, "failed to find RolePermissions")
}
roleNameHigherScopedPermissions := map[string]*model.RolePermissions{}
for _, rp := range rolesPermissions {
roleNameHigherScopedPermissions[rp.GuestRoleName] = &model.RolePermissions{RoleID: model.ChannelGuestRoleId, Permissions: strings.Split(rp.HigherScopedGuestPermissions, " ")}
roleNameHigherScopedPermissions[rp.UserRoleName] = &model.RolePermissions{RoleID: model.ChannelUserRoleId, Permissions: strings.Split(rp.HigherScopedUserPermissions, " ")}
roleNameHigherScopedPermissions[rp.AdminRoleName] = &model.RolePermissions{RoleID: model.ChannelAdminRoleId, Permissions: strings.Split(rp.HigherScopedAdminPermissions, " ")}
}
return roleNameHigherScopedPermissions, nil
}
func (s *SqlRoleStore) AllChannelSchemeRoles() ([]*model.Role, error) {
query := s.getQueryBuilder().
Select("Roles.*").
From("Schemes").
Join("Roles ON Schemes.DefaultChannelGuestRole = Roles.Name OR Schemes.DefaultChannelUserRole = Roles.Name OR Schemes.DefaultChannelAdminRole = Roles.Name").
Where(sq.Eq{"Schemes.Scope": model.SchemeScopeChannel}).
Where(sq.Eq{"Roles.DeleteAt": 0}).
Where(sq.Eq{"Schemes.DeleteAt": 0})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "role_tosql")
}
dbRoles := []*Role{}
if err = s.GetReplicaX().Select(&dbRoles, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Roles")
}
roles := []*model.Role{}
for _, dbRole := range dbRoles {
roles = append(roles, dbRole.ToModel())
}
return roles, nil
}
// ChannelRolesUnderTeamRole finds all of the channel-scheme roles under the team of the given team-scheme role.
func (s *SqlRoleStore) ChannelRolesUnderTeamRole(roleName string) ([]*model.Role, error) {
query := s.getQueryBuilder().
Select("ChannelSchemeRoles.*").
From("Roles AS HigherScopedRoles").
Join("Schemes AS HigherScopedSchemes ON (HigherScopedRoles.Name = HigherScopedSchemes.DefaultChannelGuestRole OR HigherScopedRoles.Name = HigherScopedSchemes.DefaultChannelUserRole OR HigherScopedRoles.Name = HigherScopedSchemes.DefaultChannelAdminRole)").
Join("Teams ON Teams.SchemeId = HigherScopedSchemes.Id").
Join("Channels ON Channels.TeamId = Teams.Id").
Join("Schemes AS ChannelSchemes ON Channels.SchemeId = ChannelSchemes.Id").
Join("Roles AS ChannelSchemeRoles ON (ChannelSchemeRoles.Name = ChannelSchemes.DefaultChannelGuestRole OR ChannelSchemeRoles.Name = ChannelSchemes.DefaultChannelUserRole OR ChannelSchemeRoles.Name = ChannelSchemes.DefaultChannelAdminRole)").
Where(sq.Eq{"HigherScopedSchemes.Scope": model.SchemeScopeTeam}).
Where(sq.Eq{"HigherScopedRoles.Name": roleName}).
Where(sq.Eq{"HigherScopedRoles.DeleteAt": 0}).
Where(sq.Eq{"HigherScopedSchemes.DeleteAt": 0}).
Where(sq.Eq{"Teams.DeleteAt": 0}).
Where(sq.Eq{"Channels.DeleteAt": 0}).
Where(sq.Eq{"ChannelSchemes.DeleteAt": 0}).
Where(sq.Eq{"ChannelSchemeRoles.DeleteAt": 0})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "role_tosql")
}
dbRoles := []*Role{}
if err = s.GetReplicaX().Select(&dbRoles, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Roles")
}
roles := []*model.Role{}
for _, dbRole := range dbRoles {
roles = append(roles, dbRole.ToModel())
}
return roles, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const (
SchemeRoleDisplayNameTeamAdmin = "Team Admin Role for Scheme"
SchemeRoleDisplayNameTeamUser = "Team User Role for Scheme"
SchemeRoleDisplayNameTeamGuest = "Team Guest Role for Scheme"
SchemeRoleDisplayNameChannelAdmin = "Channel Admin Role for Scheme"
SchemeRoleDisplayNameChannelUser = "Channel User Role for Scheme"
SchemeRoleDisplayNameChannelGuest = "Channel Guest Role for Scheme"
SchemeRoleDisplayNamePlaybookAdmin = "Playbook Admin Role for Scheme"
SchemeRoleDisplayNamePlaybookMember = "Playbook Member Role for Scheme"
SchemeRoleDisplayNameRunAdmin = "Run Admin Role for Scheme"
SchemeRoleDisplayNameRunMember = "Run Member Role for Scheme"
)
type SqlSchemeStore struct {
*SqlStore
}
func newSqlSchemeStore(sqlStore *SqlStore) store.SchemeStore {
return &SqlSchemeStore{sqlStore}
}
func (s *SqlSchemeStore) Save(scheme *model.Scheme) (_ *model.Scheme, err error) {
if scheme.Id == "" {
transaction, terr := s.GetMasterX().Beginx()
if terr != nil {
return nil, errors.Wrap(terr, "begin_transaction")
}
defer finalizeTransactionX(transaction, &terr)
newScheme, terr := s.createScheme(scheme, transaction)
if terr != nil {
return nil, terr
}
if terr = transaction.Commit(); terr != nil {
return nil, errors.Wrap(terr, "commit_transaction")
}
return newScheme, nil
}
if !scheme.IsValid() {
return nil, store.NewErrInvalidInput("Scheme", "<any>", fmt.Sprintf("%v", scheme))
}
scheme.UpdateAt = model.GetMillis()
res, err := s.GetMasterX().NamedExec(`UPDATE Schemes
SET UpdateAt=:UpdateAt, CreateAt=:CreateAt, DeleteAt=:DeleteAt, Name=:Name, DisplayName=:DisplayName, Description=:Description, Scope=:Scope,
DefaultTeamAdminRole=:DefaultTeamAdminRole, DefaultTeamUserRole=:DefaultTeamUserRole, DefaultTeamGuestRole=:DefaultTeamGuestRole,
DefaultChannelAdminRole=:DefaultChannelAdminRole, DefaultChannelUserRole=:DefaultChannelUserRole, DefaultChannelGuestRole=:DefaultChannelGuestRole,
DefaultPlaybookMemberRole=:DefaultPlaybookMemberRole, DefaultPlaybookAdminRole=:DefaultPlaybookAdminRole, DefaultRunMemberRole=:DefaultRunMemberRole, DefaultRunAdminRole=:DefaultRunAdminRole
WHERE Id=:Id`, scheme)
if err != nil {
return nil, errors.Wrap(err, "failed to update Scheme")
}
rowsChanged, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "error while getting rows_affected")
}
if rowsChanged != 1 {
return nil, errors.New("no record to update")
}
return scheme, nil
}
func (s *SqlSchemeStore) createScheme(scheme *model.Scheme, transaction *sqlxTxWrapper) (*model.Scheme, error) {
// Fetch the default system scheme roles to populate default permissions.
defaultRoleNames := []string{
model.TeamAdminRoleId,
model.TeamUserRoleId,
model.TeamGuestRoleId,
model.ChannelAdminRoleId,
model.ChannelUserRoleId,
model.ChannelGuestRoleId,
model.PlaybookAdminRoleId,
model.PlaybookMemberRoleId,
model.RunAdminRoleId,
model.RunMemberRoleId,
}
defaultRoles := make(map[string]*model.Role)
roles, err := s.SqlStore.Role().GetByNames(defaultRoleNames)
if err != nil {
return nil, err
}
for _, role := range roles {
defaultRoles[role.Name] = role
}
if len(defaultRoles) != len(defaultRoleNames) {
return nil, errors.New("createScheme: unable to retrieve default scheme roles")
}
// Create the appropriate default roles for the scheme.
if scheme.Scope == model.SchemeScopeTeam {
// Team Admin Role
teamAdminRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("%s %s", SchemeRoleDisplayNameTeamAdmin, scheme.Name),
Permissions: defaultRoles[model.TeamAdminRoleId].Permissions,
SchemeManaged: true,
}
savedRole, err := s.SqlStore.Role().(*SqlRoleStore).createRole(teamAdminRole, transaction)
if err != nil {
return nil, err
}
scheme.DefaultTeamAdminRole = savedRole.Name
// Team User Role
teamUserRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("%s %s", SchemeRoleDisplayNameTeamUser, scheme.Name),
Permissions: defaultRoles[model.TeamUserRoleId].Permissions,
SchemeManaged: true,
}
savedRole, err = s.SqlStore.Role().(*SqlRoleStore).createRole(teamUserRole, transaction)
if err != nil {
return nil, err
}
scheme.DefaultTeamUserRole = savedRole.Name
// Team Guest Role
teamGuestRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("%s %s", SchemeRoleDisplayNameTeamGuest, scheme.Name),
Permissions: defaultRoles[model.TeamGuestRoleId].Permissions,
SchemeManaged: true,
}
savedRole, err = s.SqlStore.Role().(*SqlRoleStore).createRole(teamGuestRole, transaction)
if err != nil {
return nil, err
}
scheme.DefaultTeamGuestRole = savedRole.Name
// playbook admin role
playbookAdminRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("%s %s", SchemeRoleDisplayNamePlaybookAdmin, scheme.Name),
Permissions: defaultRoles[model.PlaybookAdminRoleId].Permissions,
SchemeManaged: true,
}
savedRole, err = s.SqlStore.Role().(*SqlRoleStore).createRole(playbookAdminRole, transaction)
if err != nil {
return nil, err
}
scheme.DefaultPlaybookAdminRole = savedRole.Name
// playbook member role
playbookMemberRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("%s %s", SchemeRoleDisplayNamePlaybookMember, scheme.Name),
Permissions: defaultRoles[model.PlaybookMemberRoleId].Permissions,
SchemeManaged: true,
}
savedRole, err = s.SqlStore.Role().(*SqlRoleStore).createRole(playbookMemberRole, transaction)
if err != nil {
return nil, err
}
scheme.DefaultPlaybookMemberRole = savedRole.Name
// run admin role
runAdminRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("%s %s", SchemeRoleDisplayNameRunAdmin, scheme.Name),
Permissions: defaultRoles[model.RunAdminRoleId].Permissions,
SchemeManaged: true,
}
savedRole, err = s.SqlStore.Role().(*SqlRoleStore).createRole(runAdminRole, transaction)
if err != nil {
return nil, err
}
scheme.DefaultRunAdminRole = savedRole.Name
// run member role
runMemberRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("%s %s", SchemeRoleDisplayNameRunMember, scheme.Name),
Permissions: defaultRoles[model.RunMemberRoleId].Permissions,
SchemeManaged: true,
}
savedRole, err = s.SqlStore.Role().(*SqlRoleStore).createRole(runMemberRole, transaction)
if err != nil {
return nil, err
}
scheme.DefaultRunMemberRole = savedRole.Name
}
if scheme.Scope == model.SchemeScopeTeam || scheme.Scope == model.SchemeScopeChannel {
// Channel Admin Role
channelAdminRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("Channel Admin Role for Scheme %s", scheme.Name),
Permissions: defaultRoles[model.ChannelAdminRoleId].Permissions,
SchemeManaged: true,
}
if scheme.Scope == model.SchemeScopeChannel {
channelAdminRole.Permissions = []string{}
}
savedRole, err := s.SqlStore.Role().(*SqlRoleStore).createRole(channelAdminRole, transaction)
if err != nil {
return nil, err
}
scheme.DefaultChannelAdminRole = savedRole.Name
// Channel User Role
channelUserRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("Channel User Role for Scheme %s", scheme.Name),
Permissions: defaultRoles[model.ChannelUserRoleId].Permissions,
SchemeManaged: true,
}
if scheme.Scope == model.SchemeScopeChannel {
channelUserRole.Permissions = filterModerated(channelUserRole.Permissions)
}
savedRole, err = s.SqlStore.Role().(*SqlRoleStore).createRole(channelUserRole, transaction)
if err != nil {
return nil, err
}
scheme.DefaultChannelUserRole = savedRole.Name
// Channel Guest Role
channelGuestRole := &model.Role{
Name: model.NewId(),
DisplayName: fmt.Sprintf("Channel Guest Role for Scheme %s", scheme.Name),
Permissions: defaultRoles[model.ChannelGuestRoleId].Permissions,
SchemeManaged: true,
}
if scheme.Scope == model.SchemeScopeChannel {
channelGuestRole.Permissions = filterModerated(channelGuestRole.Permissions)
}
savedRole, err = s.SqlStore.Role().(*SqlRoleStore).createRole(channelGuestRole, transaction)
if err != nil {
return nil, err
}
scheme.DefaultChannelGuestRole = savedRole.Name
}
scheme.Id = model.NewId()
if scheme.Name == "" {
scheme.Name = model.NewId()
}
scheme.CreateAt = model.GetMillis()
scheme.UpdateAt = scheme.CreateAt
// Validate the scheme
if !scheme.IsValidForCreate() {
return nil, store.NewErrInvalidInput("Scheme", "<any>", fmt.Sprintf("%v", scheme))
}
if _, err := transaction.NamedExec(`INSERT INTO Schemes
(Id, Name, DisplayName, Description, Scope, DefaultTeamAdminRole, DefaultTeamUserRole, DefaultTeamGuestRole, DefaultChannelAdminRole, DefaultChannelUserRole, DefaultChannelGuestRole, CreateAt, UpdateAt, DeleteAt, DefaultPlaybookAdminRole, DefaultPlaybookMemberRole, DefaultRunAdminRole, DefaultRunMemberRole)
VALUES
(:Id, :Name, :DisplayName, :Description, :Scope, :DefaultTeamAdminRole, :DefaultTeamUserRole, :DefaultTeamGuestRole, :DefaultChannelAdminRole, :DefaultChannelUserRole, :DefaultChannelGuestRole, :CreateAt, :UpdateAt, :DeleteAt, :DefaultPlaybookAdminRole, :DefaultPlaybookMemberRole, :DefaultRunAdminRole, :DefaultRunMemberRole)`, scheme); err != nil {
return nil, errors.Wrap(err, "failed to save Scheme")
}
return scheme, nil
}
func filterModerated(permissions []string) []string {
filteredPermissions := []string{}
for _, perm := range permissions {
if _, ok := model.ChannelModeratedPermissionsMap[perm]; ok {
filteredPermissions = append(filteredPermissions, perm)
}
}
return filteredPermissions
}
func (s *SqlSchemeStore) Get(schemeId string) (*model.Scheme, error) {
var scheme model.Scheme
if err := s.GetReplicaX().Get(&scheme, "SELECT * from Schemes WHERE Id = ?", schemeId); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Scheme", fmt.Sprintf("schemeId=%s", schemeId))
}
return nil, errors.Wrapf(err, "failed to get Scheme with schemeId=%s", schemeId)
}
return &scheme, nil
}
func (s *SqlSchemeStore) GetByName(schemeName string) (*model.Scheme, error) {
var scheme model.Scheme
if err := s.GetReplicaX().Get(&scheme, "SELECT * from Schemes WHERE Name = ?", schemeName); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Scheme", fmt.Sprintf("schemeName=%s", schemeName))
}
return nil, errors.Wrapf(err, "failed to get Scheme with schemeName=%s", schemeName)
}
return &scheme, nil
}
func (s *SqlSchemeStore) Delete(schemeId string) (*model.Scheme, error) {
// Get the scheme
scheme := model.Scheme{}
if err := s.GetMasterX().Get(&scheme, `SELECT * from Schemes WHERE Id = ?`, schemeId); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Scheme", fmt.Sprintf("schemeId=%s", schemeId))
}
return nil, errors.Wrapf(err, "failed to get Scheme with schemeId=%s", schemeId)
}
// Update any teams or channels using this scheme to the default scheme.
if scheme.Scope == model.SchemeScopeTeam {
if _, err := s.GetMasterX().Exec(`UPDATE Teams SET SchemeId = '' WHERE SchemeId = ?`, schemeId); err != nil {
return nil, errors.Wrapf(err, "failed to update Teams with schemeId=%s", schemeId)
}
s.Team().ClearCaches()
} else if scheme.Scope == model.SchemeScopeChannel {
if _, err := s.GetMasterX().Exec(`UPDATE Channels SET SchemeId = '' WHERE SchemeId = ?`, schemeId); err != nil {
return nil, errors.Wrapf(err, "failed to update Channels with schemeId=%s", schemeId)
}
}
// Blow away the channel caches.
s.Channel().ClearCaches()
// Delete the roles belonging to the scheme.
roleNames := []string{scheme.DefaultChannelGuestRole, scheme.DefaultChannelUserRole, scheme.DefaultChannelAdminRole}
if scheme.Scope == model.SchemeScopeTeam {
roleNames = append(roleNames, scheme.DefaultTeamGuestRole, scheme.DefaultTeamUserRole, scheme.DefaultTeamAdminRole)
}
if scheme.Scope == model.SchemeScopePlaybook {
roleNames = append(roleNames, scheme.DefaultPlaybookAdminRole, scheme.DefaultPlaybookMemberRole)
}
if scheme.Scope == model.SchemeScopeRun {
roleNames = append(roleNames, scheme.DefaultRunAdminRole, scheme.DefaultRunMemberRole)
}
time := model.GetMillis()
updateQuery, args, err := s.getQueryBuilder().
Update("Roles").
Where(sq.Eq{"Name": roleNames}).
Set("UpdateAt", time).
Set("DeleteAt", time).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "status_tosql")
}
if _, err = s.GetMasterX().Exec(updateQuery, args...); err != nil {
return nil, errors.Wrapf(err, "failed to update Roles with name in (%s)", roleNames)
}
// Delete the scheme itself.
scheme.UpdateAt = time
scheme.DeleteAt = time
res, err := s.GetMasterX().NamedExec(`UPDATE Schemes
SET UpdateAt=:UpdateAt, DeleteAt=:DeleteAt, CreateAt=:CreateAt, Name=:Name, DisplayName=:DisplayName, Description=:Description, Scope=:Scope,
DefaultTeamAdminRole=:DefaultTeamAdminRole, DefaultTeamUserRole=:DefaultTeamUserRole, DefaultTeamGuestRole=:DefaultTeamGuestRole,
DefaultChannelAdminRole=:DefaultChannelAdminRole, DefaultChannelUserRole=:DefaultChannelUserRole, DefaultChannelGuestRole=:DefaultChannelGuestRole
WHERE Id=:Id`, &scheme)
if err != nil {
return nil, errors.Wrapf(err, "failed to update Scheme with schemeId=%s", schemeId)
}
rowsChanged, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrapf(err, "failed to get RowsAffected while updating scheme with schemeId=%s", schemeId)
}
if rowsChanged != 1 {
return nil, errors.New("no record to update")
}
return &scheme, nil
}
func (s *SqlSchemeStore) GetAllPage(scope string, offset int, limit int) ([]*model.Scheme, error) {
schemes := []*model.Scheme{}
query := s.getQueryBuilder().
Select("*").
From("Schemes").
Where(sq.Eq{"DeleteAt": 0}).
OrderBy("CreateAt DESC").
Limit(uint64(limit)).
Offset(uint64(offset))
if scope != "" {
query = query.Where(sq.Eq{"Scope": scope})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "status_tosql")
}
if err := s.GetReplicaX().Select(&schemes, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to get Schemes")
}
return schemes, nil
}
func (s *SqlSchemeStore) PermanentDeleteAll() error {
if _, err := s.GetMasterX().Exec("DELETE from Schemes"); err != nil {
return errors.Wrap(err, "failed to delete Schemes")
}
return nil
}
func (s *SqlSchemeStore) CountByScope(scope string) (int64, error) {
var count int64
err := s.GetReplicaX().Get(&count, `SELECT count(*) FROM Schemes WHERE Scope = ? AND DeleteAt = 0`, scope)
if err != nil {
return 0, errors.Wrap(err, "failed to count Schemes by scope")
}
return count, nil
}
func (s *SqlSchemeStore) CountWithoutPermission(schemeScope, permissionID string, roleScope model.RoleScope, roleType model.RoleType) (int64, error) {
joinCol := fmt.Sprintf("Default%s%sRole", roleScope, roleType)
query := fmt.Sprintf(`
SELECT
count(*)
FROM Schemes
JOIN Roles ON Roles.Name = Schemes.%s
WHERE
Schemes.DeleteAt = 0 AND
Schemes.Scope = '%s' AND
Roles.Permissions NOT LIKE '%%%s%%'
`, joinCol, schemeScope, permissionID)
var count int64
err := s.GetReplicaX().Get(&count, query)
if err != nil {
return 0, errors.Wrap(err, "failed to count Schemes without permission")
}
return count, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"encoding/json"
"fmt"
"time"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const (
sessionsCleanupDelay = 100 * time.Millisecond
)
type SqlSessionStore struct {
*SqlStore
}
func newSqlSessionStore(sqlStore *SqlStore) store.SessionStore {
return &SqlSessionStore{sqlStore}
}
func (me SqlSessionStore) Save(session *model.Session) (*model.Session, error) {
if session.Id != "" {
return nil, store.NewErrInvalidInput("Session", "id", session.Id)
}
session.PreSave()
if err := session.IsValid(); err != nil {
return nil, err
}
jsonProps, err := json.Marshal(session.Props)
if err != nil {
return nil, errors.Wrap(err, "failed marshalling session props")
}
if me.IsBinaryParamEnabled() {
jsonProps = AppendBinaryFlag(jsonProps)
}
query, args, err := me.getQueryBuilder().
Insert("Sessions").
Columns("Id", "Token", "CreateAt", "ExpiresAt", "LastActivityAt", "UserId", "DeviceId", "Roles", "IsOAuth", "ExpiredNotify", "Props").
Values(session.Id, session.Token, session.CreateAt, session.ExpiresAt, session.LastActivityAt, session.UserId, session.DeviceId, session.Roles, session.IsOAuth, session.ExpiredNotify, jsonProps).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "sessions_tosql")
}
if _, err = me.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to save Session with id=%s", session.Id)
}
teamMembers, err := me.Team().GetTeamsForUser(context.Background(), session.UserId, "", true)
if err != nil {
return nil, errors.Wrapf(err, "failed to find TeamMembers for Session with userId=%s", session.UserId)
}
session.TeamMembers = make([]*model.TeamMember, 0, len(teamMembers))
for _, tm := range teamMembers {
if tm.DeleteAt == 0 {
session.TeamMembers = append(session.TeamMembers, tm)
}
}
return session, nil
}
func (me SqlSessionStore) Get(ctx context.Context, sessionIdOrToken string) (*model.Session, error) {
sessions := []*model.Session{}
if err := me.DBXFromContext(ctx).Select(&sessions, "SELECT * FROM Sessions WHERE Token = ? OR Id = ? LIMIT 1", sessionIdOrToken, sessionIdOrToken); err != nil {
return nil, errors.Wrapf(err, "failed to find Sessions with sessionIdOrToken=%s", sessionIdOrToken)
}
if len(sessions) == 0 {
return nil, store.NewErrNotFound("Session", fmt.Sprintf("sessionIdOrToken=%s", sessionIdOrToken))
}
session := sessions[0]
tempMembers, err := me.Team().GetTeamsForUser(
WithMaster(context.Background()),
session.UserId, "", true)
if err != nil {
return nil, errors.Wrapf(err, "failed to find TeamMembers for Session with userId=%s", session.UserId)
}
sessions[0].TeamMembers = make([]*model.TeamMember, 0, len(tempMembers))
for _, tm := range tempMembers {
if tm.DeleteAt == 0 {
sessions[0].TeamMembers = append(sessions[0].TeamMembers, tm)
}
}
return session, nil
}
func (me SqlSessionStore) GetSessions(userId string) ([]*model.Session, error) {
sessions := []*model.Session{}
if err := me.GetReplicaX().Select(&sessions, "SELECT * FROM Sessions WHERE UserId = ? ORDER BY LastActivityAt DESC", userId); err != nil {
return nil, errors.Wrapf(err, "failed to find Sessions with userId=%s", userId)
}
teamMembers, err := me.Team().GetTeamsForUser(context.Background(), userId, "", true)
if err != nil {
return nil, errors.Wrapf(err, "failed to find TeamMembers for Session with userId=%s", userId)
}
for _, session := range sessions {
session.TeamMembers = make([]*model.TeamMember, 0, len(teamMembers))
for _, tm := range teamMembers {
if tm.DeleteAt == 0 {
session.TeamMembers = append(session.TeamMembers, tm)
}
}
}
return sessions, nil
}
func (me SqlSessionStore) GetSessionsWithActiveDeviceIds(userId string) ([]*model.Session, error) {
query :=
`SELECT *
FROM
Sessions
WHERE
UserId = ? AND
ExpiresAt != 0 AND
? <= ExpiresAt AND
DeviceId != ''`
sessions := []*model.Session{}
if err := me.GetReplicaX().Select(&sessions, query, userId, model.GetMillis()); err != nil {
return nil, errors.Wrapf(err, "failed to find Sessions with userId=%s", userId)
}
return sessions, nil
}
func (me SqlSessionStore) GetSessionsExpired(thresholdMillis int64, mobileOnly bool, unnotifiedOnly bool) ([]*model.Session, error) {
now := model.GetMillis()
builder := me.getQueryBuilder().
Select("*").
From("Sessions").
Where(sq.NotEq{"ExpiresAt": 0}).
Where(sq.Lt{"ExpiresAt": now}).
Where(sq.Gt{"ExpiresAt": now - thresholdMillis})
if mobileOnly {
builder = builder.Where(sq.NotEq{"DeviceId": ""})
}
if unnotifiedOnly {
builder = builder.Where(sq.NotEq{"ExpiredNotify": true})
}
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "sessions_tosql")
}
sessions := []*model.Session{}
err = me.GetReplicaX().Select(&sessions, query, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Sessions")
}
return sessions, nil
}
func (me SqlSessionStore) UpdateExpiredNotify(sessionId string, notified bool) error {
query, args, err := me.getQueryBuilder().
Update("Sessions").
Set("ExpiredNotify", notified).
Where(sq.Eq{"Id": sessionId}).
ToSql()
if err != nil {
return errors.Wrap(err, "sessions_tosql")
}
_, err = me.GetMasterX().Exec(query, args...)
if err != nil {
return errors.Wrapf(err, "failed to update Session with id=%s", sessionId)
}
return nil
}
func (me SqlSessionStore) Remove(sessionIdOrToken string) error {
_, err := me.GetMasterX().Exec("DELETE FROM Sessions WHERE Id = ? Or Token = ?", sessionIdOrToken, sessionIdOrToken)
if err != nil {
return errors.Wrapf(err, "failed to delete Session with sessionIdOrToken=%s", sessionIdOrToken)
}
return nil
}
func (me SqlSessionStore) RemoveAllSessions() error {
_, err := me.GetMasterX().Exec("DELETE FROM Sessions")
if err != nil {
return errors.Wrap(err, "failed to delete all Sessions")
}
return nil
}
func (me SqlSessionStore) PermanentDeleteSessionsByUser(userId string) error {
_, err := me.GetMasterX().Exec("DELETE FROM Sessions WHERE UserId = ?", userId)
if err != nil {
return errors.Wrapf(err, "failed to delete Session with userId=%s", userId)
}
return nil
}
func (me SqlSessionStore) UpdateExpiresAt(sessionId string, time int64) error {
_, err := me.GetMasterX().Exec("UPDATE Sessions SET ExpiresAt = ?, ExpiredNotify = false WHERE Id = ?", time, sessionId)
if err != nil {
return errors.Wrapf(err, "failed to update Session with sessionId=%s", sessionId)
}
return nil
}
func (me SqlSessionStore) UpdateLastActivityAt(sessionId string, time int64) error {
_, err := me.GetMasterX().Exec("UPDATE Sessions SET LastActivityAt = ? WHERE Id = ?", time, sessionId)
if err != nil {
return errors.Wrapf(err, "failed to update Session with id=%s", sessionId)
}
return nil
}
func (me SqlSessionStore) UpdateRoles(userId, roles string) (string, error) {
if len(roles) > model.UserRolesMaxLength {
return "", fmt.Errorf("given session roles length (%d) exceeds max storage limit (%d)", len(roles), model.UserRolesMaxLength)
}
_, err := me.GetMasterX().Exec("UPDATE Sessions SET Roles = ? WHERE UserId = ?", roles, userId)
if err != nil {
return "", errors.Wrapf(err, "failed to update Session with userId=%s and roles=%s", userId, roles)
}
return userId, nil
}
func (me SqlSessionStore) UpdateDeviceId(id string, deviceId string, expiresAt int64) (string, error) {
query := "UPDATE Sessions SET DeviceId = ?, ExpiresAt = ?, ExpiredNotify = false WHERE Id = ?"
_, err := me.GetMasterX().Exec(query, deviceId, expiresAt, id)
if err != nil {
return "", errors.Wrapf(err, "failed to update Session with id=%s", id)
}
return deviceId, nil
}
func (me SqlSessionStore) UpdateProps(session *model.Session) error {
jsonProps, err := json.Marshal(session.Props)
if err != nil {
return errors.Wrap(err, "failed marshalling session props")
}
if me.IsBinaryParamEnabled() {
jsonProps = AppendBinaryFlag(jsonProps)
}
query, args, err := me.getQueryBuilder().
Update("Sessions").
Set("Props", jsonProps).
Where(sq.Eq{"Id": session.Id}).
ToSql()
if err != nil {
errors.Wrap(err, "sessions_tosql")
}
_, err = me.GetMasterX().Exec(query, args...)
if err != nil {
return errors.Wrap(err, "failed to update Session")
}
return nil
}
func (me SqlSessionStore) AnalyticsSessionCount() (int64, error) {
var count int64
query :=
`SELECT
COUNT(*)
FROM
Sessions
WHERE ExpiresAt > ?`
if err := me.GetReplicaX().Get(&count, query, model.GetMillis()); err != nil {
return int64(0), errors.Wrap(err, "failed to count Sessions")
}
return count, nil
}
func (me SqlSessionStore) Cleanup(expiryTime int64, batchSize int64) error {
var query string
if me.DriverName() == model.DatabaseDriverPostgres {
query = "DELETE FROM Sessions WHERE Id IN (SELECT Id FROM Sessions WHERE ExpiresAt != 0 AND ? > ExpiresAt LIMIT ?)"
} else {
query = "DELETE FROM Sessions WHERE ExpiresAt != 0 AND ? > ExpiresAt LIMIT ?"
}
var rowsAffected int64 = 1
for rowsAffected > 0 {
sqlResult, err := me.GetMasterX().Exec(query, expiryTime, batchSize)
if err != nil {
return errors.Wrap(err, "unable to delete sessions")
}
var rowErr error
rowsAffected, rowErr = sqlResult.RowsAffected()
if rowErr != nil {
return errors.Wrap(err, "unable to delete sessions")
}
time.Sleep(sessionsCleanupDelay)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
)
const (
DefaultGetUsersForSyncLimit = 100
)
type SqlSharedChannelStore struct {
*SqlStore
}
func newSqlSharedChannelStore(sqlStore *SqlStore) store.SharedChannelStore {
return &SqlSharedChannelStore{
SqlStore: sqlStore,
}
}
// Save inserts a new shared channel record.
func (s SqlSharedChannelStore) Save(sc *model.SharedChannel) (sh *model.SharedChannel, err error) {
sc.PreSave()
if err := sc.IsValid(); err != nil {
return nil, err
}
// make sure the shared channel is associated with a real channel.
channel, err := s.stores.channel.Get(sc.ChannelId, true)
if err != nil {
return nil, fmt.Errorf("invalid channel: %w", err)
}
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
query, args, err := s.getQueryBuilder().Insert("SharedChannels").
Columns("ChannelId", "TeamId", "Home", "ReadOnly", "ShareName", "ShareDisplayName", "SharePurpose", "ShareHeader", "CreatorId", "CreateAt", "UpdateAt", "RemoteId").
Values(sc.ChannelId, sc.TeamId, sc.Home, sc.ReadOnly, sc.ShareName, sc.ShareDisplayName, sc.SharePurpose, sc.ShareHeader, sc.CreatorId, sc.CreateAt, sc.UpdateAt, sc.RemoteId).
ToSql()
if err != nil {
return nil, errors.Wrapf(err, "savesharedchannel_tosql")
}
if _, err := transaction.Exec(query, args...); err != nil {
return nil, errors.Wrapf(err, "save_shared_channel: ChannelId=%s", sc.ChannelId)
}
// set `Shared` flag in Channels table if needed
if channel.Shared == nil || !*channel.Shared {
if err := s.stores.channel.SetShared(channel.Id, true); err != nil {
return nil, err
}
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return sc, nil
}
// Get fetches a shared channel by channel_id.
func (s SqlSharedChannelStore) Get(channelId string) (*model.SharedChannel, error) {
var sc model.SharedChannel
query := s.getQueryBuilder().
Select("*").
From("SharedChannels").
Where(sq.Eq{"SharedChannels.ChannelId": channelId})
squery, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getsharedchannel_tosql")
}
if err := s.GetReplicaX().Get(&sc, squery, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("SharedChannel", channelId)
}
return nil, errors.Wrapf(err, "failed to find shared channel with ChannelId=%s", channelId)
}
return &sc, nil
}
// HasChannel returns whether a given channelID is a shared channel or not.
func (s SqlSharedChannelStore) HasChannel(channelID string) (bool, error) {
builder := s.getQueryBuilder().
Select("1").
Prefix("SELECT EXISTS (").
From("SharedChannels").
Where(sq.Eq{"SharedChannels.ChannelId": channelID}).
Suffix(")")
query, args, err := builder.ToSql()
if err != nil {
return false, errors.Wrapf(err, "get_shared_channel_exists_tosql")
}
var exists bool
if err := s.GetReplicaX().Get(&exists, query, args...); err != nil {
return exists, errors.Wrapf(err, "failed to get shared channel for channel_id=%s", channelID)
}
return exists, nil
}
// GetAll fetches a paginated list of shared channels filtered by SharedChannelSearchOpts.
func (s SqlSharedChannelStore) GetAll(offset, limit int, opts model.SharedChannelFilterOpts) ([]*model.SharedChannel, error) {
if opts.ExcludeHome && opts.ExcludeRemote {
return nil, errors.New("cannot exclude home and remote shared channels")
}
safeConv := func(offset, limit int) (uint64, uint64, error) {
if offset < 0 {
return 0, 0, errors.New("offset must be positive integer")
}
if limit < 0 {
return 0, 0, errors.New("limit must be positive integer")
}
return uint64(offset), uint64(limit), nil
}
safeOffset, safeLimit, err := safeConv(offset, limit)
if err != nil {
return nil, err
}
query := s.getSharedChannelsQuery(opts, false)
query = query.OrderBy("sc.ShareDisplayName, sc.ShareName").Limit(safeLimit).Offset(safeOffset)
squery, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "failed to create query")
}
channels := []*model.SharedChannel{}
err = s.GetReplicaX().Select(&channels, squery, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to get shared channels")
}
return channels, nil
}
// GetAllCount returns the number of shared channels that would be fetched using SharedChannelSearchOpts.
func (s SqlSharedChannelStore) GetAllCount(opts model.SharedChannelFilterOpts) (int64, error) {
if opts.ExcludeHome && opts.ExcludeRemote {
return 0, errors.New("cannot exclude home and remote shared channels")
}
query := s.getSharedChannelsQuery(opts, true)
squery, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "failed to create query")
}
var count int64
err = s.GetReplicaX().Get(&count, squery, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to count channels")
}
return count, nil
}
func (s SqlSharedChannelStore) getSharedChannelsQuery(opts model.SharedChannelFilterOpts, forCount bool) sq.SelectBuilder {
var selectStr string
if forCount {
selectStr = "count(sc.ChannelId)"
} else {
selectStr = "sc.*"
}
query := s.getQueryBuilder().
Select(selectStr).
From("SharedChannels AS sc")
if opts.MemberId != "" {
query = query.Join("ChannelMembers AS cm ON cm.ChannelId = sc.ChannelId").
Where(sq.Eq{"cm.UserId": opts.MemberId})
}
if opts.TeamId != "" {
query = query.Where(sq.Eq{"sc.TeamId": opts.TeamId})
}
if opts.CreatorId != "" {
query = query.Where(sq.Eq{"sc.CreatorId": opts.CreatorId})
}
if opts.ExcludeHome {
query = query.Where(sq.NotEq{"sc.Home": true})
}
if opts.ExcludeRemote {
query = query.Where(sq.Eq{"sc.Home": true})
}
return query
}
// Update updates the shared channel.
func (s SqlSharedChannelStore) Update(sc *model.SharedChannel) (*model.SharedChannel, error) {
if err := sc.IsValid(); err != nil {
return nil, err
}
query, args, err := s.getQueryBuilder().Update("SharedChannels").Set("ChannelId", sc.ChannelId).
Set("TeamId", sc.TeamId).
Set("Home", sc.Home).
Set("ReadOnly", sc.ReadOnly).
Set("ShareName", sc.ShareName).
Set("ShareDisplayName", sc.ShareDisplayName).
Set("SharePurpose", sc.SharePurpose).
Set("ShareHeader", sc.ShareHeader).
Set("CreatorId", sc.CreatorId).
Set("CreateAt", sc.CreateAt).
Set("UpdateAt", sc.UpdateAt).
Set("RemoteId", sc.RemoteId).
Where(sq.Eq{"ChannelId": sc.ChannelId}).ToSql()
if err != nil {
return nil, errors.Wrapf(err, "updatesharedchannel_tosql")
}
res, err := s.GetMasterX().Exec(query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to update shared channel with channelId=%s", sc.ChannelId)
}
count, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "error while getting rows_affected")
}
if count != 1 {
return nil, fmt.Errorf("expected number of shared channels to be updated is 1 but was %d", count)
}
return sc, nil
}
// Delete deletes a single shared channel plus associated SharedChannelRemotes.
// Returns true if shared channel found and deleted, false if not found.
func (s SqlSharedChannelStore) Delete(channelId string) (ok bool, err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return false, errors.Wrap(err, "DeleteSharedChannel: begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
squery, args, err := s.getQueryBuilder().
Delete("SharedChannels").
Where(sq.Eq{"SharedChannels.ChannelId": channelId}).
ToSql()
if err != nil {
return false, errors.Wrap(err, "delete_shared_channel_tosql")
}
result, err := transaction.Exec(squery, args...)
if err != nil {
return false, errors.Wrap(err, "failed to delete SharedChannel")
}
// Also remove remotes from SharedChannelRemotes (if any).
squery, args, err = s.getQueryBuilder().
Delete("SharedChannelRemotes").
Where(sq.Eq{"ChannelId": channelId}).
ToSql()
if err != nil {
return false, errors.Wrap(err, "delete_shared_channel_remotes_tosql")
}
_, err = transaction.Exec(squery, args...)
if err != nil {
return false, errors.Wrap(err, "failed to delete SharedChannelRemotes")
}
count, err := result.RowsAffected()
if err != nil {
return false, errors.Wrap(err, "failed to determine rows affected")
}
if count > 0 {
// unset the channel's Shared flag
if err = s.Channel().SetShared(channelId, false); err != nil {
return false, errors.Wrap(err, "error unsetting channel share flag")
}
}
if err = transaction.Commit(); err != nil {
return false, errors.Wrap(err, "commit_transaction")
}
return count > 0, nil
}
// SaveRemote inserts a new shared channel remote record.
func (s SqlSharedChannelStore) SaveRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
remote.PreSave()
if err := remote.IsValid(); err != nil {
return nil, err
}
// make sure the shared channel remote is associated with a real channel.
if _, err := s.stores.channel.Get(remote.ChannelId, true); err != nil {
return nil, fmt.Errorf("invalid channel: %w", err)
}
query, args, err := s.getQueryBuilder().Insert("SharedChannelRemotes").
Columns("Id", "ChannelId", "CreatorId", "CreateAt", "UpdateAt", "IsInviteAccepted", "IsInviteConfirmed", "RemoteId", "LastPostUpdateAt", "LastPostId").
Values(remote.Id, remote.ChannelId, remote.CreatorId, remote.CreateAt, remote.UpdateAt, remote.IsInviteAccepted, remote.IsInviteConfirmed, remote.RemoteId, remote.LastPostUpdateAt, remote.LastPostId).
ToSql()
if err != nil {
return nil, errors.Wrapf(err, "savesharedchannelremote_tosql")
}
if _, err := s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrapf(err, "save_shared_channel_remote: channel_id=%s, id=%s", remote.ChannelId, remote.Id)
}
return remote, nil
}
// Update updates the shared channel remote.
func (s SqlSharedChannelStore) UpdateRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
if err := remote.IsValid(); err != nil {
return nil, err
}
query, args, err := s.getQueryBuilder().Update("SharedChannelRemotes").
Set("CreatorId", remote.CreatorId).
Set("CreateAt", remote.CreateAt).
Set("UpdateAt", remote.UpdateAt).
Set("IsInviteAccepted", remote.IsInviteAccepted).
Set("IsInviteConfirmed", remote.IsInviteConfirmed).
Set("RemoteId", remote.RemoteId).
Set("LastPostUpdateAt", remote.LastPostUpdateAt).
Set("LastPostId", remote.LastPostId).
Where(sq.And{
sq.Eq{"Id": remote.Id},
sq.Eq{"ChannelId": remote.ChannelId},
}).
ToSql()
if err != nil {
return nil, errors.Wrapf(err, "updatesharedchannelremote_tosql")
}
res, err := s.GetMasterX().Exec(query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to update shared channel remote with remoteId=%s", remote.Id)
}
count, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "error while getting rows_affected")
}
if count != 1 {
return nil, fmt.Errorf("expected number of shared channel remotes to be updated is 1 but was %d", count)
}
return remote, nil
}
// GetRemote fetches a shared channel remote by id.
func (s SqlSharedChannelStore) GetRemote(id string) (*model.SharedChannelRemote, error) {
var remote model.SharedChannelRemote
query := s.getQueryBuilder().
Select("*").
From("SharedChannelRemotes").
Where(sq.Eq{"SharedChannelRemotes.Id": id})
squery, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "get_shared_channel_remote_tosql")
}
if err := s.GetReplicaX().Get(&remote, squery, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("SharedChannelRemote", id)
}
return nil, errors.Wrapf(err, "failed to find shared channel remote with id=%s", id)
}
return &remote, nil
}
// GetRemoteByIds fetches a shared channel remote by channel id and remote cluster id.
func (s SqlSharedChannelStore) GetRemoteByIds(channelId string, remoteId string) (*model.SharedChannelRemote, error) {
var remote model.SharedChannelRemote
query := s.getQueryBuilder().
Select("*").
From("SharedChannelRemotes").
Where(sq.Eq{"SharedChannelRemotes.ChannelId": channelId}).
Where(sq.Eq{"SharedChannelRemotes.RemoteId": remoteId})
squery, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "get_shared_channel_remote_by_ids_tosql")
}
if err := s.GetReplicaX().Get(&remote, squery, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("SharedChannelRemote", fmt.Sprintf("channelId=%s, remoteId=%s", channelId, remoteId))
}
return nil, errors.Wrapf(err, "failed to find shared channel remote with channelId=%s, remoteId=%s", channelId, remoteId)
}
return &remote, nil
}
// GetRemotes fetches all shared channel remotes associated with channel_id.
func (s SqlSharedChannelStore) GetRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) {
remotes := []*model.SharedChannelRemote{}
query := s.getQueryBuilder().
Select("*").
From("SharedChannelRemotes")
if opts.ChannelId != "" {
query = query.Where(sq.Eq{"ChannelId": opts.ChannelId})
}
if opts.RemoteId != "" {
query = query.Where(sq.Eq{"RemoteId": opts.RemoteId})
}
if !opts.InclUnconfirmed {
query = query.Where(sq.Eq{"IsInviteConfirmed": true})
}
squery, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "get_shared_channel_remotes_tosql")
}
if err := s.GetReplicaX().Select(&remotes, squery, args...); err != nil {
if err != sql.ErrNoRows {
return nil, errors.Wrapf(err, "failed to get shared channel remotes for channel_id=%s; remote_id=%s",
opts.ChannelId, opts.RemoteId)
}
}
return remotes, nil
}
// HasRemote returns whether a given remoteId and channelId are present in the shared channel remotes or not.
func (s SqlSharedChannelStore) HasRemote(channelID string, remoteId string) (bool, error) {
builder := s.getQueryBuilder().
Select("1").
Prefix("SELECT EXISTS (").
From("SharedChannelRemotes").
Where(sq.Eq{"RemoteId": remoteId}).
Where(sq.Eq{"ChannelId": channelID}).
Suffix(")")
query, args, err := builder.ToSql()
if err != nil {
return false, errors.Wrapf(err, "get_shared_channel_hasremote_tosql")
}
var hasRemote bool
if err := s.GetReplicaX().Get(&hasRemote, query, args...); err != nil {
return hasRemote, errors.Wrapf(err, "failed to get channel remotes for channel_id=%s", channelID)
}
return hasRemote, nil
}
// GetRemoteForUser returns a remote cluster for the given userId only if the user belongs to at least one channel
// shared with the remote.
func (s SqlSharedChannelStore) GetRemoteForUser(remoteId string, userId string) (*model.RemoteCluster, error) {
builder := s.getQueryBuilder().
Select("rc.*").
From("RemoteClusters AS rc").
Join("SharedChannelRemotes AS scr ON rc.RemoteId = scr.RemoteId").
Join("ChannelMembers AS cm ON scr.ChannelId = cm.ChannelId").
Where(sq.Eq{"rc.RemoteId": remoteId}).
Where(sq.Eq{"cm.UserId": userId})
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "get_remote_for_user_tosql")
}
var rc model.RemoteCluster
if err := s.GetReplicaX().Get(&rc, query, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("RemoteCluster", remoteId)
}
return nil, errors.Wrapf(err, "failed to get remote for user_id=%s", userId)
}
return &rc, nil
}
// UpdateRemoteCursor updates the LastPostUpdateAt timestamp and LastPostId for the specified SharedChannelRemote.
func (s SqlSharedChannelStore) UpdateRemoteCursor(id string, cursor model.GetPostsSinceForSyncCursor) error {
squery, args, err := s.getQueryBuilder().
Update("SharedChannelRemotes").
Set("LastPostUpdateAt", cursor.LastPostUpdateAt).
Set("LastPostId", cursor.LastPostId).
Where(sq.Eq{"Id": id}).
ToSql()
if err != nil {
return errors.Wrap(err, "update_shared_channel_remote_cursor_tosql")
}
result, err := s.GetMasterX().Exec(squery, args...)
if err != nil {
return errors.Wrap(err, "failed to update cursor for SharedChannelRemote")
}
count, err := result.RowsAffected()
if err != nil {
return errors.Wrap(err, "failed to determine rows affected")
}
if count == 0 {
return fmt.Errorf("id not found: %s", id)
}
return nil
}
// DeleteRemote deletes a single shared channel remote.
// Returns true if remote found and deleted, false if not found.
func (s SqlSharedChannelStore) DeleteRemote(id string) (bool, error) {
squery, args, err := s.getQueryBuilder().
Delete("SharedChannelRemotes").
Where(sq.Eq{"Id": id}).
ToSql()
if err != nil {
return false, errors.Wrap(err, "delete_shared_channel_remote_tosql")
}
result, err := s.GetMasterX().Exec(squery, args...)
if err != nil {
return false, errors.Wrap(err, "failed to delete SharedChannelRemote")
}
count, err := result.RowsAffected()
if err != nil {
return false, errors.Wrap(err, "failed to determine rows affected")
}
return count > 0, nil
}
// GetRemotesStatus returns the status for each remote invited to the
// specified shared channel.
func (s SqlSharedChannelStore) GetRemotesStatus(channelId string) ([]*model.SharedChannelRemoteStatus, error) {
status := []*model.SharedChannelRemoteStatus{}
query := s.getQueryBuilder().
Select("scr.ChannelId, rc.DisplayName, rc.SiteURL, rc.LastPingAt, sc.ReadOnly, scr.IsInviteAccepted").
From("SharedChannelRemotes scr, RemoteClusters rc, SharedChannels sc").
Where("scr.RemoteId = rc.RemoteId").
Where("scr.ChannelId = sc.ChannelId").
Where(sq.Eq{"scr.ChannelId": channelId})
squery, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "get_shared_channel_remotes_status_tosql")
}
if err := s.GetReplicaX().Select(&status, squery, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("SharedChannelRemoteStatus", channelId)
}
return nil, errors.Wrapf(err, "failed to get shared channel remote status for channel_id=%s", channelId)
}
return status, nil
}
// SaveUser inserts a new shared channel user record to the SharedChannelUsers table.
func (s SqlSharedChannelStore) SaveUser(scUser *model.SharedChannelUser) (*model.SharedChannelUser, error) {
scUser.PreSave()
if err := scUser.IsValid(); err != nil {
return nil, err
}
query, args, err := s.getQueryBuilder().Insert("SharedChannelUsers").
Columns("Id", "UserId", "ChannelId", "RemoteId", "CreateAt", "LastSyncAt").
Values(scUser.Id, scUser.UserId, scUser.ChannelId, scUser.RemoteId, scUser.CreateAt, scUser.LastSyncAt).
ToSql()
if err != nil {
return nil, errors.Wrapf(err, "savesharedchanneluser_tosql")
}
if _, err := s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrapf(err, "save_shared_channel_user: user_id=%s, remote_id=%s", scUser.UserId, scUser.RemoteId)
}
return scUser, nil
}
// GetSingleUser fetches a shared channel user based on userID, channelID and remoteID.
func (s SqlSharedChannelStore) GetSingleUser(userID string, channelID string, remoteID string) (*model.SharedChannelUser, error) {
var scu model.SharedChannelUser
squery, args, err := s.getQueryBuilder().
Select("*").
From("SharedChannelUsers").
Where(sq.Eq{"SharedChannelUsers.UserId": userID}).
Where(sq.Eq{"SharedChannelUsers.RemoteId": remoteID}).
Where(sq.Eq{"SharedChannelUsers.ChannelId": channelID}).
ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getsharedchannelsingleuser_tosql")
}
if err := s.GetReplicaX().Get(&scu, squery, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("SharedChannelUser", userID)
}
return nil, errors.Wrapf(err, "failed to find shared channel user with UserId=%s, ChannelId=%s, RemoteId=%s", userID, channelID, remoteID)
}
return &scu, nil
}
// GetUsersForUser fetches all shared channel user records based on userID.
func (s SqlSharedChannelStore) GetUsersForUser(userID string) ([]*model.SharedChannelUser, error) {
squery, args, err := s.getQueryBuilder().
Select("*").
From("SharedChannelUsers").
Where(sq.Eq{"SharedChannelUsers.UserId": userID}).
ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getsharedchanneluser_tosql")
}
users := []*model.SharedChannelUser{}
if err := s.GetReplicaX().Select(&users, squery, args...); err != nil {
if err == sql.ErrNoRows {
return make([]*model.SharedChannelUser, 0), nil
}
return nil, errors.Wrapf(err, "failed to find shared channel user with UserId=%s", userID)
}
return users, nil
}
// GetUsersForSync fetches all shared channel users that need to be synchronized, meaning their
// `SharedChannelUsers.LastSyncAt` is less than or equal to `User.UpdateAt`.
func (s SqlSharedChannelStore) GetUsersForSync(filter model.GetUsersForSyncFilter) ([]*model.User, error) {
if filter.Limit <= 0 {
filter.Limit = DefaultGetUsersForSyncLimit
}
query := s.getQueryBuilder().
Select("u.*").
Distinct().
From("Users AS u").
Join("SharedChannelUsers AS scu ON u.Id = scu.UserId").
OrderBy("u.Id").
Limit(filter.Limit)
if filter.CheckProfileImage {
query = query.Where("scu.LastSyncAt < u.LastPictureUpdate")
} else {
query = query.Where("scu.LastSyncAt < u.UpdateAt")
}
if filter.ChannelID != "" {
query = query.Where(sq.Eq{"scu.ChannelId": filter.ChannelID})
}
sqlQuery, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getsharedchannelusersforsync_tosql")
}
users := []*model.User{}
if err := s.GetReplicaX().Select(&users, sqlQuery, args...); err != nil {
if err == sql.ErrNoRows {
return make([]*model.User, 0), nil
}
return nil, errors.Wrapf(err, "failed to fetch shared channel users with ChannelId=%s",
filter.ChannelID)
}
return users, nil
}
// UpdateUserLastSyncAt updates the LastSyncAt timestamp for the specified SharedChannelUser.
func (s SqlSharedChannelStore) UpdateUserLastSyncAt(userID string, channelID string, remoteID string) error {
var query string
if s.DriverName() == model.DatabaseDriverPostgres {
query = `
UPDATE
SharedChannelUsers AS scu
SET
LastSyncAt = GREATEST(Users.UpdateAt, Users.LastPictureUpdate)
FROM
Users
WHERE
Users.Id = scu.UserId AND scu.UserId = ? AND scu.ChannelId = ? AND scu.RemoteId = ?
`
} else if s.DriverName() == model.DatabaseDriverMysql {
query = `
UPDATE
SharedChannelUsers AS scu
INNER JOIN
Users ON scu.UserId = Users.Id
SET
LastSyncAt = GREATEST(Users.UpdateAt, Users.LastPictureUpdate)
WHERE
scu.UserId = ? AND scu.ChannelId = ? AND scu.RemoteId = ?
`
} else {
return errors.New("unsupported DB driver " + s.DriverName())
}
result, err := s.GetMasterX().Exec(query, userID, channelID, remoteID)
if err != nil {
return fmt.Errorf("failed to update LastSyncAt for SharedChannelUser with userId=%s, channelId=%s, remoteId=%s: %w",
userID, channelID, remoteID, err)
}
count, err := result.RowsAffected()
if err != nil {
return errors.Wrap(err, "failed to determine rows affected")
}
if count == 0 {
return fmt.Errorf("SharedChannelUser not found: userId=%s, channelId=%s, remoteId=%s", userID, channelID, remoteID)
}
return nil
}
// SaveAttachment inserts a new shared channel file attachment record to the SharedChannelFiles table.
func (s SqlSharedChannelStore) SaveAttachment(attachment *model.SharedChannelAttachment) (*model.SharedChannelAttachment, error) {
attachment.PreSave()
if err := attachment.IsValid(); err != nil {
return nil, err
}
query, args, err := s.getQueryBuilder().Insert("SharedChannelAttachments").
Columns("Id", "FileId", "RemoteId", "CreateAt", "LastSyncAt").
Values(attachment.Id, attachment.FileId, attachment.RemoteId, attachment.CreateAt, attachment.LastSyncAt).
ToSql()
if err != nil {
return nil, errors.Wrapf(err, "savesahredchannelattachment_tosql")
}
if _, err := s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrapf(err, "save_shared_channel_attachment: file_id=%s, remote_id=%s", attachment.FileId, attachment.RemoteId)
}
return attachment, nil
}
// UpsertAttachment inserts a new shared channel file attachment record to the SharedChannelFiles table or updates its
// LastSyncAt.
func (s SqlSharedChannelStore) UpsertAttachment(attachment *model.SharedChannelAttachment) (string, error) {
attachment.PreSave()
if err := attachment.IsValid(); err != nil {
return "", err
}
query := s.getQueryBuilder().
Insert("SharedChannelAttachments").
Columns("Id", "FileId", "RemoteId", "CreateAt", "LastSyncAt").
Values(attachment.Id, attachment.FileId, attachment.RemoteId, attachment.CreateAt, attachment.LastSyncAt)
if s.DriverName() == model.DatabaseDriverMysql {
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE LastSyncAt = ?", attachment.LastSyncAt))
} else if s.DriverName() == model.DatabaseDriverPostgres {
query = query.SuffixExpr(sq.Expr("ON CONFLICT (id) DO UPDATE SET LastSyncAt = ?", attachment.LastSyncAt))
}
queryString, args, err := query.ToSql()
if err != nil {
return "", errors.Wrap(err, "upsertsharedchannelattachment_tosql")
}
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
return "", errors.Wrap(err, "failed to upsert SharedChannelAttachments")
}
return attachment.Id, nil
}
// GetAttachment fetches a shared channel file attachment record based on file_id and remoteId.
func (s SqlSharedChannelStore) GetAttachment(fileId string, remoteId string) (*model.SharedChannelAttachment, error) {
var attachment model.SharedChannelAttachment
squery, args, err := s.getQueryBuilder().
Select("*").
From("SharedChannelAttachments").
Where(sq.Eq{"SharedChannelAttachments.FileId": fileId}).
Where(sq.Eq{"SharedChannelAttachments.RemoteId": remoteId}).
ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getsharedchannelattachment_tosql")
}
if err := s.GetReplicaX().Get(&attachment, squery, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("SharedChannelAttachment", fileId)
}
return nil, errors.Wrapf(err, "failed to find shared channel attachment with FileId=%s, RemoteId=%s", fileId, remoteId)
}
return &attachment, nil
}
// UpdateAttachmentLastSyncAt updates the LastSyncAt timestamp for the specified SharedChannelAttachment.
func (s SqlSharedChannelStore) UpdateAttachmentLastSyncAt(id string, syncTime int64) error {
squery, args, err := s.getQueryBuilder().
Update("SharedChannelAttachments").
Set("LastSyncAt", syncTime).
Where(sq.Eq{"Id": id}).
ToSql()
if err != nil {
return errors.Wrap(err, "update_shared_channel_attachment_last_sync_at_tosql")
}
result, err := s.GetMasterX().Exec(squery, args...)
if err != nil {
return errors.Wrap(err, "failed to update LastSyncAt for SharedChannelAttachment")
}
count, err := result.RowsAffected()
if err != nil {
return errors.Wrap(err, "failed to determine rows affected")
}
if count == 0 {
return fmt.Errorf("id not found: %s", id)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
"regexp"
"strconv"
"strings"
"time"
"unicode"
"github.com/jmoiron/sqlx"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type StoreTestWrapper struct {
orig *SqlStore
}
func NewStoreTestWrapper(orig *SqlStore) *StoreTestWrapper {
return &StoreTestWrapper{orig}
}
func (w *StoreTestWrapper) GetMasterX() storetest.SqlXExecutor {
return w.orig.GetMasterX()
}
func (w *StoreTestWrapper) DriverName() string {
return w.orig.DriverName()
}
type Builder interface {
ToSql() (string, []any, error)
}
// sqlxExecutor exposes sqlx operations. It is used to enable some internal store methods to
// accept both transactions (*sqlxTxWrapper) and common db handlers (*sqlxDbWrapper).
type sqlxExecutor interface {
Get(dest any, query string, args ...any) error
GetBuilder(dest any, builder Builder) error
NamedExec(query string, arg any) (sql.Result, error)
Exec(query string, args ...any) (sql.Result, error)
ExecBuilder(builder Builder) (sql.Result, error)
ExecRaw(query string, args ...any) (sql.Result, error)
NamedQuery(query string, arg any) (*sqlx.Rows, error)
QueryRowX(query string, args ...any) *sqlx.Row
QueryX(query string, args ...any) (*sqlx.Rows, error)
Select(dest any, query string, args ...any) error
SelectBuilder(dest any, builder Builder) error
}
// namedParamRegex is used to capture all named parameters and convert them
// to lowercase. This is necessary to be able to use a single query for both
// Postgres and MySQL.
// This will also lowercase any constant strings containing a :, but sqlx
// will fail the query, so it won't be checked in inadvertently.
var namedParamRegex = regexp.MustCompile(`:\w+`)
type sqlxDBWrapper struct {
*sqlx.DB
queryTimeout time.Duration
trace bool
}
func newSqlxDBWrapper(db *sqlx.DB, timeout time.Duration, trace bool) *sqlxDBWrapper {
return &sqlxDBWrapper{
DB: db,
queryTimeout: timeout,
trace: trace,
}
}
func (w *sqlxDBWrapper) Stats() sql.DBStats {
return w.DB.Stats()
}
func (w *sqlxDBWrapper) Beginx() (*sqlxTxWrapper, error) {
tx, err := w.DB.Beginx()
if err != nil {
return nil, err
}
return newSqlxTxWrapper(tx, w.queryTimeout, w.trace), nil
}
func (w *sqlxDBWrapper) BeginXWithIsolation(opts *sql.TxOptions) (*sqlxTxWrapper, error) {
tx, err := w.DB.BeginTxx(context.Background(), opts)
if err != nil {
return nil, err
}
return newSqlxTxWrapper(tx, w.queryTimeout, w.trace), nil
}
func (w *sqlxDBWrapper) Get(dest any, query string, args ...any) error {
query = w.DB.Rebind(query)
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.DB.GetContext(ctx, dest, query, args...)
}
func (w *sqlxDBWrapper) GetBuilder(dest any, builder Builder) error {
query, args, err := builder.ToSql()
if err != nil {
return err
}
return w.Get(dest, query, args...)
}
func (w *sqlxDBWrapper) NamedExec(query string, arg any) (sql.Result, error) {
if w.DB.DriverName() == model.DatabaseDriverPostgres {
query = namedParamRegex.ReplaceAllStringFunc(query, strings.ToLower)
}
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), arg)
}(time.Now())
}
return w.DB.NamedExecContext(ctx, query, arg)
}
func (w *sqlxDBWrapper) Exec(query string, args ...any) (sql.Result, error) {
query = w.DB.Rebind(query)
return w.ExecRaw(query, args...)
}
func (w *sqlxDBWrapper) ExecBuilder(builder Builder) (sql.Result, error) {
query, args, err := builder.ToSql()
if err != nil {
return nil, err
}
return w.Exec(query, args...)
}
func (w *sqlxDBWrapper) ExecNoTimeout(query string, args ...any) (sql.Result, error) {
query = w.DB.Rebind(query)
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.DB.ExecContext(context.Background(), query, args...)
}
// ExecRaw is like Exec but without any rebinding of params. You need to pass
// the exact param types of your target database.
func (w *sqlxDBWrapper) ExecRaw(query string, args ...any) (sql.Result, error) {
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.DB.ExecContext(ctx, query, args...)
}
func (w *sqlxDBWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) {
if w.DB.DriverName() == model.DatabaseDriverPostgres {
query = namedParamRegex.ReplaceAllStringFunc(query, strings.ToLower)
}
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), arg)
}(time.Now())
}
return w.DB.NamedQueryContext(ctx, query, arg)
}
func (w *sqlxDBWrapper) QueryRowX(query string, args ...any) *sqlx.Row {
query = w.DB.Rebind(query)
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.DB.QueryRowxContext(ctx, query, args...)
}
func (w *sqlxDBWrapper) QueryX(query string, args ...any) (*sqlx.Rows, error) {
query = w.DB.Rebind(query)
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.DB.QueryxContext(ctx, query, args)
}
func (w *sqlxDBWrapper) Select(dest any, query string, args ...any) error {
return w.SelectCtx(context.Background(), dest, query, args...)
}
func (w *sqlxDBWrapper) SelectCtx(ctx context.Context, dest any, query string, args ...any) error {
query = w.DB.Rebind(query)
ctx, cancel := context.WithTimeout(ctx, w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.DB.SelectContext(ctx, dest, query, args...)
}
func (w *sqlxDBWrapper) SelectBuilder(dest any, builder Builder) error {
query, args, err := builder.ToSql()
if err != nil {
return err
}
return w.Select(dest, query, args...)
}
type sqlxTxWrapper struct {
*sqlx.Tx
queryTimeout time.Duration
trace bool
}
func newSqlxTxWrapper(tx *sqlx.Tx, timeout time.Duration, trace bool) *sqlxTxWrapper {
return &sqlxTxWrapper{
Tx: tx,
queryTimeout: timeout,
trace: trace,
}
}
func (w *sqlxTxWrapper) Get(dest any, query string, args ...any) error {
query = w.Tx.Rebind(query)
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.Tx.GetContext(ctx, dest, query, args...)
}
func (w *sqlxTxWrapper) GetBuilder(dest any, builder Builder) error {
query, args, err := builder.ToSql()
if err != nil {
return err
}
return w.Get(dest, query, args...)
}
func (w *sqlxTxWrapper) Exec(query string, args ...any) (sql.Result, error) {
query = w.Tx.Rebind(query)
return w.ExecRaw(query, args...)
}
func (w *sqlxTxWrapper) ExecNoTimeout(query string, args ...any) (sql.Result, error) {
query = w.Tx.Rebind(query)
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.Tx.ExecContext(context.Background(), query, args...)
}
func (w *sqlxTxWrapper) ExecBuilder(builder Builder) (sql.Result, error) {
query, args, err := builder.ToSql()
if err != nil {
return nil, err
}
return w.Exec(query, args...)
}
// ExecRaw is like Exec but without any rebinding of params. You need to pass
// the exact param types of your target database.
func (w *sqlxTxWrapper) ExecRaw(query string, args ...any) (sql.Result, error) {
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.Tx.ExecContext(ctx, query, args...)
}
func (w *sqlxTxWrapper) NamedExec(query string, arg any) (sql.Result, error) {
if w.Tx.DriverName() == model.DatabaseDriverPostgres {
query = namedParamRegex.ReplaceAllStringFunc(query, strings.ToLower)
}
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), arg)
}(time.Now())
}
return w.Tx.NamedExecContext(ctx, query, arg)
}
func (w *sqlxTxWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) {
if w.Tx.DriverName() == model.DatabaseDriverPostgres {
query = namedParamRegex.ReplaceAllStringFunc(query, strings.ToLower)
}
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), arg)
}(time.Now())
}
// There is no tx.NamedQueryContext support in the sqlx API. (https://github.com/jmoiron/sqlx/issues/447)
// So we need to implement this ourselves.
type result struct {
rows *sqlx.Rows
err error
}
// Need to add a buffer of 1 to prevent goroutine leak.
resChan := make(chan *result, 1)
go func() {
rows, err := w.Tx.NamedQuery(query, arg)
resChan <- &result{
rows: rows,
err: err,
}
}()
// staticcheck fails to check that res gets re-assigned later.
res := &result{} //nolint:staticcheck
select {
case res = <-resChan:
case <-ctx.Done():
res = &result{
rows: nil,
err: ctx.Err(),
}
}
return res.rows, res.err
}
func (w *sqlxTxWrapper) QueryRowX(query string, args ...any) *sqlx.Row {
query = w.Tx.Rebind(query)
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.Tx.QueryRowxContext(ctx, query, args...)
}
func (w *sqlxTxWrapper) QueryX(query string, args ...any) (*sqlx.Rows, error) {
query = w.Tx.Rebind(query)
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.Tx.QueryxContext(ctx, query, args)
}
func (w *sqlxTxWrapper) Select(dest any, query string, args ...any) error {
query = w.Tx.Rebind(query)
ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout)
defer cancel()
if w.trace {
defer func(then time.Time) {
printArgs(query, time.Since(then), args)
}(time.Now())
}
return w.Tx.SelectContext(ctx, dest, query, args...)
}
func (w *sqlxTxWrapper) SelectBuilder(dest any, builder Builder) error {
query, args, err := builder.ToSql()
if err != nil {
return err
}
return w.Select(dest, query, args...)
}
func removeSpace(r rune) rune {
// Strip everything except ' '
// This also strips out more than one space,
// but we ignore it for now until someone complains.
if unicode.IsSpace(r) && r != ' ' {
return -1
}
return r
}
func printArgs(query string, dur time.Duration, args ...any) {
query = strings.Map(removeSpace, query)
fields := make([]mlog.Field, 0, len(args)+1)
fields = append(fields, mlog.Duration("duration", dur))
for i, arg := range args {
fields = append(fields, mlog.Any("arg"+strconv.Itoa(i), arg))
}
mlog.Debug(query, fields...)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"time"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlStatusStore struct {
*SqlStore
}
func newSqlStatusStore(sqlStore *SqlStore) store.StatusStore {
return &SqlStatusStore{sqlStore}
}
func (s SqlStatusStore) SaveOrUpdate(st *model.Status) error {
query := s.getQueryBuilder().
Insert("Status").
Columns("UserId", "Status", "Manual", "LastActivityAt", "DNDEndTime", "PrevStatus").
Values(st.UserId, st.Status, st.Manual, st.LastActivityAt, st.DNDEndTime, st.PrevStatus)
if s.DriverName() == model.DatabaseDriverMysql {
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE Status = ?, Manual = ?, LastActivityAt = ?, DNDEndTime = ?, PrevStatus = ?",
st.Status, st.Manual, st.LastActivityAt, st.DNDEndTime, st.PrevStatus))
} else {
query = query.SuffixExpr(sq.Expr("ON CONFLICT (userid) DO UPDATE SET Status = ?, Manual = ?, LastActivityAt = ?, DNDEndTime = ?, PrevStatus = ?",
st.Status, st.Manual, st.LastActivityAt, st.DNDEndTime, st.PrevStatus))
}
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "status_tosql")
}
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrap(err, "failed to upsert Status")
}
return nil
}
func (s SqlStatusStore) Get(userId string) (*model.Status, error) {
var status model.Status
if err := s.GetReplicaX().Get(&status, "SELECT * FROM Status WHERE UserId = ?", userId); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Status", fmt.Sprintf("userId=%s", userId))
}
return nil, errors.Wrapf(err, "failed to get Status with userId=%s", userId)
}
return &status, nil
}
func (s SqlStatusStore) GetByIds(userIds []string) ([]*model.Status, error) {
query := s.getQueryBuilder().
Select("UserId, Status, Manual, LastActivityAt").
From("Status").
Where(sq.Eq{"UserId": userIds})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "status_tosql")
}
rows, err := s.GetReplicaX().DB.Query(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Statuses")
}
statuses := []*model.Status{}
defer rows.Close()
for rows.Next() {
var status model.Status
if err = rows.Scan(&status.UserId, &status.Status, &status.Manual, &status.LastActivityAt); err != nil {
return nil, errors.Wrap(err, "unable to scan from rows")
}
statuses = append(statuses, &status)
}
if err = rows.Err(); err != nil {
return nil, errors.Wrap(err, "failed while iterating over rows")
}
return statuses, nil
}
// MySQL doesn't have support for RETURNING clause, so we use a transaction to get the updated rows.
func (s SqlStatusStore) updateExpiredStatuses(t *sqlxTxWrapper) ([]*model.Status, error) {
statuses := []*model.Status{}
currUnixTime := time.Now().UTC().Unix()
selectQuery, selectParams, err := s.getQueryBuilder().
Select("*").
From("Status").
Where(
sq.And{
sq.Eq{"Status": model.StatusDnd},
sq.Gt{"DNDEndTime": 0},
sq.LtOrEq{"DNDEndTime": currUnixTime},
},
).ToSql()
if err != nil {
return nil, errors.Wrap(err, "status_tosql")
}
err = t.Select(&statuses, selectQuery, selectParams...)
if err != nil {
return nil, errors.Wrap(err, "updateExpiredStatusesT: failed to get expired dnd statuses")
}
updateQuery, args, err := s.getQueryBuilder().
Update("Status").
Where(
sq.And{
sq.Eq{"Status": model.StatusDnd},
sq.Gt{"DNDEndTime": 0},
sq.LtOrEq{"DNDEndTime": currUnixTime},
},
).
Set("Status", sq.Expr("PrevStatus")).
Set("PrevStatus", model.StatusDnd).
Set("DNDEndTime", 0).
Set("Manual", false).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "status_tosql")
}
if _, err := t.Exec(updateQuery, args...); err != nil {
return nil, errors.Wrapf(err, "updateExpiredStatusesT: failed to update statuses")
}
return statuses, nil
}
func (s SqlStatusStore) UpdateExpiredDNDStatuses() (_ []*model.Status, err error) {
if s.DriverName() == model.DatabaseDriverMysql {
transaction, terr := s.GetMasterX().Beginx()
if terr != nil {
return nil, errors.Wrap(terr, "UpdateExpiredDNDStatuses: begin_transaction")
}
defer finalizeTransactionX(transaction, &terr)
statuses, terr := s.updateExpiredStatuses(transaction)
if terr != nil {
return nil, errors.Wrap(terr, "UpdateExpiredDNDStatuses: updateExpiredDNDStatusesT")
}
if terr = transaction.Commit(); terr != nil {
return nil, errors.Wrap(terr, "UpdateExpiredDNDStatuses: commit_transaction")
}
for _, status := range statuses {
status.Status = status.PrevStatus
status.PrevStatus = model.StatusDnd
status.DNDEndTime = 0
status.Manual = false
}
return statuses, nil
}
queryString, args, err := s.getQueryBuilder().
Update("Status").
Where(
sq.And{
sq.Eq{"Status": model.StatusDnd},
sq.Gt{"DNDEndTime": 0},
sq.LtOrEq{"DNDEndTime": time.Now().UTC().Unix()},
},
).
Set("Status", sq.Expr("PrevStatus")).
Set("PrevStatus", model.StatusDnd).
Set("DNDEndTime", 0).
Set("Manual", false).
Suffix("RETURNING *").
ToSql()
if err != nil {
return nil, errors.Wrap(err, "status_tosql")
}
rows, err := s.GetMasterX().Query(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Statuses")
}
defer rows.Close()
statuses := []*model.Status{}
for rows.Next() {
var status model.Status
if err = rows.Scan(&status.UserId, &status.Status, &status.Manual, &status.LastActivityAt,
&status.DNDEndTime, &status.PrevStatus); err != nil {
return nil, errors.Wrap(err, "unable to scan from rows")
}
statuses = append(statuses, &status)
}
if err = rows.Err(); err != nil {
return nil, errors.Wrap(err, "failed while iterating over rows")
}
return statuses, nil
}
func (s SqlStatusStore) ResetAll() error {
if _, err := s.GetMasterX().Exec("UPDATE Status SET Status = ? WHERE Manual = false", model.StatusOffline); err != nil {
return errors.Wrap(err, "failed to update Statuses")
}
return nil
}
func (s SqlStatusStore) GetTotalActiveUsersCount() (int64, error) {
time := model.GetMillis() - (1000 * 60 * 60 * 24)
var count int64
err := s.GetReplicaX().Get(&count, "SELECT COUNT(UserId) FROM Status WHERE LastActivityAt > ?", time)
if err != nil {
return count, errors.Wrap(err, "failed to count active users")
}
return count, nil
}
func (s SqlStatusStore) UpdateLastActivityAt(userId string, lastActivityAt int64) error {
if _, err := s.GetMasterX().Exec("UPDATE Status SET LastActivityAt = ? WHERE UserId = ?", lastActivityAt, userId); err != nil {
return errors.Wrapf(err, "failed to update last activity for userId=%s", userId)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
dbsql "database/sql"
"fmt"
"log"
"path"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/mattermost/morph"
sq "github.com/mattermost/squirrel"
"github.com/mattermost/morph/drivers"
ms "github.com/mattermost/morph/drivers/mysql"
ps "github.com/mattermost/morph/drivers/postgres"
"github.com/go-sql-driver/mysql"
_ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
mbindata "github.com/mattermost/morph/sources/embedded"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/db"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type migrationDirection string
const (
IndexTypeFullText = "full_text"
IndexTypeFullTextFunc = "full_text_func"
IndexTypeDefault = "default"
PGDupTableErrorCode = "42P07" // see https://github.com/lib/pq/blob/master/error.go#L268
MySQLDupTableErrorCode = uint16(1050) // see https://dev.mysql.com/doc/mysql-errors/5.7/en/server-error-reference.html#error_er_table_exists_error
PGForeignKeyViolationErrorCode = "23503"
MySQLForeignKeyViolationErrorCode = 1452
PGDuplicateObjectErrorCode = "42710"
MySQLDuplicateObjectErrorCode = 1022
DBPingAttempts = 18
DBPingTimeoutSecs = 10
// This is a numerical version string by postgres. The format is
// 2 characters for major, minor, and patch version prior to 10.
// After 10, it's major and minor only.
// 10.1 would be 100001.
// 9.6.3 would be 90603.
minimumRequiredPostgresVersion = 100000
// major*1000 + minor*100 + patch
minimumRequiredMySQLVersion = 5712
migrationsDirectionUp migrationDirection = "up"
migrationsDirectionDown migrationDirection = "down"
replicaLagPrefix = "replica-lag"
RemoteClusterSiteURLUniqueIndex = "remote_clusters_site_url_unique"
)
var tablesToCheckForCollation = []string{"incomingwebhooks", "preferences", "users", "uploadsessions", "channels", "publicchannels"}
type SqlStoreStores struct {
team store.TeamStore
channel store.ChannelStore
post store.PostStore
retentionPolicy store.RetentionPolicyStore
thread store.ThreadStore
user store.UserStore
bot store.BotStore
audit store.AuditStore
cluster store.ClusterDiscoveryStore
remoteCluster store.RemoteClusterStore
compliance store.ComplianceStore
session store.SessionStore
oauth store.OAuthStore
system store.SystemStore
webhook store.WebhookStore
command store.CommandStore
commandWebhook store.CommandWebhookStore
preference store.PreferenceStore
license store.LicenseStore
token store.TokenStore
emoji store.EmojiStore
status store.StatusStore
fileInfo store.FileInfoStore
uploadSession store.UploadSessionStore
reaction store.ReactionStore
job store.JobStore
userAccessToken store.UserAccessTokenStore
plugin store.PluginStore
channelMemberHistory store.ChannelMemberHistoryStore
role store.RoleStore
scheme store.SchemeStore
TermsOfService store.TermsOfServiceStore
productNotices store.ProductNoticesStore
group store.GroupStore
UserTermsOfService store.UserTermsOfServiceStore
linkMetadata store.LinkMetadataStore
sharedchannel store.SharedChannelStore
draft store.DraftStore
notifyAdmin store.NotifyAdminStore
postPriority store.PostPriorityStore
postAcknowledgement store.PostAcknowledgementStore
trueUpReview store.TrueUpReviewStore
}
type SqlStore struct {
// rrCounter and srCounter should be kept first.
// See https://github.com/mattermost/mattermost-server/v6/server/channels/pull/7281
rrCounter int64
srCounter int64
masterX *sqlxDBWrapper
ReplicaXs []*sqlxDBWrapper
searchReplicaXs []*sqlxDBWrapper
replicaLagHandles []*dbsql.DB
stores SqlStoreStores
settings *model.SqlSettings
lockedToMaster bool
context context.Context
license *model.License
licenseMutex sync.RWMutex
metrics einterfaces.MetricsInterface
isBinaryParam bool
pgDefaultTextSearchConfig string
}
func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlStore {
store := &SqlStore{
rrCounter: 0,
srCounter: 0,
settings: &settings,
metrics: metrics,
}
store.initConnection()
ver, err := store.GetDbVersion(true)
if err != nil {
mlog.Fatal("Error while getting DB version.", mlog.Err(err))
}
ok, err := store.ensureMinimumDBVersion(ver)
if !ok {
mlog.Fatal("Error while checking DB version.", mlog.Err(err))
}
err = store.ensureDatabaseCollation()
if err != nil {
mlog.Fatal("Error while checking DB collation.", mlog.Err(err))
}
err = store.migrate(migrationsDirectionUp)
if err != nil {
mlog.Fatal("Failed to apply database migrations.", mlog.Err(err))
}
store.isBinaryParam, err = store.computeBinaryParam()
if err != nil {
mlog.Fatal("Failed to compute binary param", mlog.Err(err))
}
store.pgDefaultTextSearchConfig, err = store.computeDefaultTextSearchConfig()
if err != nil {
mlog.Fatal("Failed to compute default text search config", mlog.Err(err))
}
store.stores.team = newSqlTeamStore(store)
store.stores.channel = newSqlChannelStore(store, metrics)
store.stores.post = newSqlPostStore(store, metrics)
store.stores.retentionPolicy = newSqlRetentionPolicyStore(store, metrics)
store.stores.user = newSqlUserStore(store, metrics)
store.stores.bot = newSqlBotStore(store, metrics)
store.stores.audit = newSqlAuditStore(store)
store.stores.cluster = newSqlClusterDiscoveryStore(store)
store.stores.remoteCluster = newSqlRemoteClusterStore(store)
store.stores.compliance = newSqlComplianceStore(store)
store.stores.session = newSqlSessionStore(store)
store.stores.oauth = newSqlOAuthStore(store)
store.stores.system = newSqlSystemStore(store)
store.stores.webhook = newSqlWebhookStore(store, metrics)
store.stores.command = newSqlCommandStore(store)
store.stores.commandWebhook = newSqlCommandWebhookStore(store)
store.stores.preference = newSqlPreferenceStore(store)
store.stores.license = newSqlLicenseStore(store)
store.stores.token = newSqlTokenStore(store)
store.stores.emoji = newSqlEmojiStore(store, metrics)
store.stores.status = newSqlStatusStore(store)
store.stores.fileInfo = newSqlFileInfoStore(store, metrics)
store.stores.uploadSession = newSqlUploadSessionStore(store)
store.stores.thread = newSqlThreadStore(store)
store.stores.job = newSqlJobStore(store)
store.stores.userAccessToken = newSqlUserAccessTokenStore(store)
store.stores.channelMemberHistory = newSqlChannelMemberHistoryStore(store)
store.stores.plugin = newSqlPluginStore(store)
store.stores.TermsOfService = newSqlTermsOfServiceStore(store, metrics)
store.stores.UserTermsOfService = newSqlUserTermsOfServiceStore(store)
store.stores.linkMetadata = newSqlLinkMetadataStore(store)
store.stores.sharedchannel = newSqlSharedChannelStore(store)
store.stores.reaction = newSqlReactionStore(store)
store.stores.role = newSqlRoleStore(store)
store.stores.scheme = newSqlSchemeStore(store)
store.stores.group = newSqlGroupStore(store)
store.stores.productNotices = newSqlProductNoticesStore(store)
store.stores.draft = newSqlDraftStore(store, metrics)
store.stores.notifyAdmin = newSqlNotifyAdminStore(store)
store.stores.postPriority = newSqlPostPriorityStore(store)
store.stores.postAcknowledgement = newSqlPostAcknowledgementStore(store)
store.stores.trueUpReview = newSqlTrueUpReviewStore(store)
store.stores.preference.(*SqlPreferenceStore).deleteUnusedFeatures()
return store
}
// SetupConnection sets up the connection to the database and pings it to make sure it's alive.
// It also applies any database configuration settings that are required.
func SetupConnection(connType string, dataSource string, settings *model.SqlSettings) *dbsql.DB {
db, err := dbsql.Open(*settings.DriverName, dataSource)
if err != nil {
mlog.Fatal("Failed to open SQL connection to err.", mlog.Err(err))
}
for i := 0; i < DBPingAttempts; i++ {
mlog.Info("Pinging SQL", mlog.String("database", connType), mlog.String("dataSource", dataSource))
ctx, cancel := context.WithTimeout(context.Background(), DBPingTimeoutSecs*time.Second)
defer cancel()
err = db.PingContext(ctx)
if err == nil {
break
} else {
if i == DBPingAttempts-1 {
mlog.Fatal("Failed to ping DB, server will exit.", mlog.Err(err))
} else {
mlog.Error("Failed to ping DB", mlog.Err(err), mlog.Int("retrying in seconds", DBPingTimeoutSecs))
time.Sleep(DBPingTimeoutSecs * time.Second)
}
}
}
if strings.HasPrefix(connType, replicaLagPrefix) {
// If this is a replica lag connection, we just open one connection.
//
// Arguably, if the query doesn't require a special credential, it does take up
// one extra connection from the replica DB. But falling back to the replica
// data source when the replica lag data source is null implies an ordering constraint
// which makes things brittle and is not a good design.
// If connections are an overhead, it is advised to use a connection pool.
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
} else {
db.SetMaxIdleConns(*settings.MaxIdleConns)
db.SetMaxOpenConns(*settings.MaxOpenConns)
}
db.SetConnMaxLifetime(time.Duration(*settings.ConnMaxLifetimeMilliseconds) * time.Millisecond)
db.SetConnMaxIdleTime(time.Duration(*settings.ConnMaxIdleTimeMilliseconds) * time.Millisecond)
return db
}
func (ss *SqlStore) SetContext(context context.Context) {
ss.context = context
}
func (ss *SqlStore) Context() context.Context {
return ss.context
}
func noOpMapper(s string) string { return s }
func (ss *SqlStore) initConnection() {
dataSource := *ss.settings.DataSource
if ss.DriverName() == model.DatabaseDriverMysql {
// TODO: We ignore the readTimeout datasource parameter for MySQL since QueryTimeout
// covers that already. Ideally we'd like to do this only for the upgrade
// step. To be reviewed in MM-35789.
var err error
dataSource, err = ResetReadTimeout(dataSource)
if err != nil {
mlog.Fatal("Failed to reset read timeout from datasource.", mlog.Err(err), mlog.String("src", dataSource))
}
}
handle := SetupConnection("master", dataSource, ss.settings)
ss.masterX = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()),
time.Duration(*ss.settings.QueryTimeout)*time.Second,
*ss.settings.Trace)
if ss.DriverName() == model.DatabaseDriverMysql {
ss.masterX.MapperFunc(noOpMapper)
}
if ss.metrics != nil {
ss.metrics.RegisterDBCollector(ss.masterX.DB.DB, "master")
}
if len(ss.settings.DataSourceReplicas) > 0 {
ss.ReplicaXs = make([]*sqlxDBWrapper, len(ss.settings.DataSourceReplicas))
for i, replica := range ss.settings.DataSourceReplicas {
handle := SetupConnection(fmt.Sprintf("replica-%v", i), replica, ss.settings)
ss.ReplicaXs[i] = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()),
time.Duration(*ss.settings.QueryTimeout)*time.Second,
*ss.settings.Trace)
if ss.DriverName() == model.DatabaseDriverMysql {
ss.ReplicaXs[i].MapperFunc(noOpMapper)
}
if ss.metrics != nil {
ss.metrics.RegisterDBCollector(ss.ReplicaXs[i].DB.DB, "replica-"+strconv.Itoa(i))
}
}
}
if len(ss.settings.DataSourceSearchReplicas) > 0 {
ss.searchReplicaXs = make([]*sqlxDBWrapper, len(ss.settings.DataSourceSearchReplicas))
for i, replica := range ss.settings.DataSourceSearchReplicas {
handle := SetupConnection(fmt.Sprintf("search-replica-%v", i), replica, ss.settings)
ss.searchReplicaXs[i] = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()),
time.Duration(*ss.settings.QueryTimeout)*time.Second,
*ss.settings.Trace)
if ss.DriverName() == model.DatabaseDriverMysql {
ss.searchReplicaXs[i].MapperFunc(noOpMapper)
}
if ss.metrics != nil {
ss.metrics.RegisterDBCollector(ss.searchReplicaXs[i].DB.DB, "searchreplica-"+strconv.Itoa(i))
}
}
}
if len(ss.settings.ReplicaLagSettings) > 0 {
ss.replicaLagHandles = make([]*dbsql.DB, len(ss.settings.ReplicaLagSettings))
for i, src := range ss.settings.ReplicaLagSettings {
if src.DataSource == nil {
continue
}
ss.replicaLagHandles[i] = SetupConnection(fmt.Sprintf(replicaLagPrefix+"-%d", i), *src.DataSource, ss.settings)
}
}
}
func (ss *SqlStore) DriverName() string {
return *ss.settings.DriverName
}
// specialSearchChars have special meaning and can be treated as spaces
func (ss *SqlStore) specialSearchChars() []string {
chars := []string{
"<",
">",
"+",
"-",
"(",
")",
"~",
":",
}
// Postgres can handle "@" without any errors
// Also helps postgres in enabling search for EmailAddresses
if ss.DriverName() != model.DatabaseDriverPostgres {
chars = append(chars, "@")
}
return chars
}
// computeBinaryParam returns whether the data source uses binary_parameters
// when using Postgres
func (ss *SqlStore) computeBinaryParam() (bool, error) {
if ss.DriverName() != model.DatabaseDriverPostgres {
return false, nil
}
return DSNHasBinaryParam(*ss.settings.DataSource)
}
func (ss *SqlStore) computeDefaultTextSearchConfig() (string, error) {
if ss.DriverName() != model.DatabaseDriverPostgres {
return "", nil
}
var defaultTextSearchConfig string
err := ss.GetMasterX().Get(&defaultTextSearchConfig, `SHOW default_text_search_config`)
return defaultTextSearchConfig, err
}
func (ss *SqlStore) IsBinaryParamEnabled() bool {
return ss.isBinaryParam
}
// GetDbVersion returns the version of the database being used.
// If numerical is set to true, it attempts to return a numerical version string
// that can be parsed by callers.
func (ss *SqlStore) GetDbVersion(numerical bool) (string, error) {
var sqlVersion string
if ss.DriverName() == model.DatabaseDriverPostgres {
if numerical {
sqlVersion = `SHOW server_version_num`
} else {
sqlVersion = `SHOW server_version`
}
} else if ss.DriverName() == model.DatabaseDriverMysql {
sqlVersion = `SELECT version()`
} else {
return "", errors.New("Not supported driver")
}
var version string
err := ss.GetReplicaX().Get(&version, sqlVersion)
if err != nil {
return "", err
}
return version, nil
}
func (ss *SqlStore) GetMasterX() *sqlxDBWrapper {
return ss.masterX
}
func (ss *SqlStore) SetMasterX(db *sql.DB) {
ss.masterX = newSqlxDBWrapper(sqlx.NewDb(db, ss.DriverName()),
time.Duration(*ss.settings.QueryTimeout)*time.Second,
*ss.settings.Trace)
if ss.DriverName() == model.DatabaseDriverMysql {
ss.masterX.MapperFunc(noOpMapper)
}
}
func (ss *SqlStore) GetInternalMasterDB() *sql.DB {
return ss.GetMasterX().DB.DB
}
func (ss *SqlStore) GetSearchReplicaX() *sqlxDBWrapper {
if !ss.hasLicense() {
return ss.GetMasterX()
}
if len(ss.settings.DataSourceSearchReplicas) == 0 {
return ss.GetReplicaX()
}
rrNum := atomic.AddInt64(&ss.srCounter, 1) % int64(len(ss.searchReplicaXs))
return ss.searchReplicaXs[rrNum]
}
func (ss *SqlStore) GetReplicaX() *sqlxDBWrapper {
if len(ss.settings.DataSourceReplicas) == 0 || ss.lockedToMaster || !ss.hasLicense() {
return ss.GetMasterX()
}
rrNum := atomic.AddInt64(&ss.rrCounter, 1) % int64(len(ss.ReplicaXs))
return ss.ReplicaXs[rrNum]
}
func (ss *SqlStore) GetInternalReplicaDBs() []*sql.DB {
if len(ss.settings.DataSourceReplicas) == 0 || ss.lockedToMaster || !ss.hasLicense() {
return []*sql.DB{
ss.GetMasterX().DB.DB,
}
}
dbs := make([]*sql.DB, len(ss.ReplicaXs))
for i, rx := range ss.ReplicaXs {
dbs[i] = rx.DB.DB
}
return dbs
}
func (ss *SqlStore) GetInternalReplicaDB() *sql.DB {
if len(ss.settings.DataSourceReplicas) == 0 || ss.lockedToMaster || !ss.hasLicense() {
return ss.GetMasterX().DB.DB
}
rrNum := atomic.AddInt64(&ss.rrCounter, 1) % int64(len(ss.ReplicaXs))
return ss.ReplicaXs[rrNum].DB.DB
}
func (ss *SqlStore) TotalMasterDbConnections() int {
return ss.GetMasterX().Stats().OpenConnections
}
// ReplicaLagAbs queries all the replica databases to get the absolute replica lag value
// and updates the Prometheus metric with it.
func (ss *SqlStore) ReplicaLagAbs() error {
for i, item := range ss.settings.ReplicaLagSettings {
if item.QueryAbsoluteLag == nil || *item.QueryAbsoluteLag == "" {
continue
}
var binDiff float64
var node string
err := ss.replicaLagHandles[i].QueryRow(*item.QueryAbsoluteLag).Scan(&node, &binDiff)
if err != nil {
return err
}
// There is no nil check needed here because it's called from the metrics store.
ss.metrics.SetReplicaLagAbsolute(node, binDiff)
}
return nil
}
// ReplicaLagAbs queries all the replica databases to get the time-based replica lag value
// and updates the Prometheus metric with it.
func (ss *SqlStore) ReplicaLagTime() error {
for i, item := range ss.settings.ReplicaLagSettings {
if item.QueryTimeLag == nil || *item.QueryTimeLag == "" {
continue
}
var timeDiff float64
var node string
err := ss.replicaLagHandles[i].QueryRow(*item.QueryTimeLag).Scan(&node, &timeDiff)
if err != nil {
return err
}
// There is no nil check needed here because it's called from the metrics store.
ss.metrics.SetReplicaLagTime(node, timeDiff)
}
return nil
}
func (ss *SqlStore) TotalReadDbConnections() int {
if len(ss.settings.DataSourceReplicas) == 0 {
return 0
}
count := 0
for _, db := range ss.ReplicaXs {
count = count + db.Stats().OpenConnections
}
return count
}
func (ss *SqlStore) TotalSearchDbConnections() int {
if len(ss.settings.DataSourceSearchReplicas) == 0 {
return 0
}
count := 0
for _, db := range ss.searchReplicaXs {
count = count + db.Stats().OpenConnections
}
return count
}
func (ss *SqlStore) MarkSystemRanUnitTests() {
props, err := ss.System().Get()
if err != nil {
return
}
unitTests := props[model.SystemRanUnitTests]
if unitTests == "" {
systemTests := &model.System{Name: model.SystemRanUnitTests, Value: "1"}
ss.System().Save(systemTests)
}
}
func (ss *SqlStore) DoesTableExist(tableName string) bool {
if ss.DriverName() == model.DatabaseDriverPostgres {
var count int64
err := ss.GetMasterX().Get(&count,
`SELECT count(relname) FROM pg_class WHERE relname=$1`,
strings.ToLower(tableName),
)
if err != nil {
mlog.Fatal("Failed to check if table exists", mlog.Err(err))
}
return count > 0
} else if ss.DriverName() == model.DatabaseDriverMysql {
var count int64
err := ss.GetMasterX().Get(&count,
`SELECT
COUNT(0) AS table_exists
FROM
information_schema.TABLES
WHERE
TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
`,
tableName,
)
if err != nil {
mlog.Fatal("Failed to check if table exists", mlog.Err(err))
}
return count > 0
} else {
mlog.Fatal("Failed to check if column exists because of missing driver")
return false
}
}
func (ss *SqlStore) DoesColumnExist(tableName string, columnName string) bool {
if ss.DriverName() == model.DatabaseDriverPostgres {
var count int64
err := ss.GetMasterX().Get(&count,
`SELECT COUNT(0)
FROM pg_attribute
WHERE attrelid = $1::regclass
AND attname = $2
AND NOT attisdropped`,
strings.ToLower(tableName),
strings.ToLower(columnName),
)
if err != nil {
if err.Error() == "pq: relation \""+strings.ToLower(tableName)+"\" does not exist" {
return false
}
mlog.Fatal("Failed to check if column exists", mlog.Err(err))
}
return count > 0
} else if ss.DriverName() == model.DatabaseDriverMysql {
var count int64
err := ss.GetMasterX().Get(&count,
`SELECT
COUNT(0) AS column_exists
FROM
information_schema.COLUMNS
WHERE
TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
AND COLUMN_NAME = ?`,
tableName,
columnName,
)
if err != nil {
mlog.Fatal("Failed to check if column exists", mlog.Err(err))
}
return count > 0
} else {
mlog.Fatal("Failed to check if column exists because of missing driver")
return false
}
}
func (ss *SqlStore) DoesTriggerExist(triggerName string) bool {
if ss.DriverName() == model.DatabaseDriverPostgres {
var count int64
err := ss.GetMasterX().Get(&count, `
SELECT
COUNT(0)
FROM
pg_trigger
WHERE
tgname = $1
`, triggerName)
if err != nil {
mlog.Fatal("Failed to check if trigger exists", mlog.Err(err))
}
return count > 0
} else if ss.DriverName() == model.DatabaseDriverMysql {
var count int64
err := ss.GetMasterX().Get(&count, `
SELECT
COUNT(0)
FROM
information_schema.triggers
WHERE
trigger_schema = DATABASE()
AND trigger_name = ?
`, triggerName)
if err != nil {
mlog.Fatal("Failed to check if trigger exists", mlog.Err(err))
}
return count > 0
} else {
mlog.Fatal("Failed to check if column exists because of missing driver")
return false
}
}
func (ss *SqlStore) CreateColumnIfNotExists(tableName string, columnName string, mySqlColType string, postgresColType string, defaultValue string) bool {
if ss.DoesColumnExist(tableName, columnName) {
return false
}
if ss.DriverName() == model.DatabaseDriverPostgres {
_, err := ss.GetMasterX().ExecNoTimeout("ALTER TABLE " + tableName + " ADD " + columnName + " " + postgresColType + " DEFAULT '" + defaultValue + "'")
if err != nil {
mlog.Fatal("Failed to create column", mlog.Err(err))
}
return true
} else if ss.DriverName() == model.DatabaseDriverMysql {
_, err := ss.GetMasterX().ExecNoTimeout("ALTER TABLE " + tableName + " ADD " + columnName + " " + mySqlColType + " DEFAULT '" + defaultValue + "'")
if err != nil {
mlog.Fatal("Failed to create column", mlog.Err(err))
}
return true
} else {
mlog.Fatal("Failed to create column because of missing driver")
return false
}
}
func (ss *SqlStore) RemoveTableIfExists(tableName string) bool {
if !ss.DoesTableExist(tableName) {
return false
}
_, err := ss.GetMasterX().ExecNoTimeout("DROP TABLE " + tableName)
if err != nil {
mlog.Fatal("Failed to drop table", mlog.Err(err))
}
return true
}
func IsConstraintAlreadyExistsError(err error) bool {
switch dbErr := err.(type) {
case *pq.Error:
if dbErr.Code == PGDuplicateObjectErrorCode {
return true
}
case *mysql.MySQLError:
if dbErr.Number == MySQLDuplicateObjectErrorCode {
return true
}
}
return false
}
func IsUniqueConstraintError(err error, indexName []string) bool {
unique := false
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code == "23505" {
unique = true
}
if mysqlErr, ok := err.(*mysql.MySQLError); ok && mysqlErr.Number == 1062 {
unique = true
}
field := false
for _, contain := range indexName {
if strings.Contains(err.Error(), contain) {
field = true
break
}
}
return unique && field
}
func (ss *SqlStore) GetAllConns() []*sqlxDBWrapper {
all := make([]*sqlxDBWrapper, len(ss.ReplicaXs)+1)
copy(all, ss.ReplicaXs)
all[len(ss.ReplicaXs)] = ss.masterX
return all
}
// RecycleDBConnections closes active connections by setting the max conn lifetime
// to d, and then resets them back to their original duration.
func (ss *SqlStore) RecycleDBConnections(d time.Duration) {
// Get old time.
originalDuration := time.Duration(*ss.settings.ConnMaxLifetimeMilliseconds) * time.Millisecond
// Set the max lifetimes for all connections.
for _, conn := range ss.GetAllConns() {
conn.SetConnMaxLifetime(d)
}
// Wait for that period with an additional 2 seconds of scheduling delay.
time.Sleep(d + 2*time.Second)
// Reset max lifetime back to original value.
for _, conn := range ss.GetAllConns() {
conn.SetConnMaxLifetime(originalDuration)
}
}
func (ss *SqlStore) Close() {
ss.masterX.Close()
for _, replica := range ss.ReplicaXs {
replica.Close()
}
for _, replica := range ss.searchReplicaXs {
replica.Close()
}
}
func (ss *SqlStore) LockToMaster() {
ss.lockedToMaster = true
}
func (ss *SqlStore) UnlockFromMaster() {
ss.lockedToMaster = false
}
func (ss *SqlStore) Team() store.TeamStore {
return ss.stores.team
}
func (ss *SqlStore) Channel() store.ChannelStore {
return ss.stores.channel
}
func (ss *SqlStore) Post() store.PostStore {
return ss.stores.post
}
func (ss *SqlStore) RetentionPolicy() store.RetentionPolicyStore {
return ss.stores.retentionPolicy
}
func (ss *SqlStore) User() store.UserStore {
return ss.stores.user
}
func (ss *SqlStore) Bot() store.BotStore {
return ss.stores.bot
}
func (ss *SqlStore) Session() store.SessionStore {
return ss.stores.session
}
func (ss *SqlStore) Audit() store.AuditStore {
return ss.stores.audit
}
func (ss *SqlStore) ClusterDiscovery() store.ClusterDiscoveryStore {
return ss.stores.cluster
}
func (ss *SqlStore) RemoteCluster() store.RemoteClusterStore {
return ss.stores.remoteCluster
}
func (ss *SqlStore) Compliance() store.ComplianceStore {
return ss.stores.compliance
}
func (ss *SqlStore) OAuth() store.OAuthStore {
return ss.stores.oauth
}
func (ss *SqlStore) System() store.SystemStore {
return ss.stores.system
}
func (ss *SqlStore) Webhook() store.WebhookStore {
return ss.stores.webhook
}
func (ss *SqlStore) Command() store.CommandStore {
return ss.stores.command
}
func (ss *SqlStore) CommandWebhook() store.CommandWebhookStore {
return ss.stores.commandWebhook
}
func (ss *SqlStore) Preference() store.PreferenceStore {
return ss.stores.preference
}
func (ss *SqlStore) License() store.LicenseStore {
return ss.stores.license
}
func (ss *SqlStore) Token() store.TokenStore {
return ss.stores.token
}
func (ss *SqlStore) Emoji() store.EmojiStore {
return ss.stores.emoji
}
func (ss *SqlStore) Status() store.StatusStore {
return ss.stores.status
}
func (ss *SqlStore) FileInfo() store.FileInfoStore {
return ss.stores.fileInfo
}
func (ss *SqlStore) UploadSession() store.UploadSessionStore {
return ss.stores.uploadSession
}
func (ss *SqlStore) Reaction() store.ReactionStore {
return ss.stores.reaction
}
func (ss *SqlStore) Job() store.JobStore {
return ss.stores.job
}
func (ss *SqlStore) UserAccessToken() store.UserAccessTokenStore {
return ss.stores.userAccessToken
}
func (ss *SqlStore) ChannelMemberHistory() store.ChannelMemberHistoryStore {
return ss.stores.channelMemberHistory
}
func (ss *SqlStore) Plugin() store.PluginStore {
return ss.stores.plugin
}
func (ss *SqlStore) Thread() store.ThreadStore {
return ss.stores.thread
}
func (ss *SqlStore) Role() store.RoleStore {
return ss.stores.role
}
func (ss *SqlStore) TermsOfService() store.TermsOfServiceStore {
return ss.stores.TermsOfService
}
func (ss *SqlStore) ProductNotices() store.ProductNoticesStore {
return ss.stores.productNotices
}
func (ss *SqlStore) UserTermsOfService() store.UserTermsOfServiceStore {
return ss.stores.UserTermsOfService
}
func (ss *SqlStore) Scheme() store.SchemeStore {
return ss.stores.scheme
}
func (ss *SqlStore) Group() store.GroupStore {
return ss.stores.group
}
func (ss *SqlStore) LinkMetadata() store.LinkMetadataStore {
return ss.stores.linkMetadata
}
func (ss *SqlStore) NotifyAdmin() store.NotifyAdminStore {
return ss.stores.notifyAdmin
}
func (ss *SqlStore) SharedChannel() store.SharedChannelStore {
return ss.stores.sharedchannel
}
func (ss *SqlStore) PostPriority() store.PostPriorityStore {
return ss.stores.postPriority
}
func (ss *SqlStore) Draft() store.DraftStore {
return ss.stores.draft
}
func (ss *SqlStore) PostAcknowledgement() store.PostAcknowledgementStore {
return ss.stores.postAcknowledgement
}
func (ss *SqlStore) TrueUpReview() store.TrueUpReviewStore {
return ss.stores.trueUpReview
}
func (ss *SqlStore) DropAllTables() {
if ss.DriverName() == model.DatabaseDriverPostgres {
ss.masterX.Exec(`DO
$func$
BEGIN
EXECUTE
(SELECT 'TRUNCATE TABLE ' || string_agg(oid::regclass::text, ', ') || ' CASCADE'
FROM pg_class
WHERE relkind = 'r' -- only tables
AND relnamespace = 'public'::regnamespace
AND NOT relname = 'db_migrations'
);
END
$func$;`)
} else {
tables := []string{}
ss.masterX.Select(&tables, `show tables`)
for _, t := range tables {
if t != "db_migrations" {
ss.masterX.Exec(`TRUNCATE TABLE ` + t)
}
}
}
}
func (ss *SqlStore) getQueryBuilder() sq.StatementBuilderType {
return sq.StatementBuilder.PlaceholderFormat(ss.getQueryPlaceholder())
}
func (ss *SqlStore) getQueryPlaceholder() sq.PlaceholderFormat {
if ss.DriverName() == model.DatabaseDriverPostgres {
return sq.Dollar
}
return sq.Question
}
// getSubQueryBuilder is necessary to generate the SQL query and args to pass to sub-queries because squirrel does not support WHERE clause in sub-queries.
func (ss *SqlStore) getSubQueryBuilder() sq.StatementBuilderType {
return sq.StatementBuilder.PlaceholderFormat(sq.Question)
}
func (ss *SqlStore) CheckIntegrity() <-chan model.IntegrityCheckResult {
results := make(chan model.IntegrityCheckResult)
go CheckRelationalIntegrity(ss, results)
return results
}
func (ss *SqlStore) UpdateLicense(license *model.License) {
ss.licenseMutex.Lock()
defer ss.licenseMutex.Unlock()
ss.license = license
}
func (ss *SqlStore) GetLicense() *model.License {
return ss.license
}
func (ss *SqlStore) hasLicense() bool {
ss.licenseMutex.Lock()
hasLicense := ss.license != nil
ss.licenseMutex.Unlock()
return hasLicense
}
func (ss *SqlStore) migrate(direction migrationDirection) error {
assets := db.Assets()
assetsList, err := assets.ReadDir(path.Join("migrations", ss.DriverName()))
if err != nil {
return err
}
assetNamesForDriver := make([]string, len(assetsList))
for i, entry := range assetsList {
assetNamesForDriver[i] = entry.Name()
}
src, err := mbindata.WithInstance(&mbindata.AssetSource{
Names: assetNamesForDriver,
AssetFunc: func(name string) ([]byte, error) {
return assets.ReadFile(path.Join("migrations", ss.DriverName(), name))
},
})
if err != nil {
return err
}
var driver drivers.Driver
switch ss.DriverName() {
case model.DatabaseDriverMysql:
dataSource, rErr := ResetReadTimeout(*ss.settings.DataSource)
if rErr != nil {
mlog.Fatal("Failed to reset read timeout from datasource.", mlog.Err(rErr), mlog.String("src", *ss.settings.DataSource))
return rErr
}
dataSource, err = AppendMultipleStatementsFlag(dataSource)
if err != nil {
return err
}
db := SetupConnection("master", dataSource, ss.settings)
driver, err = ms.WithInstance(db)
defer db.Close()
case model.DatabaseDriverPostgres:
driver, err = ps.WithInstance(ss.GetMasterX().DB.DB)
default:
err = fmt.Errorf("unsupported database type %s for migration", ss.DriverName())
}
if err != nil {
return err
}
opts := []morph.EngineOption{
morph.WithLogger(log.New(&morphWriter{}, "", log.Lshortfile)),
morph.WithLock("mm-lock-key"),
morph.SetStatementTimeoutInSeconds(*ss.settings.MigrationsStatementTimeoutSeconds),
}
engine, err := morph.New(context.Background(), driver, src, opts...)
if err != nil {
return err
}
defer engine.Close()
switch direction {
case migrationsDirectionDown:
_, err = engine.ApplyDown(-1)
return err
default:
return engine.ApplyAll()
}
}
func convertMySQLFullTextColumnsToPostgres(columnNames string) string {
columns := strings.Split(columnNames, ", ")
concatenatedColumnNames := ""
for i, c := range columns {
concatenatedColumnNames += c
if i < len(columns)-1 {
concatenatedColumnNames += " || ' ' || "
}
}
return concatenatedColumnNames
}
// IsDuplicate checks whether an error is a duplicate key error, which comes when processes are competing on creating the same
// tables in the database.
func IsDuplicate(err error) bool {
var pqErr *pq.Error
var mysqlErr *mysql.MySQLError
switch {
case errors.As(errors.Cause(err), &pqErr):
if pqErr.Code == PGDupTableErrorCode {
return true
}
case errors.As(errors.Cause(err), &mysqlErr):
if mysqlErr.Number == MySQLDupTableErrorCode {
return true
}
}
return false
}
// ensureMinimumDBVersion gets the DB version and ensures it is
// above the required minimum version requirements.
func (ss *SqlStore) ensureMinimumDBVersion(ver string) (bool, error) {
switch *ss.settings.DriverName {
case model.DatabaseDriverPostgres:
intVer, err2 := strconv.Atoi(ver)
if err2 != nil {
return false, fmt.Errorf("cannot parse DB version: %v", err2)
}
if intVer < minimumRequiredPostgresVersion {
return false, fmt.Errorf("minimum Postgres version requirements not met. Found: %s, Wanted: %s", versionString(intVer, *ss.settings.DriverName), versionString(minimumRequiredPostgresVersion, *ss.settings.DriverName))
}
case model.DatabaseDriverMysql:
// Usually a version string is of the form 5.6.49-log, 10.4.5-MariaDB etc.
if strings.Contains(strings.ToLower(ver), "maria") {
mlog.Warn("MariaDB detected. You are using an unsupported database. Please consider using MySQL or Postgres.")
return true, nil
}
parts := strings.Split(ver, "-")
if len(parts) < 1 {
return false, fmt.Errorf("cannot parse MySQL DB version: %s", ver)
}
// Get the major and minor versions.
versions := strings.Split(parts[0], ".")
if len(versions) < 3 {
return false, fmt.Errorf("cannot parse MySQL DB version: %s", ver)
}
majorVer, err2 := strconv.Atoi(versions[0])
if err2 != nil {
return false, fmt.Errorf("cannot parse MySQL DB version: %w", err2)
}
minorVer, err2 := strconv.Atoi(versions[1])
if err2 != nil {
return false, fmt.Errorf("cannot parse MySQL DB version: %w", err2)
}
patchVer, err2 := strconv.Atoi(versions[2])
if err2 != nil {
return false, fmt.Errorf("cannot parse MySQL DB version: %w", err2)
}
intVer := majorVer*1000 + minorVer*100 + patchVer
if intVer < minimumRequiredMySQLVersion {
return false, fmt.Errorf("minimum MySQL version requirements not met. Found: %s, Wanted: %s", versionString(intVer, *ss.settings.DriverName), versionString(minimumRequiredMySQLVersion, *ss.settings.DriverName))
}
}
return true, nil
}
func (ss *SqlStore) ensureDatabaseCollation() error {
if *ss.settings.DriverName != model.DatabaseDriverMysql {
return nil
}
var connCollation struct {
Variable_name string
Value string
}
if err := ss.GetMasterX().Get(&connCollation, "SHOW VARIABLES LIKE 'collation_connection'"); err != nil {
return errors.Wrap(err, "unable to select variables")
}
// we compare table collation with the connection collation value so that we can
// catch collation mismatches for tables we have a migration for.
for _, tableName := range tablesToCheckForCollation {
// we check if table exists because this code runs before the migrations applied
// which means if there is a fresh db, we may fail on selecting the table_collation
var exists int
if err := ss.GetMasterX().Get(&exists, "SELECT count(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND LOWER(table_name) = ?", tableName); err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to check if table exists for collation check: %q", tableName))
} else if exists == 0 {
continue
}
var tableCollation string
if err := ss.GetMasterX().Get(&tableCollation, "SELECT table_collation FROM information_schema.tables WHERE table_schema = DATABASE() AND LOWER(table_name) = ?", tableName); err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to get table collation: %q", tableName))
}
if tableCollation != connCollation.Value {
mlog.Warn("Table collation mismatch", mlog.String("table_name", tableName), mlog.String("connection_collation", connCollation.Value), mlog.String("table_collation", tableCollation))
}
}
return nil
}
// versionString converts an integer representation of a DB version
// to a pretty-printed string.
// Postgres doesn't follow three-part version numbers from 10.0 onwards:
// https://www.postgresql.org/docs/13/libpq-status.html#LIBPQ-PQSERVERVERSION.
// For MySQL, we consider a major*1000 + minor*100 + patch format.
func versionString(v int, driver string) string {
switch driver {
case model.DatabaseDriverPostgres:
minor := v % 10000
major := v / 10000
return strconv.Itoa(major) + "." + strconv.Itoa(minor)
case model.DatabaseDriverMysql:
minor := v % 1000
major := v / 1000
patch := minor % 100
minor = minor / 100
return strconv.Itoa(major) + "." + strconv.Itoa(minor) + "." + strconv.Itoa(patch)
}
return ""
}
func (ss *SqlStore) toReserveCase(str string) string {
if ss.DriverName() == model.DatabaseDriverPostgres {
return fmt.Sprintf("%q", str)
}
return fmt.Sprintf("`%s`", strings.Title(str))
}
func (ss *SqlStore) GetDBSchemaVersion() (int, error) {
var version int
if err := ss.GetMasterX().Get(&version, "SELECT Version FROM db_migrations ORDER BY Version DESC LIMIT 1"); err != nil {
return 0, errors.Wrap(err, "unable to select from db_migrations")
}
return version, nil
}
func (ss *SqlStore) GetAppliedMigrations() ([]model.AppliedMigration, error) {
migrations := []model.AppliedMigration{}
if err := ss.GetMasterX().Select(&migrations, "SELECT Version, Name FROM db_migrations ORDER BY Version DESC"); err != nil {
return nil, errors.Wrap(err, "unable to select from db_migrations")
}
return migrations, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"strconv"
"strings"
"time"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
type SqlSystemStore struct {
*SqlStore
}
func newSqlSystemStore(sqlStore *SqlStore) store.SystemStore {
return &SqlSystemStore{sqlStore}
}
func (s SqlSystemStore) Save(system *model.System) error {
query := "INSERT INTO Systems (Name, Value) VALUES (:Name, :Value)"
if _, err := s.GetMasterX().NamedExec(query, system); err != nil {
return errors.Wrapf(err, "failed to save system property with name=%s", system.Name)
}
return nil
}
func (s SqlSystemStore) SaveOrUpdate(system *model.System) error {
query := s.getQueryBuilder().
Insert("Systems").
Columns("Name", "Value").
Values(system.Name, system.Value)
if s.DriverName() == model.DatabaseDriverMysql {
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE Value = ?", system.Value))
} else {
query = query.SuffixExpr(sq.Expr("ON CONFLICT (name) DO UPDATE SET Value = ?", system.Value))
}
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "system_tosql")
}
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
return errors.Wrap(err, "failed to upsert system property")
}
return nil
}
func (s SqlSystemStore) SaveOrUpdateWithWarnMetricHandling(system *model.System) error {
if err := s.SaveOrUpdate(system); err != nil {
return err
}
if strings.HasPrefix(system.Name, model.WarnMetricStatusStorePrefix) &&
(system.Value == model.WarnMetricStatusRunonce || system.Value == model.WarnMetricStatusLimitReached) {
if err := s.SaveOrUpdate(&model.System{
Name: model.SystemWarnMetricLastRunTimestampKey,
Value: strconv.FormatInt(utils.MillisFromTime(time.Now()), 10),
}); err != nil {
return errors.Wrapf(err, "failed to save system property with name=%s", model.SystemWarnMetricLastRunTimestampKey)
}
}
return nil
}
func (s SqlSystemStore) Update(system *model.System) error {
query := "UPDATE Systems SET Value=:Value WHERE Name=:Name"
if _, err := s.GetMasterX().NamedExec(query, system); err != nil {
return errors.Wrapf(err, "failed to update system property with name=%s", system.Name)
}
return nil
}
func (s SqlSystemStore) Get() (model.StringMap, error) {
systems := []model.System{}
props := make(model.StringMap)
if err := s.GetReplicaX().Select(&systems, "SELECT * FROM Systems"); err != nil {
return nil, errors.Wrap(err, "failed to get System list")
}
for _, prop := range systems {
props[prop.Name] = prop.Value
}
return props, nil
}
func (s SqlSystemStore) GetByName(name string) (*model.System, error) {
var system model.System
if err := s.GetMasterX().Get(&system, "SELECT * FROM Systems WHERE Name = ?", name); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("System", fmt.Sprintf("name=%s", system.Name))
}
return nil, errors.Wrapf(err, "failed to get system property with name=%s", system.Name)
}
return &system, nil
}
func (s SqlSystemStore) PermanentDeleteByName(name string) (*model.System, error) {
var system model.System
if _, err := s.GetMasterX().Exec("DELETE FROM Systems WHERE Name = ?", name); err != nil {
return nil, errors.Wrapf(err, "failed to permanent delete system property with name=%s", system.Name)
}
return &system, nil
}
// InsertIfExists inserts a given system value if it does not already exist. If a value
// already exists, it returns the old one, else returns the new one.
func (s SqlSystemStore) InsertIfExists(system *model.System) (_ *model.System, err error) {
tx, err := s.GetMasterX().BeginXWithIsolation(&sql.TxOptions{
Isolation: sql.LevelSerializable,
})
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(tx, &err)
var origSystem model.System
if err := tx.Get(&origSystem, `SELECT * FROM Systems
WHERE Name = ?`, system.Name); err != nil && err != sql.ErrNoRows {
return nil, errors.Wrapf(err, "failed to get system property with name=%s", system.Name)
}
if origSystem.Value != "" {
// Already a value exists, return that.
return &origSystem, nil
}
// Key does not exist, need to insert.
if _, err := tx.NamedExec("INSERT INTO Systems (Name, Value) VALUES (:Name, :Value)", system); err != nil {
return nil, errors.Wrapf(err, "failed to save system property with name=%s", system.Name)
}
if err := tx.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return system, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
"fmt"
"strings"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
const (
TeamMemberExistsError = "store.sql_team.save_member.exists.app_error"
)
type SqlTeamStore struct {
*SqlStore
teamsQuery sq.SelectBuilder
}
type teamMember struct {
TeamId string
UserId string
Roles string
DeleteAt int64
SchemeUser sql.NullBool
SchemeAdmin sql.NullBool
SchemeGuest sql.NullBool
CreateAt int64
}
func NewTeamMemberFromModel(tm *model.TeamMember) *teamMember {
return &teamMember{
TeamId: tm.TeamId,
UserId: tm.UserId,
Roles: tm.ExplicitRoles,
DeleteAt: tm.DeleteAt,
SchemeGuest: sql.NullBool{Valid: true, Bool: tm.SchemeGuest},
SchemeUser: sql.NullBool{Valid: true, Bool: tm.SchemeUser},
SchemeAdmin: sql.NullBool{Valid: true, Bool: tm.SchemeAdmin},
CreateAt: tm.CreateAt,
}
}
type teamMemberWithSchemeRoles struct {
TeamId string
UserId string
Roles string
DeleteAt int64
SchemeGuest sql.NullBool
SchemeUser sql.NullBool
SchemeAdmin sql.NullBool
TeamSchemeDefaultGuestRole sql.NullString
TeamSchemeDefaultUserRole sql.NullString
TeamSchemeDefaultAdminRole sql.NullString
CreateAt int64
}
type teamMemberWithSchemeRolesList []teamMemberWithSchemeRoles
func teamMemberSliceColumns() []string {
return []string{"TeamId", "UserId", "Roles", "DeleteAt", "SchemeUser", "SchemeAdmin", "SchemeGuest", "CreateAt"}
}
func teamMemberToSlice(member *model.TeamMember) []any {
resultSlice := []any{}
resultSlice = append(resultSlice, member.TeamId)
resultSlice = append(resultSlice, member.UserId)
resultSlice = append(resultSlice, member.ExplicitRoles)
resultSlice = append(resultSlice, member.DeleteAt)
resultSlice = append(resultSlice, member.SchemeUser)
resultSlice = append(resultSlice, member.SchemeAdmin)
resultSlice = append(resultSlice, member.SchemeGuest)
resultSlice = append(resultSlice, member.CreateAt)
return resultSlice
}
func wildcardSearchTerm(term string) string {
return strings.ToLower("%" + term + "%")
}
type rolesInfo struct {
roles []string
explicitRoles []string
schemeGuest bool
schemeUser bool
schemeAdmin bool
}
func getTeamRoles(schemeGuest, schemeUser, schemeAdmin bool, defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole string, roles []string) rolesInfo {
result := rolesInfo{
roles: []string{},
explicitRoles: []string{},
schemeGuest: schemeGuest,
schemeUser: schemeUser,
schemeAdmin: schemeAdmin,
}
// Identify any scheme derived roles that are in "Roles" field due to not yet being migrated, and exclude
// them from ExplicitRoles field.
for _, role := range roles {
switch role {
case model.TeamGuestRoleId:
result.schemeGuest = true
case model.TeamUserRoleId:
result.schemeUser = true
case model.TeamAdminRoleId:
result.schemeAdmin = true
default:
result.explicitRoles = append(result.explicitRoles, role)
result.roles = append(result.roles, role)
}
}
// Add any scheme derived roles that are not in the Roles field due to being Implicit from the Scheme, and add
// them to the Roles field for backwards compatibility reasons.
var schemeImpliedRoles []string
if result.schemeGuest {
if defaultTeamGuestRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultTeamGuestRole)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.TeamGuestRoleId)
}
}
if result.schemeUser {
if defaultTeamUserRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultTeamUserRole)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.TeamUserRoleId)
}
}
if result.schemeAdmin {
if defaultTeamAdminRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultTeamAdminRole)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.TeamAdminRoleId)
}
}
for _, impliedRole := range schemeImpliedRoles {
alreadyThere := false
for _, role := range result.roles {
if role == impliedRole {
alreadyThere = true
}
}
if !alreadyThere {
result.roles = append(result.roles, impliedRole)
}
}
return result
}
func (db teamMemberWithSchemeRoles) ToModel() *model.TeamMember {
// Identify any scheme derived roles that are in "Roles" field due to not yet being migrated, and exclude
// them from ExplicitRoles field.
schemeGuest := db.SchemeGuest.Valid && db.SchemeGuest.Bool
schemeUser := db.SchemeUser.Valid && db.SchemeUser.Bool
schemeAdmin := db.SchemeAdmin.Valid && db.SchemeAdmin.Bool
defaultTeamGuestRole := ""
if db.TeamSchemeDefaultGuestRole.Valid {
defaultTeamGuestRole = db.TeamSchemeDefaultGuestRole.String
}
defaultTeamUserRole := ""
if db.TeamSchemeDefaultUserRole.Valid {
defaultTeamUserRole = db.TeamSchemeDefaultUserRole.String
}
defaultTeamAdminRole := ""
if db.TeamSchemeDefaultAdminRole.Valid {
defaultTeamAdminRole = db.TeamSchemeDefaultAdminRole.String
}
rolesResult := getTeamRoles(schemeGuest, schemeUser, schemeAdmin, defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole, strings.Fields(db.Roles))
tm := &model.TeamMember{
TeamId: db.TeamId,
UserId: db.UserId,
Roles: strings.Join(rolesResult.roles, " "),
DeleteAt: db.DeleteAt,
SchemeGuest: rolesResult.schemeGuest,
SchemeUser: rolesResult.schemeUser,
SchemeAdmin: rolesResult.schemeAdmin,
ExplicitRoles: strings.Join(rolesResult.explicitRoles, " "),
CreateAt: db.CreateAt,
}
return tm
}
func (db teamMemberWithSchemeRolesList) ToModel() []*model.TeamMember {
tms := make([]*model.TeamMember, 0)
for _, tm := range db {
tms = append(tms, tm.ToModel())
}
return tms
}
func newSqlTeamStore(sqlStore *SqlStore) store.TeamStore {
s := &SqlTeamStore{
SqlStore: sqlStore,
}
s.teamsQuery = s.getQueryBuilder().
Select("Teams.*").
From("Teams")
return s
}
// Save adds the team to the database if a team with the same name does not already
// exist in the database. It returns the team added if the operation is successful.
func (s SqlTeamStore) Save(team *model.Team) (*model.Team, error) {
if team.Id != "" {
return nil, store.NewErrInvalidInput("Team", "id", team.Id)
}
team.PreSave()
if err := team.IsValid(); err != nil {
return nil, err
}
if _, err := s.GetMasterX().NamedExec(`INSERT INTO Teams
(Id, CreateAt, UpdateAt, DeleteAt, DisplayName, Name, Description, Email, Type, CompanyName, AllowedDomains,
InviteId, AllowOpenInvite, LastTeamIconUpdate, SchemeId, GroupConstrained, CloudLimitsArchived)
VALUES
(:Id, :CreateAt, :UpdateAt, :DeleteAt, :DisplayName, :Name, :Description, :Email, :Type, :CompanyName, :AllowedDomains,
:InviteId, :AllowOpenInvite, :LastTeamIconUpdate, :SchemeId, :GroupConstrained, :CloudLimitsArchived)`, team); err != nil {
if IsUniqueConstraintError(err, []string{"Name", "teams_name_key"}) {
return nil, store.NewErrInvalidInput("Team", "id", team.Id)
}
return nil, errors.Wrapf(err, "failed to save Team with id=%s", team.Id)
}
return team, nil
}
// Update updates the details of the team passed as the parameter using the team Id
// if the team exists in the database.
// It returns the updated team if the operation is successful.
func (s SqlTeamStore) Update(team *model.Team) (*model.Team, error) {
team.PreUpdate()
if err := team.IsValid(); err != nil {
return nil, err
}
oldTeam := model.Team{}
err := s.GetMasterX().Get(&oldTeam, `SELECT * FROM Teams WHERE Id=?`, team.Id)
if err != nil {
return nil, errors.Wrapf(err, "failed to get Team with id=%s", team.Id)
}
if oldTeam.Id == "" {
return nil, store.NewErrInvalidInput("Team", "id", team.Id)
}
team.CreateAt = oldTeam.CreateAt
team.UpdateAt = model.GetMillis()
res, err := s.GetMasterX().NamedExec(`UPDATE Teams
SET CreateAt=:CreateAt, UpdateAt=:UpdateAt, DeleteAt=:DeleteAt, DisplayName=:DisplayName, Name=:Name,
Description=:Description, Email=:Email, Type=:Type, CompanyName=:CompanyName, AllowedDomains=:AllowedDomains,
InviteId=:InviteId, AllowOpenInvite=:AllowOpenInvite, LastTeamIconUpdate=:LastTeamIconUpdate,
SchemeId=:SchemeId, GroupConstrained=:GroupConstrained, CloudLimitsArchived=:CloudLimitsArchived
WHERE Id=:Id`, team)
if err != nil {
return nil, errors.Wrapf(err, "failed to update Team with id=%s", team.Id)
}
count, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "failed to get rows_affected")
}
if count > 1 {
return nil, errors.Wrapf(err, "multiple Teams updated with id=%s", team.Id)
}
return team, nil
}
// Get returns from the database the team that matches the id provided as parameter.
// If the team doesn't exist it returns a model.AppError with a
// http.StatusNotFound in the StatusCode field.
func (s SqlTeamStore) Get(id string) (*model.Team, error) {
team := model.Team{}
if err := s.GetReplicaX().Get(&team, `SELECT * FROM Teams WHERE Id=?`, id); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Team", id)
}
return nil, errors.Wrapf(err, "failed to get Team with id=%s", id)
}
if team.Id == "" {
return nil, store.NewErrNotFound("Team", id)
}
return &team, nil
}
func (s SqlTeamStore) GetMany(ids []string) ([]*model.Team, error) {
query := s.getQueryBuilder().
Select("*").
From("Teams").
Where(sq.Eq{"Id": ids})
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getmany_tosql")
}
teams := []*model.Team{}
err = s.GetReplicaX().Select(&teams, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get teams with ids %v", ids)
}
if len(teams) == 0 {
return nil, store.NewErrNotFound("Team", fmt.Sprintf("ids=%v", ids))
}
return teams, nil
}
// GetByInviteId returns from the database the team that matches the inviteId provided as parameter.
// If the parameter provided is empty or if there is no match in the database, it returns a model.AppError
// with a http.StatusNotFound in the StatusCode field.
func (s SqlTeamStore) GetByInviteId(inviteId string) (*model.Team, error) {
team := model.Team{}
query, args, err := s.teamsQuery.Where(sq.Eq{"InviteId": inviteId}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
err = s.GetReplicaX().Get(&team, query, args...)
if err != nil {
return nil, store.NewErrNotFound("Team", fmt.Sprintf("inviteId=%s", inviteId))
}
if inviteId == "" || team.InviteId != inviteId {
return nil, store.NewErrNotFound("Team", fmt.Sprintf("inviteId=%s", inviteId))
}
return &team, nil
}
func (s SqlTeamStore) GetByEmptyInviteID() ([]*model.Team, error) {
teams := []*model.Team{}
err := s.GetReplicaX().Select(&teams, "SELECT * FROM Teams WHERE InviteId = ''")
if err != nil {
return nil, errors.Wrap(err, "failed to find Teams with empty InviteID")
}
return teams, nil
}
// GetByName returns from the database the team that matches the name provided as parameter.
// If there is no match in the database, it returns a model.AppError with a
// http.StatusNotFound in the StatusCode field.
func (s SqlTeamStore) GetByName(name string) (*model.Team, error) {
team := model.Team{}
query, args, err := s.teamsQuery.Where(sq.Eq{"Name": name}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
err = s.GetReplicaX().Get(&team, query, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Team", fmt.Sprintf("name=%s", name))
}
return nil, errors.Wrapf(err, "failed to find Team with name=%s", name)
}
return &team, nil
}
func (s SqlTeamStore) GetByNames(names []string) ([]*model.Team, error) {
uniqueNames := utils.RemoveDuplicatesFromStringArray(names)
query, args, err := s.teamsQuery.Where(sq.Eq{"Name": uniqueNames}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
teams := []*model.Team{}
err = s.GetReplicaX().Select(&teams, query, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Team", fmt.Sprintf("nameIn=%v", names))
}
return nil, errors.Wrap(err, "failed to find Teams")
}
if len(teams) != len(uniqueNames) {
return nil, store.NewErrNotFound("Team", fmt.Sprintf("nameIn=%v", names))
}
return teams, nil
}
func (s SqlTeamStore) teamSearchQuery(opts *model.TeamSearch, countQuery bool) sq.SelectBuilder {
var selectStr string
if countQuery {
selectStr = "count(*)"
} else {
selectStr = "t.*"
if opts.IncludePolicyID != nil && *opts.IncludePolicyID {
selectStr += ", RetentionPoliciesTeams.PolicyId as PolicyID"
}
}
query := s.getQueryBuilder().
Select(selectStr).
From("Teams as t")
// Don't order or limit if getting count
if !countQuery {
query = query.OrderBy("t.DisplayName")
if opts.IsPaginated() {
query = query.Limit(uint64(*opts.PerPage)).Offset(uint64(*opts.Page * *opts.PerPage))
}
}
term := opts.Term
if term != "" {
term = sanitizeSearchTerm(term, "\\")
term = wildcardSearchTerm(term)
operatorKeyword := "ILIKE"
if s.DriverName() == model.DatabaseDriverMysql {
operatorKeyword = "LIKE"
}
query = query.Where(fmt.Sprintf("(Name %[1]s ? OR DisplayName %[1]s ?)", operatorKeyword), term, term)
}
if opts.PolicyID != nil && *opts.PolicyID != "" {
query = query.
InnerJoin("RetentionPoliciesTeams ON t.Id = RetentionPoliciesTeams.TeamId").
Where(sq.Eq{"RetentionPoliciesTeams.PolicyId": *opts.PolicyID})
} else if opts.ExcludePolicyConstrained != nil && *opts.ExcludePolicyConstrained {
query = query.
LeftJoin("RetentionPoliciesTeams ON t.Id = RetentionPoliciesTeams.TeamId").
Where("RetentionPoliciesTeams.TeamId IS NULL")
} else if opts.IncludePolicyID != nil && *opts.IncludePolicyID {
query = query.
LeftJoin("RetentionPoliciesTeams ON t.Id = RetentionPoliciesTeams.TeamId")
}
var teamFilters sq.Sqlizer
var openInviteFilter sq.Sqlizer
if opts.AllowOpenInvite != nil {
if *opts.AllowOpenInvite {
openInviteFilter = sq.Eq{"AllowOpenInvite": true}
} else {
openInviteFilter = sq.And{
sq.Or{
sq.NotEq{"AllowOpenInvite": true},
sq.Eq{"AllowOpenInvite": nil},
},
sq.Or{
sq.NotEq{"GroupConstrained": true},
sq.Eq{"GroupConstrained": nil},
},
}
}
teamFilters = openInviteFilter
}
var groupConstrainedFilter sq.Sqlizer
if opts.GroupConstrained != nil {
if *opts.GroupConstrained {
groupConstrainedFilter = sq.Eq{"GroupConstrained": true}
} else {
groupConstrainedFilter = sq.Or{
sq.NotEq{"GroupConstrained": true},
sq.Eq{"GroupConstrained": nil},
}
}
if teamFilters == nil {
teamFilters = groupConstrainedFilter
} else {
teamFilters = sq.Or{teamFilters, groupConstrainedFilter}
}
}
if opts.TeamType != nil {
teamTypeFilter := sq.Eq{"Type": *opts.TeamType}
teamFilters = sq.And{teamFilters, teamTypeFilter}
}
query = query.Where(teamFilters)
return query
}
// SearchAll returns from the database a list of teams that match the Name or DisplayName
// passed as the term search parameter.
func (s SqlTeamStore) SearchAll(opts *model.TeamSearch) ([]*model.Team, error) {
teams := []*model.Team{}
queryString, args, err := s.teamSearchQuery(opts, false).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
if err = s.GetReplicaX().Select(&teams, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Teams with term=%s", opts.Term)
}
return teams, nil
}
// SearchAllPaged returns a teams list and the total count of teams that matched the search.
func (s SqlTeamStore) SearchAllPaged(opts *model.TeamSearch) ([]*model.Team, int64, error) {
teams := []*model.Team{}
var totalCount int64
queryString, args, err := s.teamSearchQuery(opts, false).ToSql()
if err != nil {
return nil, 0, errors.Wrap(err, "team_tosql")
}
if err = s.GetReplicaX().Select(&teams, queryString, args...); err != nil {
return nil, 0, errors.Wrapf(err, "failed to find Teams with term=%s", opts.Term)
}
queryString, args, err = s.teamSearchQuery(opts, true).ToSql()
if err != nil {
return nil, 0, errors.Wrap(err, "team_tosql")
}
err = s.GetReplicaX().Get(&totalCount, queryString, args...)
if err != nil {
return nil, 0, errors.Wrapf(err, "failed to count Teams with term=%s", opts.Term)
}
return teams, totalCount, nil
}
// SearchOpen returns from the database a list of public teams that match the Name or DisplayName
// passed as the term search parameter.
func (s SqlTeamStore) SearchOpen(opts *model.TeamSearch) ([]*model.Team, error) {
opts.TeamType = model.NewString("O")
opts.AllowOpenInvite = model.NewBool(true)
return s.SearchAll(opts)
}
// SearchPrivate returns from the database a list of private teams that match the Name or DisplayName
// passed as the term search parameter.
func (s SqlTeamStore) SearchPrivate(opts *model.TeamSearch) ([]*model.Team, error) {
opts.TeamType = model.NewString("O")
opts.AllowOpenInvite = model.NewBool(false)
return s.SearchAll(opts)
}
// GetAll returns all teams
func (s SqlTeamStore) GetAll() ([]*model.Team, error) {
teams := []*model.Team{}
query, args, err := s.teamsQuery.OrderBy("DisplayName").ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
err = s.GetReplicaX().Select(&teams, query, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
return teams, nil
}
// GetAllPage returns teams, up to a total limit passed as parameter and paginated by offset number passed as parameter.
func (s SqlTeamStore) GetAllPage(offset int, limit int, opts *model.TeamSearch) ([]*model.Team, error) {
teams := []*model.Team{}
selectString := "Teams.*"
if opts != nil && opts.IncludePolicyID != nil && *opts.IncludePolicyID {
selectString += ", RetentionPoliciesTeams.PolicyId as PolicyID"
}
builder := s.getQueryBuilder().
Select(selectString).
From("Teams").
OrderBy("DisplayName").
Limit(uint64(limit)).
Offset(uint64(offset))
if opts != nil {
if (opts.ExcludePolicyConstrained != nil && *opts.ExcludePolicyConstrained) ||
(opts.IncludePolicyID != nil && *opts.IncludePolicyID) {
builder = builder.LeftJoin("RetentionPoliciesTeams ON Teams.Id = RetentionPoliciesTeams.TeamId")
}
if opts.ExcludePolicyConstrained != nil && *opts.ExcludePolicyConstrained {
builder = builder.Where("RetentionPoliciesTeams.TeamId IS NULL")
}
if opts.AllowOpenInvite != nil {
builder = builder.Where(sq.Eq{"AllowOpenInvite": *opts.AllowOpenInvite})
}
}
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
if err = s.GetReplicaX().Select(&teams, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
return teams, nil
}
// GetTeamsByUserId returns from the database all teams that userId belongs to.
func (s SqlTeamStore) GetTeamsByUserId(userId string) ([]*model.Team, error) {
teams := []*model.Team{}
query, args, err := s.teamsQuery.
Join("TeamMembers ON TeamMembers.TeamId = Teams.Id").
Where(sq.Eq{"TeamMembers.UserId": userId, "TeamMembers.DeleteAt": 0, "Teams.DeleteAt": 0}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
if err = s.GetReplicaX().Select(&teams, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
return teams, nil
}
// GetAllPrivateTeamListing returns all private teams.
func (s SqlTeamStore) GetAllPrivateTeamListing() ([]*model.Team, error) {
query, args, err := s.teamsQuery.Where(sq.Eq{"AllowOpenInvite": false}).
OrderBy("DisplayName").ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
data := []*model.Team{}
if err = s.GetReplicaX().Select(&data, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
return data, nil
}
// GetAllTeamListing returns all public teams.
func (s SqlTeamStore) GetAllTeamListing() ([]*model.Team, error) {
query, args, err := s.teamsQuery.Where(sq.Eq{"AllowOpenInvite": true}).
OrderBy("DisplayName").ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
data := []*model.Team{}
if err = s.GetReplicaX().Select(&data, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
return data, nil
}
// PermanentDelete permanently deletes from the database the team entry that matches the teamId passed as parameter.
// To soft-delete the team you can Update it with the DeleteAt field set to the current millisecond using model.GetMillis()
func (s SqlTeamStore) PermanentDelete(teamId string) error {
sql, args, err := s.getQueryBuilder().
Delete("Teams").
Where(sq.Eq{"Id": teamId}).ToSql()
if err != nil {
return errors.Wrap(err, "team_tosql")
}
if _, err = s.GetMasterX().Exec(sql, args...); err != nil {
return errors.Wrapf(err, "failed to delete Team with id=%s", teamId)
}
return nil
}
// AnalyticsTeamCount returns the total number of teams.
func (s SqlTeamStore) AnalyticsTeamCount(opts *model.TeamSearch) (int64, error) {
query := s.getQueryBuilder().Select("COUNT(*) FROM Teams")
if opts == nil || (opts.IncludeDeleted != nil && !*opts.IncludeDeleted) {
query = query.Where(sq.Eq{"DeleteAt": 0})
}
if opts != nil && opts.AllowOpenInvite != nil {
query = query.Where(sq.Eq{"AllowOpenInvite": *opts.AllowOpenInvite})
}
queryString, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "team_tosql")
}
var c int64
err = s.GetReplicaX().Get(&c, queryString, args...)
if err != nil {
return int64(0), errors.Wrap(err, "failed to count Teams")
}
return c, nil
}
func (s SqlTeamStore) getTeamMembersWithSchemeSelectQuery() sq.SelectBuilder {
return s.getQueryBuilder().
Select(
"TeamMembers.*",
"TeamScheme.DefaultTeamGuestRole TeamSchemeDefaultGuestRole",
"TeamScheme.DefaultTeamUserRole TeamSchemeDefaultUserRole",
"TeamScheme.DefaultTeamAdminRole TeamSchemeDefaultAdminRole",
).
From("TeamMembers").
LeftJoin("Teams ON TeamMembers.TeamId = Teams.Id").
LeftJoin("Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id")
}
func (s SqlTeamStore) SaveMultipleMembers(members []*model.TeamMember, maxUsersPerTeam int) ([]*model.TeamMember, error) {
newTeamMembers := map[string]int{}
users := map[string]bool{}
for _, member := range members {
newTeamMembers[member.TeamId] = 0
}
for _, member := range members {
newTeamMembers[member.TeamId]++
users[member.UserId] = true
if err := member.IsValid(); err != nil {
return nil, err
}
}
teams := []string{}
for team := range newTeamMembers {
teams = append(teams, team)
}
defaultTeamRolesByTeam := map[string]struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
queryRoles := s.getQueryBuilder().
Select(
"Teams.Id as Id",
"TeamScheme.DefaultTeamGuestRole as Guest",
"TeamScheme.DefaultTeamUserRole as User",
"TeamScheme.DefaultTeamAdminRole as Admin",
).
From("Teams").
LeftJoin("Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id").
Where(sq.Eq{"Teams.Id": teams})
sqlRolesQuery, argsRoles, err := queryRoles.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_roles_tosql")
}
defaultTeamsRoles := []struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
err = s.GetMasterX().Select(&defaultTeamsRoles, sqlRolesQuery, argsRoles...)
if err != nil {
return nil, errors.Wrap(err, "default_team_roles_select")
}
for _, defaultRoles := range defaultTeamsRoles {
defaultTeamRolesByTeam[defaultRoles.Id] = defaultRoles
}
if maxUsersPerTeam >= 0 {
queryCount := s.getQueryBuilder().
Select(
"COUNT(0) as Count, TeamMembers.TeamId as TeamId",
).
From("TeamMembers").
Join("Users ON TeamMembers.UserId = Users.Id").
Where(sq.Eq{"TeamMembers.TeamId": teams}).
Where(sq.Eq{"TeamMembers.DeleteAt": 0}).
Where(sq.Eq{"Users.DeleteAt": 0}).
GroupBy("TeamMembers.TeamId")
sqlCountQuery, argsCount, errCount := queryCount.ToSql()
if errCount != nil {
return nil, errors.Wrap(err, "member_count_tosql")
}
counters := []struct {
Count int
TeamId string
}{}
err = s.GetMasterX().Select(&counters, sqlCountQuery, argsCount...)
if err != nil {
return nil, errors.Wrap(err, "failed to count users in the teams of the memberships")
}
for teamId, newMembers := range newTeamMembers {
existingMembers := 0
for _, counter := range counters {
if counter.TeamId == teamId {
existingMembers = counter.Count
}
}
if existingMembers+newMembers > maxUsersPerTeam {
return nil, store.NewErrLimitExceeded("TeamMember", existingMembers+newMembers, "team members limit exceeded")
}
}
}
query := s.getQueryBuilder().Insert("TeamMembers").Columns(teamMemberSliceColumns()...)
for _, member := range members {
query = query.Values(teamMemberToSlice(member)...)
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "insert_members_to_sql")
}
if _, err = s.GetMasterX().Exec(sql, args...); err != nil {
if IsUniqueConstraintError(err, []string{"TeamId", "teammembers_pkey", "PRIMARY"}) {
return nil, store.NewErrConflict("TeamMember", err, "")
}
return nil, errors.Wrap(err, "unable_to_save_team_member")
}
newMembers := []*model.TeamMember{}
for _, member := range members {
s.InvalidateAllTeamIdsForUser(member.UserId)
defaultTeamGuestRole := defaultTeamRolesByTeam[member.TeamId].Guest.String
defaultTeamUserRole := defaultTeamRolesByTeam[member.TeamId].User.String
defaultTeamAdminRole := defaultTeamRolesByTeam[member.TeamId].Admin.String
rolesResult := getTeamRoles(member.SchemeGuest, member.SchemeUser, member.SchemeAdmin, defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole, strings.Fields(member.ExplicitRoles))
newMember := *member
newMember.SchemeGuest = rolesResult.schemeGuest
newMember.SchemeUser = rolesResult.schemeUser
newMember.SchemeAdmin = rolesResult.schemeAdmin
newMember.Roles = strings.Join(rolesResult.roles, " ")
newMember.ExplicitRoles = strings.Join(rolesResult.explicitRoles, " ")
newMembers = append(newMembers, &newMember)
}
return newMembers, nil
}
func (s SqlTeamStore) SaveMember(member *model.TeamMember, maxUsersPerTeam int) (*model.TeamMember, error) {
members, err := s.SaveMultipleMembers([]*model.TeamMember{member}, maxUsersPerTeam)
if err != nil {
return nil, err
}
return members[0], nil
}
func (s SqlTeamStore) UpdateMultipleMembers(members []*model.TeamMember) ([]*model.TeamMember, error) {
teams := []string{}
for _, member := range members {
member.PreUpdate()
newTeamMember := NewTeamMemberFromModel(member)
if err := member.IsValid(); err != nil {
return nil, err
}
if _, err := s.GetMasterX().NamedExec(`UPDATE TeamMembers
SET Roles=:Roles, DeleteAt=:DeleteAt, CreateAt=:CreateAt, SchemeGuest=:SchemeGuest,
SchemeUser=:SchemeUser, SchemeAdmin=:SchemeAdmin
WHERE TeamId=:TeamId AND UserId=:UserId`, newTeamMember); err != nil {
return nil, errors.Wrap(err, "failed to update TeamMember")
}
teams = append(teams, member.TeamId)
}
query := s.getQueryBuilder().
Select(
"Teams.Id as Id",
"TeamScheme.DefaultTeamGuestRole as Guest",
"TeamScheme.DefaultTeamUserRole as User",
"TeamScheme.DefaultTeamAdminRole as Admin",
).
From("Teams").
LeftJoin("Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id").
Where(sq.Eq{"Teams.Id": teams})
sqlQuery, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
defaultTeamsRoles := []struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
err = s.GetMasterX().Select(&defaultTeamsRoles, sqlQuery, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
defaultTeamRolesByTeam := map[string]struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
for _, defaultRoles := range defaultTeamsRoles {
defaultTeamRolesByTeam[defaultRoles.Id] = defaultRoles
}
updatedMembers := []*model.TeamMember{}
for _, member := range members {
s.InvalidateAllTeamIdsForUser(member.UserId)
defaultTeamGuestRole := defaultTeamRolesByTeam[member.TeamId].Guest.String
defaultTeamUserRole := defaultTeamRolesByTeam[member.TeamId].User.String
defaultTeamAdminRole := defaultTeamRolesByTeam[member.TeamId].Admin.String
rolesResult := getTeamRoles(member.SchemeGuest, member.SchemeUser, member.SchemeAdmin, defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole, strings.Fields(member.ExplicitRoles))
updatedMember := *member
updatedMember.SchemeGuest = rolesResult.schemeGuest
updatedMember.SchemeUser = rolesResult.schemeUser
updatedMember.SchemeAdmin = rolesResult.schemeAdmin
updatedMember.Roles = strings.Join(rolesResult.roles, " ")
updatedMember.ExplicitRoles = strings.Join(rolesResult.explicitRoles, " ")
updatedMembers = append(updatedMembers, &updatedMember)
}
return updatedMembers, nil
}
func (s SqlTeamStore) UpdateMember(member *model.TeamMember) (*model.TeamMember, error) {
members, err := s.UpdateMultipleMembers([]*model.TeamMember{member})
if err != nil {
return nil, err
}
return members[0], nil
}
// GetMember returns a single member of the team that matches the teamId and userId provided as parameters.
func (s SqlTeamStore) GetMember(ctx context.Context, teamId string, userId string) (*model.TeamMember, error) {
query := s.getTeamMembersWithSchemeSelectQuery().
Where(sq.Eq{"TeamMembers.TeamId": teamId}).
Where(sq.Eq{"TeamMembers.UserId": userId})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
var dbMember teamMemberWithSchemeRoles
err = s.DBXFromContext(ctx).Get(&dbMember, queryString, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("TeamMember", fmt.Sprintf("teamId=%s, userId=%s", teamId, userId))
}
return nil, errors.Wrapf(err, "failed to find TeamMembers with teamId=%s and userId=%s", teamId, userId)
}
return dbMember.ToModel(), nil
}
// GetMembers returns a list of members from the database that matches the teamId passed as parameter and,
// also expects teamMembersGetOptions to be passed as a parameter which allows to further filter what to show in the result.
// TeamMembersGetOptions Model has following options->
// 1. Sort through USERNAME [ if provided, which otherwise defaults to ID ]
// 2. Sort through USERNAME [ if provided, which otherwise defaults to ID ] and exclude deleted members.
// 3. Return all the members but, exclude deleted ones.
// 4. Apply ViewUsersRestrictions to restrict what is visible to the user.
func (s SqlTeamStore) GetMembers(teamId string, offset int, limit int, teamMembersGetOptions *model.TeamMembersGetOptions) ([]*model.TeamMember, error) {
query := s.getTeamMembersWithSchemeSelectQuery().
Where(sq.Eq{"TeamMembers.TeamId": teamId}).
Where(sq.Eq{"TeamMembers.DeleteAt": 0}).
Limit(uint64(limit)).
Offset(uint64(offset))
if teamMembersGetOptions == nil || teamMembersGetOptions.Sort == "" {
query = query.OrderBy("UserId")
}
if teamMembersGetOptions != nil {
if teamMembersGetOptions.Sort == model.USERNAME || teamMembersGetOptions.ExcludeDeletedUsers {
query = query.LeftJoin("Users ON TeamMembers.UserId = Users.Id")
}
if teamMembersGetOptions.ExcludeDeletedUsers {
query = query.Where(sq.Eq{"Users.DeleteAt": 0})
}
if teamMembersGetOptions.Sort == model.USERNAME {
query = query.OrderBy(model.USERNAME)
}
query = applyTeamMemberViewRestrictionsFilter(query, teamMembersGetOptions.ViewRestrictions)
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
dbMembers := teamMemberWithSchemeRolesList{}
err = s.GetReplicaX().Select(&dbMembers, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find TeamMembers with teamId=%s", teamId)
}
return dbMembers.ToModel(), nil
}
// GetTotalMemberCount returns the number of all members in a team for the teamId passed as a parameter.
// Expects a restrictions parameter of type ViewUsersRestrictions that defines a set of Teams and Channels that are visible to the caller of the query, and applies restrictions with a filtered result.
func (s SqlTeamStore) GetTotalMemberCount(teamId string, restrictions *model.ViewUsersRestrictions) (int64, error) {
query := s.getQueryBuilder().
Select("count(DISTINCT TeamMembers.UserId)").
From("TeamMembers, Users").
Where("TeamMembers.DeleteAt = 0").
Where("TeamMembers.UserId = Users.Id").
Where(sq.Eq{"TeamMembers.TeamId": teamId})
query = applyTeamMemberViewRestrictionsFilterForStats(query, restrictions)
queryString, args, err := query.ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "team_tosql")
}
var count int64
err = s.GetReplicaX().Get(&count, queryString, args...)
if err != nil {
return int64(0), errors.Wrap(err, "failed to count TeamMembers")
}
return count, nil
}
// GetActiveMemberCount returns the number of active members in a team for the teamId passed as a parameter i.e. members with 'DeleteAt = 0'
// Expects a restrictions parameter of type ViewUsersRestrictions that defines a set of Teams and Channels that are visible to the caller of the query, and applies restrictions with a filtered result.
func (s SqlTeamStore) GetActiveMemberCount(teamId string, restrictions *model.ViewUsersRestrictions) (int64, error) {
query := s.getQueryBuilder().
Select("count(DISTINCT TeamMembers.UserId)").
From("TeamMembers, Users").
Where("TeamMembers.DeleteAt = 0").
Where("TeamMembers.UserId = Users.Id").
Where("Users.DeleteAt = 0").
Where(sq.Eq{"TeamMembers.TeamId": teamId})
query = applyTeamMemberViewRestrictionsFilterForStats(query, restrictions)
queryString, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "team_tosql")
}
var count int64
err = s.GetReplicaX().Get(&count, queryString, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to count TeamMembers")
}
return count, nil
}
// GetMembersByIds returns a list of members from the database that matches the teamId and the list of userIds passed as parameters.
// Expects a restrictions parameter of type ViewUsersRestrictions that defines a set of Teams and Channels that are visible to the caller of the query, and applies restrictions with a filtered result.
func (s SqlTeamStore) GetMembersByIds(teamId string, userIds []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, error) {
if len(userIds) == 0 {
return nil, errors.New("invalid list of user ids")
}
query := s.getTeamMembersWithSchemeSelectQuery().
Where(sq.Eq{"TeamMembers.TeamId": teamId}).
Where(sq.Eq{"TeamMembers.UserId": userIds}).
Where(sq.Eq{"TeamMembers.DeleteAt": 0})
query = applyTeamMemberViewRestrictionsFilter(query, restrictions)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
dbMembers := teamMemberWithSchemeRolesList{}
if err = s.GetReplicaX().Select(&dbMembers, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find TeamMembers")
}
return dbMembers.ToModel(), nil
}
// GetTeamsForUser returns a list of teams that the user is a member of. Expects userId to be passed as a parameter. It can also negative the teamID passed.
func (s SqlTeamStore) GetTeamsForUser(ctx context.Context, userId, excludeTeamID string, includeDeleted bool) ([]*model.TeamMember, error) {
query := s.getTeamMembersWithSchemeSelectQuery().
Where(sq.Eq{"TeamMembers.UserId": userId})
if excludeTeamID != "" {
query = query.Where(sq.NotEq{"TeamMembers.TeamId": excludeTeamID})
}
if !includeDeleted {
query = query.Where(sq.Eq{"TeamMembers.DeleteAt": 0})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
dbMembers := teamMemberWithSchemeRolesList{}
err = s.SqlStore.DBXFromContext(ctx).Select(&dbMembers, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find TeamMembers with userId=%s", userId)
}
return dbMembers.ToModel(), nil
}
// GetTeamsForUserWithPagination returns limited TeamMembers according to the perPage parameter specified.
// It also offsets the records as per the page parameter supplied.
func (s SqlTeamStore) GetTeamsForUserWithPagination(userId string, page, perPage int) ([]*model.TeamMember, error) {
query := s.getTeamMembersWithSchemeSelectQuery().
Where(sq.Eq{"TeamMembers.UserId": userId}).
Limit(uint64(perPage)).
Offset(uint64(page * perPage))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
dbMembers := teamMemberWithSchemeRolesList{}
err = s.GetReplicaX().Select(&dbMembers, queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find TeamMembers with userId=%s", userId)
}
return dbMembers.ToModel(), nil
}
// GetChannelUnreadsForAllTeams returns unreads msg count, mention counts, and notifyProps
// for all the channels in all the teams except the excluded ones.
func (s SqlTeamStore) GetChannelUnreadsForAllTeams(excludeTeamId, userId string) ([]*model.ChannelUnread, error) {
query, args, err := s.getQueryBuilder().
Select("Channels.TeamId TeamId", "Channels.Id ChannelId", "(Channels.TotalMsgCount - ChannelMembers.MsgCount) MsgCount", "(Channels.TotalMsgCountRoot - ChannelMembers.MsgCountRoot) MsgCountRoot", "ChannelMembers.MentionCount MentionCount", "ChannelMembers.MentionCountRoot MentionCountRoot", "ChannelMembers.NotifyProps NotifyProps").
From("Channels").
Join("ChannelMembers ON Id = ChannelId").
Where(sq.Eq{"UserId": userId, "DeleteAt": 0}).
Where(sq.NotEq{"TeamId": excludeTeamId}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
data := []*model.ChannelUnread{}
err = s.GetReplicaX().Select(&data, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Channels with userId=%s and teamId!=%s", userId, excludeTeamId)
}
return data, nil
}
// GetChannelUnreadsForTeam returns unreads msg count, mention counts and notifyProps for all the channels in a single team.
func (s SqlTeamStore) GetChannelUnreadsForTeam(teamId, userId string) ([]*model.ChannelUnread, error) {
query, args, err := s.getQueryBuilder().
Select("Channels.TeamId TeamId", "Channels.Id ChannelId", "(Channels.TotalMsgCount - ChannelMembers.MsgCount) MsgCount", "(Channels.TotalMsgCountRoot - ChannelMembers.MsgCountRoot) MsgCountRoot", "ChannelMembers.MentionCount MentionCount", "ChannelMembers.MentionCountRoot MentionCountRoot", "ChannelMembers.NotifyProps NotifyProps").
From("Channels").
Join("ChannelMembers ON Id = ChannelId").
Where(sq.Eq{"UserId": userId, "TeamId": teamId, "DeleteAt": 0}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
channels := []*model.ChannelUnread{}
err = s.GetReplicaX().Select(&channels, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Channels with teamId=%s and userId=%s", teamId, userId)
}
return channels, nil
}
func (s SqlTeamStore) RemoveMembers(teamId string, userIds []string) error {
builder := s.getQueryBuilder().
Delete("TeamMembers").
Where(sq.Eq{"TeamId": teamId}).
Where(sq.Eq{"UserId": userIds})
query, args, err := builder.ToSql()
if err != nil {
return errors.Wrap(err, "team_tosql")
}
_, err = s.GetMasterX().Exec(query, args...)
if err != nil {
return errors.Wrapf(err, "failed to delete TeamMembers with teamId=%s and userId in %v", teamId, userIds)
}
return nil
}
// RemoveMember remove from the database the team members that match the userId and teamId passed as parameter.
func (s SqlTeamStore) RemoveMember(teamId string, userId string) error {
return s.RemoveMembers(teamId, []string{userId})
}
// RemoveAllMembersByTeam removes from the database the team members that belong to the teamId passed as parameter.
func (s SqlTeamStore) RemoveAllMembersByTeam(teamId string) error {
query, args, err := s.getQueryBuilder().
Delete("TeamMembers").
Where(sq.Eq{"TeamId": teamId}).ToSql()
if err != nil {
return errors.Wrap(err, "team_tosql")
}
_, err = s.GetMasterX().Exec(query, args...)
if err != nil {
return errors.Wrapf(err, "failed to delete TeamMembers with teamId=%s", teamId)
}
return nil
}
// RemoveAllMembersByUser removes from the database the team members that match the userId passed as parameter.
func (s SqlTeamStore) RemoveAllMembersByUser(userId string) error {
query, args, err := s.getQueryBuilder().
Delete("TeamMembers").
Where(sq.Eq{"UserId": userId}).ToSql()
if err != nil {
return errors.Wrap(err, "team_tosql")
}
_, err = s.GetMasterX().Exec(query, args...)
if err != nil {
return errors.Wrapf(err, "failed to delete TeamMembers with userId=%s", userId)
}
return nil
}
// UpdateLastTeamIconUpdate sets the last updated time for the icon based on the parameter passed in teamId. The
// LastTeamIconUpdate and UpdateAt fields are set to the parameter passed in curTime. Returns nil on success and an error
// otherwise.
func (s SqlTeamStore) UpdateLastTeamIconUpdate(teamId string, curTime int64) error {
query, args, err := s.getQueryBuilder().
Update("Teams").
SetMap(sq.Eq{"LastTeamIconUpdate": curTime, "UpdateAt": curTime}).
Where(sq.Eq{"Id": teamId}).ToSql()
if err != nil {
return errors.Wrap(err, "team_tosql")
}
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
return errors.Wrap(err, "failed to update Team")
}
return nil
}
// GetTeamsByScheme returns from the database all teams that match the schemeId provided as parameter, up to
// a total limit passed as parameter and paginated by offset number passed as parameter.
func (s SqlTeamStore) GetTeamsByScheme(schemeId string, offset int, limit int) ([]*model.Team, error) {
query, args, err := s.teamsQuery.Where(sq.Eq{"SchemeId": schemeId}).
OrderBy("DisplayName").
Limit(uint64(limit)).
Offset(uint64(offset)).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
teams := []*model.Team{}
err = s.GetReplicaX().Select(&teams, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find Teams with schemeId=%s", schemeId)
}
return teams, nil
}
// MigrateTeamMembers performs the Advanced Permissions Phase 2 migration for TeamMember objects. Migration is done
// in batches as a single transaction per batch to ensure consistency but to also minimise execution time to avoid
// causing unnecessary table locks. **THIS FUNCTION SHOULD NOT BE USED FOR ANY OTHER PURPOSE.** Executing this function
// *after* the new Schemes functionality has been used on an installation will have unintended consequences.
func (s SqlTeamStore) MigrateTeamMembers(fromTeamId string, fromUserId string) (_ map[string]string, err error) {
var transaction *sqlxTxWrapper
if transaction, err = s.GetMasterX().Beginx(); err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
teamMembers := []teamMember{}
if err := transaction.Select(&teamMembers, "SELECT * from TeamMembers WHERE (TeamId, UserId) > (?, ?) ORDER BY TeamId, UserId LIMIT 100", fromTeamId, fromUserId); err != nil {
return nil, errors.Wrap(err, "failed to find TeamMembers")
}
if len(teamMembers) == 0 {
// No more team members in query result means that the migration has finished.
return nil, nil
}
for i := range teamMembers {
member := teamMembers[i]
roles := strings.Fields(member.Roles)
var newRoles []string
if !member.SchemeAdmin.Valid {
member.SchemeAdmin = sql.NullBool{Bool: false, Valid: true}
}
if !member.SchemeUser.Valid {
member.SchemeUser = sql.NullBool{Bool: false, Valid: true}
}
if !member.SchemeGuest.Valid {
member.SchemeGuest = sql.NullBool{Bool: false, Valid: true}
}
for _, role := range roles {
if role == model.TeamAdminRoleId {
member.SchemeAdmin = sql.NullBool{Bool: true, Valid: true}
} else if role == model.TeamUserRoleId {
member.SchemeUser = sql.NullBool{Bool: true, Valid: true}
} else if role == model.TeamGuestRoleId {
member.SchemeGuest = sql.NullBool{Bool: true, Valid: true}
} else {
newRoles = append(newRoles, role)
}
}
member.Roles = strings.Join(newRoles, " ")
if _, err := transaction.NamedExec(`UPDATE TeamMembers
SET TeamId=:TeamId,
UserId=:UserId,
Roles=:Roles,
DeleteAt=:DeleteAt,
SchemeUser=:SchemeUser,
SchemeAdmin=:SchemeAdmin,
SchemeGuest=:SchemeGuest
WHERE TeamId=:TeamId AND UserId=:UserId`, &member); err != nil {
return nil, errors.Wrap(err, "failed to update TeamMember")
}
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
data := make(map[string]string)
data["TeamId"] = teamMembers[len(teamMembers)-1].TeamId
data["UserId"] = teamMembers[len(teamMembers)-1].UserId
return data, nil
}
// ResetAllTeamSchemes Set all Team's SchemeId values to an empty string.
func (s SqlTeamStore) ResetAllTeamSchemes() error {
if _, err := s.GetMasterX().Exec("UPDATE Teams SET SchemeId=''"); err != nil {
return errors.Wrap(err, "failed to update Teams")
}
return nil
}
// ClearCaches method not implemented.
func (s SqlTeamStore) ClearCaches() {}
// InvalidateAllTeamIdsForUser does not execute anything because the store does not handle the cache.
//
//nolint:unparam
func (s SqlTeamStore) InvalidateAllTeamIdsForUser(userId string) {}
// ClearAllCustomRoleAssignments removes all custom role assignments from TeamMembers.
func (s SqlTeamStore) ClearAllCustomRoleAssignments() (err error) {
builtInRoles := model.MakeDefaultRoles()
lastUserId := strings.Repeat("0", 26)
lastTeamId := strings.Repeat("0", 26)
for {
var transaction *sqlxTxWrapper
var err error
if transaction, err = s.GetMasterX().Beginx(); err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
teamMembers := []*teamMember{}
if err := transaction.Select(&teamMembers, "SELECT * from TeamMembers WHERE (TeamId, UserId) > (?, ?) ORDER BY TeamId, UserId LIMIT 1000", lastTeamId, lastUserId); err != nil {
return errors.Wrap(err, "failed to find TeamMembers")
}
if len(teamMembers) == 0 {
break
}
for _, member := range teamMembers {
lastUserId = member.UserId
lastTeamId = member.TeamId
var newRoles []string
for _, role := range strings.Fields(member.Roles) {
for name := range builtInRoles {
if name == role {
newRoles = append(newRoles, role)
break
}
}
}
newRolesString := strings.Join(newRoles, " ")
if newRolesString != member.Roles {
if _, err := transaction.Exec("UPDATE TeamMembers SET Roles = ? WHERE UserId = ? AND TeamId = ?", newRolesString, member.UserId, member.TeamId); err != nil {
return errors.Wrap(err, "failed to update TeamMembers")
}
}
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
}
return nil
}
// AnalyticsGetTeamCountForScheme returns the number of active teams that match the schemeId passed as parameter.
func (s SqlTeamStore) AnalyticsGetTeamCountForScheme(schemeId string) (int64, error) {
query, args, err := s.getQueryBuilder().
Select("count(*)").
From("Teams").
Where(sq.Eq{"SchemeId": schemeId, "DeleteAt": 0}).ToSql()
if err != nil {
return 0, errors.Wrap(err, "team_tosql")
}
var count int64
err = s.GetReplicaX().Get(&count, query, args...)
if err != nil {
return 0, errors.Wrapf(err, "failed to count Teams with schemeId=%s", schemeId)
}
return count, nil
}
// GetAllForExportAfter returns teams for export, up to a total limit passed as parameter where Teams.Id is greater than the afterId passed as parameter.
func (s SqlTeamStore) GetAllForExportAfter(limit int, afterId string) ([]*model.TeamForExport, error) {
data := []*model.TeamForExport{}
query, args, err := s.getQueryBuilder().
Select("Teams.*", "Schemes.Name as SchemeName").
From("Teams").
LeftJoin("Schemes ON Teams.SchemeId = Schemes.Id").
Where(sq.Gt{"Teams.Id": afterId}).
OrderBy("Id").
Limit(uint64(limit)).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
if err = s.GetReplicaX().Select(&data, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Teams")
}
return data, nil
}
// GetUserTeamIds get the team ids to which the user belongs to. allowFromCache parameter does not have any effect in this Store
//
//nolint:unparam
func (s SqlTeamStore) GetUserTeamIds(userId string, allowFromCache bool) ([]string, error) {
teamIds := []string{}
query, args, err := s.getQueryBuilder().
Select("TeamId").
From("TeamMembers").
Join("Teams ON TeamMembers.TeamId = Teams.Id").
Where(sq.Eq{"TeamMembers.UserId": userId, "TeamMembers.DeleteAt": 0, "Teams.DeleteAt": 0}).ToSql()
if err != nil {
return []string{}, errors.Wrap(err, "team_tosql")
}
err = s.GetReplicaX().Select(&teamIds, query, args...)
if err != nil {
return []string{}, errors.Wrapf(err, "failed to find TeamMembers with userId=%s", userId)
}
return teamIds, nil
}
// GetCommonTeamIDsForTwoUsers returns the intersection of all the teams to which the specified
// users belong.
func (s SqlTeamStore) GetCommonTeamIDsForTwoUsers(userID, otherUserID string) ([]string, error) {
var teamIDs []string
query, args, err := s.getQueryBuilder().
Select("TM1.TeamId").
From("TeamMembers AS TM1").
InnerJoin("TeamMembers AS TM2 ON TM1.TeamId = TM2.TeamId").
InnerJoin("Teams ON TM1.TeamId = Teams.Id").
Where(sq.And{
sq.Eq{"TM1.UserId": userID},
sq.Eq{"TM1.DeleteAt": 0},
sq.Eq{"TM2.UserId": otherUserID},
sq.Eq{"TM2.DeleteAt": 0},
sq.Eq{"Teams.DeleteAt": 0},
}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
err = s.GetReplicaX().Select(&teamIDs, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find TeamMembers with user IDs %s and %s", userID, otherUserID)
}
return teamIDs, nil
}
// GetTeamMembersForExport gets the various teams for which a user, denoted by userId, is a part of.
func (s SqlTeamStore) GetTeamMembersForExport(userId string) ([]*model.TeamMemberForExport, error) {
members := []*model.TeamMemberForExport{}
query, args, err := s.getQueryBuilder().
Select("TeamMembers.TeamId", "TeamMembers.UserId", "TeamMembers.Roles", "TeamMembers.DeleteAt",
"(TeamMembers.SchemeGuest IS NOT NULL AND TeamMembers.SchemeGuest) as SchemeGuest",
"TeamMembers.SchemeUser", "TeamMembers.SchemeAdmin", "Teams.Name as TeamName").
From("TeamMembers").
Join("Teams ON TeamMembers.TeamId = Teams.Id").
Where(sq.Eq{"TeamMembers.UserId": userId, "Teams.DeleteAt": 0}).ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_tosql")
}
err = s.GetReplicaX().Select(&members, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find TeamMembers with userId=%s", userId)
}
return members, nil
}
// UserBelongsToTeams returns true if the user denoted by userId is a member of the teams in the teamIds string array.
func (s SqlTeamStore) UserBelongsToTeams(userId string, teamIds []string) (bool, error) {
idQuery := sq.Eq{
"UserId": userId,
"TeamId": teamIds,
"DeleteAt": 0,
}
query, params, err := s.getQueryBuilder().Select("Count(*)").From("TeamMembers").Where(idQuery).ToSql()
if err != nil {
return false, errors.Wrap(err, "team_tosql")
}
var c int64
err = s.GetReplicaX().Get(&c, query, params...)
if err != nil {
return false, errors.Wrap(err, "failed to count TeamMembers")
}
return c > 0, nil
}
// UpdateMembersRole updates all the members of teamID in the userIds string array to be admins and sets all other
// users as not being admin.
func (s SqlTeamStore) UpdateMembersRole(teamID string, userIDs []string) error {
query, args, err := s.getQueryBuilder().
Update("TeamMembers").
Set("SchemeAdmin", sq.Case().When(sq.Eq{"UserId": userIDs}, "true").Else("false")).
Where(sq.Eq{"TeamId": teamID, "DeleteAt": 0}).
Where(sq.Or{sq.Eq{"SchemeGuest": false}, sq.Expr("SchemeGuest IS NULL")}).ToSql()
if err != nil {
return errors.Wrap(err, "team_tosql")
}
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
return errors.Wrap(err, "failed to update TeamMembers")
}
return nil
}
func applyTeamMemberViewRestrictionsFilter(query sq.SelectBuilder, restrictions *model.ViewUsersRestrictions) sq.SelectBuilder {
if restrictions == nil {
return query
}
// If you have no access to teams or channels, return and empty result.
if restrictions.Teams != nil && len(restrictions.Teams) == 0 && restrictions.Channels != nil && len(restrictions.Channels) == 0 {
return query.Where("1 = 0")
}
teams := make([]any, len(restrictions.Teams))
for i, v := range restrictions.Teams {
teams[i] = v
}
channels := make([]any, len(restrictions.Channels))
for i, v := range restrictions.Channels {
channels[i] = v
}
resultQuery := query.Join("Users ru ON (TeamMembers.UserId = ru.Id)")
if restrictions.Teams != nil && len(restrictions.Teams) > 0 {
resultQuery = resultQuery.Join(fmt.Sprintf("TeamMembers rtm ON ( rtm.UserId = ru.Id AND rtm.DeleteAt = 0 AND rtm.TeamId IN (%s))", sq.Placeholders(len(teams))), teams...)
}
if restrictions.Channels != nil && len(restrictions.Channels) > 0 {
resultQuery = resultQuery.Join(fmt.Sprintf("ChannelMembers rcm ON ( rcm.UserId = ru.Id AND rcm.ChannelId IN (%s))", sq.Placeholders(len(channels))), channels...)
}
return resultQuery.Distinct()
}
func applyTeamMemberViewRestrictionsFilterForStats(query sq.SelectBuilder, restrictions *model.ViewUsersRestrictions) sq.SelectBuilder {
if restrictions == nil {
return query
}
// If you have no access to teams or channels, return and empty result.
if restrictions.Teams != nil && len(restrictions.Teams) == 0 && restrictions.Channels != nil && len(restrictions.Channels) == 0 {
return query.Where("1 = 0")
}
teams := make([]any, len(restrictions.Teams))
for i, v := range restrictions.Teams {
teams[i] = v
}
channels := make([]any, len(restrictions.Channels))
for i, v := range restrictions.Channels {
channels[i] = v
}
resultQuery := query
if restrictions.Teams != nil && len(restrictions.Teams) > 0 {
resultQuery = resultQuery.Join(fmt.Sprintf("TeamMembers rtm ON ( rtm.UserId = Users.Id AND rtm.DeleteAt = 0 AND rtm.TeamId IN (%s))", sq.Placeholders(len(teams))), teams...)
}
if restrictions.Channels != nil && len(restrictions.Channels) > 0 {
resultQuery = resultQuery.Join(fmt.Sprintf("ChannelMembers rcm ON ( rcm.UserId = Users.Id AND rcm.ChannelId IN (%s))", sq.Placeholders(len(channels))), channels...)
}
return resultQuery
}
// GroupSyncedTeamCount returns the number of teams that are group constrained.
func (s SqlTeamStore) GroupSyncedTeamCount() (int64, error) {
builder := s.getQueryBuilder().Select("COUNT(*)").From("Teams").Where(sq.Eq{"GroupConstrained": true, "DeleteAt": 0})
query, args, err := builder.ToSql()
if err != nil {
return 0, errors.Wrap(err, "team_tosql")
}
var count int64
err = s.GetReplicaX().Get(&count, query, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to count Teams")
}
return count, nil
}
func (s SqlTeamStore) GetNewTeamMembersSince(teamID string, since int64, offset int, limit int) (*model.NewTeamMembersList, int64, error) {
builderF := func(selectClause string) sq.SelectBuilder {
return s.getQueryBuilder().
Select(selectClause).
From("TeamMembers").
Join("Users ON Users.id = TeamMembers.userid").
LeftJoin("Bots ON Bots.userid = Users.id").
Where(sq.GtOrEq{"TeamMembers.createat": since}).
Where(sq.Eq{"TeamMembers.deleteat": 0, "teamid": teamID, "Users.deleteat": 0, "Bots.userid": nil})
}
countBuilder := builderF("count(*)")
query, args, err := countBuilder.ToSql()
if err != nil {
return nil, 0, errors.Wrap(err, "team_tosql")
}
var totalCount int64
err = s.GetReplicaX().Get(&totalCount, query, args...)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to count team members since")
}
newTeamMembersBuilder := builderF("Users.Id, Users.Username, Users.FirstName, Users.LastName, Users.Position, Users.LastPictureUpdate, TeamMembers.CreateAt, Users.Nickname").
Limit(uint64(limit + 1)).
Offset(uint64(offset))
query, args, err = newTeamMembersBuilder.ToSql()
if err != nil {
return nil, 0, errors.Wrap(err, "team_tosql")
}
var ntms []*model.NewTeamMember
err = s.GetReplicaX().Select(&ntms, query, args...)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to get team members since")
}
return model.GetNewTeamMembersListWithPagination(ntms, limit), totalCount, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlTermsOfServiceStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
}
func newSqlTermsOfServiceStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.TermsOfServiceStore {
return SqlTermsOfServiceStore{sqlStore, metrics}
}
func (s SqlTermsOfServiceStore) Save(termsOfService *model.TermsOfService) (*model.TermsOfService, error) {
if termsOfService.Id != "" {
return nil, store.NewErrInvalidInput("TermsOfService", "Id", termsOfService.Id)
}
termsOfService.PreSave()
if err := termsOfService.IsValid(); err != nil {
return nil, err
}
query := `INSERT INTO TermsOfService
(Id, CreateAt, UserId, Text)
VALUES
(:Id, :CreateAt, :UserId, :Text)
`
if _, err := s.GetMasterX().NamedExec(query, termsOfService); err != nil {
return nil, errors.Wrapf(err, "could not save a new TermsOfService")
}
return termsOfService, nil
}
func (s SqlTermsOfServiceStore) GetLatest(allowFromCache bool) (*model.TermsOfService, error) {
var termsOfService model.TermsOfService
query := s.getQueryBuilder().
Select("*").
From("TermsOfService").
OrderBy("CreateAt DESC").
Limit(uint64(1))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "could not build sql query to get latest TOS")
}
if err := s.GetReplicaX().Get(&termsOfService, queryString, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("TermsOfService", "CreateAt=latest")
}
return nil, errors.Wrap(err, "could not find latest TermsOfService")
}
return &termsOfService, nil
}
func (s SqlTermsOfServiceStore) Get(id string, allowFromCache bool) (*model.TermsOfService, error) {
var termsOfService model.TermsOfService
queryString, _, err := s.getQueryBuilder().
Select("*").
From("TermsOfService").
Where("id = ?").
ToSql()
if err != nil {
return nil, errors.Wrap(err, "terms_of_service_to_sql")
}
err = s.GetReplicaX().Get(&termsOfService, queryString, id)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("TermsOfService", "id")
}
return nil, errors.Wrapf(err, "could not find TermsOfService with id=%s", id)
}
return &termsOfService, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
"strconv"
"time"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
// JoinedThread allows querying the Threads + Posts table in a single query, before looking up
// users and unpacking into a model.ThreadResponse.
type JoinedThread struct {
PostId string
ReplyCount int64
LastReplyAt int64
LastViewedAt int64
UnreadReplies int64
UnreadMentions int64
Participants model.StringArray
ThreadDeleteAt int64
TeamId string
IsUrgent bool
model.Post
}
func (thread *JoinedThread) toThreadResponse(users map[string]*model.User) *model.ThreadResponse {
threadParticipants := make([]*model.User, 0, len(thread.Participants))
for _, participantUserId := range thread.Participants {
if participant, ok := users[participantUserId]; ok {
threadParticipants = append(threadParticipants, participant)
}
}
return &model.ThreadResponse{
PostId: thread.PostId,
ReplyCount: thread.ReplyCount,
LastReplyAt: thread.LastReplyAt,
LastViewedAt: thread.LastViewedAt,
UnreadReplies: thread.UnreadReplies,
UnreadMentions: thread.UnreadMentions,
Participants: threadParticipants,
Post: thread.Post.ToNilIfInvalid(),
DeleteAt: thread.ThreadDeleteAt,
IsUrgent: thread.IsUrgent,
}
}
type SqlThreadStore struct {
*SqlStore
// threadsSelectQuery is for querying directly into model.Thread
threadsSelectQuery sq.SelectBuilder
// threadsAndPostsSelectQuery is for querying into a struct embedding fields from
// model.Thread and model.Post.
threadsAndPostsSelectQuery sq.SelectBuilder
}
func (s *SqlThreadStore) ClearCaches() {
}
func newSqlThreadStore(sqlStore *SqlStore) store.ThreadStore {
s := SqlThreadStore{
SqlStore: sqlStore,
}
s.initializeQueries()
return &s
}
func (s *SqlThreadStore) initializeQueries() {
s.threadsSelectQuery = s.getQueryBuilder().
Select(
"Threads.PostId",
"Threads.ChannelId",
"Threads.ReplyCount",
"Threads.LastReplyAt",
"Threads.Participants",
"COALESCE(Threads.ThreadDeleteAt, 0) AS DeleteAt",
"COALESCE(Threads.ThreadTeamId, '') AS TeamId",
).
From("Threads")
s.threadsAndPostsSelectQuery = s.getQueryBuilder().
Select(
"Threads.PostId",
"Threads.ChannelId",
"Threads.ReplyCount",
"Threads.LastReplyAt",
"Threads.Participants",
"COALESCE(Threads.ThreadDeleteAt, 0) AS ThreadDeleteAt",
"COALESCE(Threads.ThreadTeamId, '') AS TeamId",
).
From("Threads")
}
func (s *SqlThreadStore) Get(id string) (*model.Thread, error) {
var thread model.Thread
query := s.threadsSelectQuery.
Where(sq.Eq{"PostId": id})
err := s.GetReplicaX().GetBuilder(&thread, query)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, errors.Wrapf(err, "failed to get thread with id=%s", id)
}
return &thread, nil
}
func (s *SqlThreadStore) getTotalThreadsQuery(userId, teamId string, opts model.GetUserThreadsOpts) sq.SelectBuilder {
query := s.getQueryBuilder().
Select("COUNT(ThreadMemberships.PostId)").
From("ThreadMemberships").
LeftJoin("Threads ON Threads.PostId = ThreadMemberships.PostId").
Where(sq.Eq{
"ThreadMemberships.UserId": userId,
"ThreadMemberships.Following": true,
})
if teamId != "" {
query = query.
Where(sq.Or{
sq.Eq{"Threads.ThreadTeamId": teamId},
sq.Eq{"Threads.ThreadTeamId": ""},
})
}
if !opts.Deleted {
query = query.Where(sq.Eq{"COALESCE(Threads.ThreadDeleteAt, 0)": 0})
}
return query
}
// GetTotalUnreadThreads counts the number of unread threads for the given user, optionally
// constrained to the given team + DMs/GMs.
func (s *SqlThreadStore) GetTotalUnreadThreads(userId, teamId string, opts model.GetUserThreadsOpts) (int64, error) {
query := s.getTotalThreadsQuery(userId, teamId, opts).
Where(sq.Expr("ThreadMemberships.LastViewed < Threads.LastReplyAt"))
var totalUnreadThreads int64
err := s.GetReplicaX().GetBuilder(&totalUnreadThreads, query)
if err != nil {
return 0, errors.Wrapf(err, "failed to count unread threads for user id=%s", userId)
}
return totalUnreadThreads, nil
}
// GetTotalUnreadThreads counts the number of threads for the given user, optionally constrained
// to the given team + DMs/GMs.
func (s *SqlThreadStore) GetTotalThreads(userId, teamId string, opts model.GetUserThreadsOpts) (int64, error) {
if opts.Unread {
return 0, errors.New("GetTotalThreads does not support the Unread flag; use GetTotalUnreadThreads instead")
}
query := s.getTotalThreadsQuery(userId, teamId, opts)
var totalThreads int64
err := s.GetReplicaX().GetBuilder(&totalThreads, query)
if err != nil {
return 0, errors.Wrapf(err, "failed to count threads for user id=%s", userId)
}
return totalThreads, nil
}
// GetTotalUnreadMentions counts the number of unread mentions for the given user, optionally
// constrained to the given team + DMs/GMs.
func (s *SqlThreadStore) GetTotalUnreadMentions(userId, teamId string, opts model.GetUserThreadsOpts) (int64, error) {
var totalUnreadMentions int64
query := s.getQueryBuilder().
Select("COALESCE(SUM(ThreadMemberships.UnreadMentions),0)").
From("ThreadMemberships").
LeftJoin("Threads ON Threads.PostId = ThreadMemberships.PostId").
Where(sq.Eq{
"ThreadMemberships.UserId": userId,
"ThreadMemberships.Following": true,
})
if teamId != "" {
query = query.
Where(sq.Or{
sq.Eq{"Threads.ThreadTeamId": teamId},
sq.Eq{"Threads.ThreadTeamId": ""},
})
}
if !opts.Deleted {
query = query.Where(sq.Eq{"COALESCE(Threads.ThreadDeleteAt, 0)": 0})
}
err := s.GetReplicaX().GetBuilder(&totalUnreadMentions, query)
if err != nil {
return 0, errors.Wrapf(err, "failed to count unread mentions for user id=%s", userId)
}
return totalUnreadMentions, nil
}
// GetTotalUnreadUrgentMentions counts the number of unread mentions for the given user, optionally
// constrained to the given team + DMs/GMs.
func (s *SqlThreadStore) GetTotalUnreadUrgentMentions(userId, teamId string, opts model.GetUserThreadsOpts) (int64, error) {
var totalUnreadUrgentMentions int64
query := s.getQueryBuilder().
Select("COALESCE(SUM(ThreadMemberships.UnreadMentions),0)").
From("ThreadMemberships").
Join("PostsPriority ON PostsPriority.PostId = ThreadMemberships.PostId").
Where(sq.Eq{
"ThreadMemberships.UserId": userId,
"ThreadMemberships.Following": true,
"PostsPriority.Priority": model.PostPriorityUrgent,
})
if teamId != "" || !opts.Deleted {
query = query.Join("Threads ON Threads.PostId = ThreadMemberships.PostId")
}
if teamId != "" {
query = query.
Where(sq.Or{
sq.Eq{"Threads.ThreadTeamId": teamId},
sq.Eq{"Threads.ThreadTeamId": ""},
})
}
if !opts.Deleted {
query = query.
Where(sq.Eq{"COALESCE(Threads.ThreadDeleteAt, 0)": 0})
}
err := s.GetReplicaX().GetBuilder(&totalUnreadUrgentMentions, query)
if err != nil {
return 0, errors.Wrapf(err, "failed to count unread urgent mentions for user id=%s", userId)
}
return totalUnreadUrgentMentions, nil
}
func (s *SqlThreadStore) GetThreadsForUser(userId, teamId string, opts model.GetUserThreadsOpts) ([]*model.ThreadResponse, error) {
pageSize := uint64(30)
if opts.PageSize != 0 {
pageSize = opts.PageSize
}
unreadRepliesQuery := sq.
Select("COUNT(Posts.Id)").
From("Posts").
Where(sq.Expr("Posts.RootId = ThreadMemberships.PostId")).
Where(sq.Expr("Posts.CreateAt > ThreadMemberships.LastViewed"))
if !opts.Deleted {
unreadRepliesQuery = unreadRepliesQuery.Where(sq.Eq{"Posts.DeleteAt": 0})
}
query := s.threadsAndPostsSelectQuery.
Column(postSliceCoalesceQuery()).
Columns(
"ThreadMemberships.LastViewed as LastViewedAt",
"ThreadMemberships.UnreadMentions as UnreadMentions",
).
Column(sq.Alias(unreadRepliesQuery, "UnreadReplies")).
Join("Posts ON Posts.Id = Threads.PostId").
Join("ThreadMemberships ON ThreadMemberships.PostId = Threads.PostId")
query = query.
Where(sq.Eq{"ThreadMemberships.UserId": userId}).
Where(sq.Eq{"ThreadMemberships.Following": true})
if opts.IncludeIsUrgent {
urgencyCase := sq.
Case().
When(sq.Eq{"PostsPriority.Priority": model.PostPriorityUrgent}, "true").
Else("false")
query = query.
Column(sq.Alias(urgencyCase, "IsUrgent")).
LeftJoin("PostsPriority ON PostsPriority.PostId = Threads.PostId")
}
// If a team is specified, constrain to channels in that team or DMs/GMs without
// a team at all.
if teamId != "" {
query = query.
Where(sq.Or{
sq.Eq{"Threads.ThreadTeamId": teamId},
sq.Eq{"Threads.ThreadTeamId": ""},
})
}
if !opts.Deleted {
query = query.Where(sq.Or{
sq.Eq{"Threads.ThreadDeleteAt": nil},
sq.Eq{"Threads.ThreadDeleteAt": 0},
})
}
if opts.Since > 0 {
query = query.
Where(sq.Or{
sq.GtOrEq{"ThreadMemberships.LastUpdated": opts.Since},
sq.GtOrEq{"Threads.LastReplyAt": opts.Since},
})
}
if opts.Unread {
query = query.Where(sq.Expr("ThreadMemberships.LastViewed < Threads.LastReplyAt"))
}
order := "DESC"
if opts.Before != "" {
query = query.Where(sq.Expr(`Threads.LastReplyAt < (SELECT LastReplyAt FROM Threads WHERE PostId = ?)`, opts.Before))
}
if opts.After != "" {
order = "ASC"
query = query.Where(sq.Expr(`Threads.LastReplyAt > (SELECT LastReplyAt FROM Threads WHERE PostId = ?)`, opts.After))
}
query = query.
OrderBy("Threads.LastReplyAt " + order).
Limit(pageSize)
var threads []*JoinedThread
err := s.GetReplicaX().SelectBuilder(&threads, query)
if err != nil {
return nil, errors.Wrapf(err, "failed to fetch threads for user id=%s", userId)
}
// Build the de-duplicated set of user ids representing participants across all threads.
var participantUserIds []string
for _, thread := range threads {
for _, participantUserId := range thread.Participants {
participantUserIds = append(participantUserIds, participantUserId)
}
}
participantUserIds = model.RemoveDuplicateStrings(participantUserIds)
// Resolve the user objects for all participants, with extended metadata if requested.
allParticipants := make(map[string]*model.User, len(participantUserIds))
if opts.Extended {
users, err := s.User().GetProfileByIds(context.Background(), participantUserIds, &store.UserGetByIdsOpts{}, true)
if err != nil {
return nil, errors.Wrapf(err, "failed to get %d thread profiles for user id=%s", len(participantUserIds), userId)
}
for _, user := range users {
allParticipants[user.Id] = user
}
} else {
for _, participantUserId := range participantUserIds {
allParticipants[participantUserId] = &model.User{Id: participantUserId}
}
}
result := make([]*model.ThreadResponse, 0, len(threads))
for _, thread := range threads {
result = append(result, thread.toThreadResponse(allParticipants))
}
return result, nil
}
// GetTeamsUnreadForUser returns the total unread threads and unread mentions
// for a user from all teams.
func (s *SqlThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string, includeUrgentMentionCount bool) (map[string]*model.TeamUnread, error) {
fetchConditions := sq.And{
sq.Eq{"ThreadMemberships.UserId": userID},
sq.Eq{"ThreadMemberships.Following": true},
sq.Eq{"Threads.ThreadTeamId": teamIDs},
sq.Eq{"COALESCE(Threads.ThreadDeleteAt, 0)": 0},
}
var eg errgroup.Group
unreadThreads := []struct {
Count int64
TeamId string
}{}
unreadMentions := []struct {
Count int64
TeamId string
}{}
unreadUrgentMentions := []struct {
Count int64
TeamId string
}{}
// Running these concurrently hasn't shown any major downside
// than running them serially. So using a bit of perf boost.
// In any case, they will be replaced by computed columns later.
eg.Go(func() error {
repliesQuery := s.getQueryBuilder().
Select("COUNT(Threads.PostId) AS Count, ThreadTeamId AS TeamId").
From("Threads").
LeftJoin("ThreadMemberships ON Threads.PostId = ThreadMemberships.PostId").
Where(fetchConditions).
Where("Threads.LastReplyAt > ThreadMemberships.LastViewed").
GroupBy("Threads.ThreadTeamId")
return errors.Wrap(s.GetReplicaX().SelectBuilder(&unreadThreads, repliesQuery), "failed to get total unread threads")
})
eg.Go(func() error {
mentionsQuery := s.getQueryBuilder().
Select("COALESCE(SUM(ThreadMemberships.UnreadMentions),0) AS Count, ThreadTeamId AS TeamId").
From("ThreadMemberships").
LeftJoin("Threads ON Threads.PostId = ThreadMemberships.PostId").
Where(fetchConditions).
GroupBy("Threads.ThreadTeamId")
return errors.Wrap(s.GetReplicaX().SelectBuilder(&unreadMentions, mentionsQuery), "failed to get total unread mentions")
})
if includeUrgentMentionCount {
eg.Go(func() error {
urgentMentionsQuery := s.getQueryBuilder().
Select("COALESCE(SUM(ThreadMemberships.UnreadMentions),0) AS Count, ThreadTeamId AS TeamId").
From("ThreadMemberships").
LeftJoin("Threads ON Threads.PostId = ThreadMemberships.PostId").
Join("PostsPriority ON PostsPriority.PostId = ThreadMemberships.PostId").
Where(sq.Eq{"PostsPriority.Priority": model.PostPriorityUrgent}).
Where(fetchConditions).
GroupBy("Threads.ThreadTeamId")
return errors.Wrap(s.GetReplicaX().SelectBuilder(&unreadUrgentMentions, urgentMentionsQuery), "failed to get total unread urgent mentions")
})
}
// Wait for them to be over
if err := eg.Wait(); err != nil {
return nil, err
}
res := make(map[string]*model.TeamUnread)
// A bit of linear complexity here to create and return the map.
// This makes it easy to consume the output in the app layer.
for _, item := range unreadThreads {
res[item.TeamId] = &model.TeamUnread{
ThreadCount: item.Count,
}
}
for _, item := range unreadMentions {
if _, ok := res[item.TeamId]; ok {
res[item.TeamId].ThreadMentionCount = item.Count
} else {
res[item.TeamId] = &model.TeamUnread{
ThreadMentionCount: item.Count,
}
}
}
for _, item := range unreadUrgentMentions {
if _, ok := res[item.TeamId]; ok {
res[item.TeamId].ThreadUrgentMentionCount = item.Count
} else {
res[item.TeamId] = &model.TeamUnread{
ThreadUrgentMentionCount: item.Count,
}
}
}
return res, nil
}
func (s *SqlThreadStore) GetThreadFollowers(threadID string, fetchOnlyActive bool) ([]string, error) {
users := []string{}
fetchConditions := sq.And{
sq.Eq{"PostId": threadID},
}
if fetchOnlyActive {
fetchConditions = sq.And{
sq.Eq{"Following": true},
fetchConditions,
}
}
query := s.getQueryBuilder().
Select("ThreadMemberships.UserId").
From("ThreadMemberships").
Where(fetchConditions)
err := s.GetReplicaX().SelectBuilder(&users, query)
if err != nil {
return nil, errors.Wrapf(err, "failed to get thread followers for thread id=%s", threadID)
}
return users, nil
}
func (s *SqlThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership, extended, postPriorityEnabled bool) (*model.ThreadResponse, error) {
if !threadMembership.Following {
return nil, store.NewErrNotFound("ThreadMembership", "<following>")
}
unreadRepliesQuery := sq.
Select("COUNT(Posts.Id)").
From("Posts").
Where(sq.And{
sq.Eq{"Posts.RootId": threadMembership.PostId},
sq.Gt{"Posts.CreateAt": threadMembership.LastViewed},
sq.Eq{"Posts.DeleteAt": 0},
})
query := s.threadsAndPostsSelectQuery
for _, c := range postSliceColumns() {
query = query.Column("Posts." + c)
}
var thread JoinedThread
query = query.
Column(sq.Alias(unreadRepliesQuery, "UnreadReplies")).
LeftJoin("Posts ON Posts.Id = Threads.PostId").
Where(sq.Eq{"Threads.PostId": threadMembership.PostId})
if postPriorityEnabled {
urgencyCase := sq.
Case().
When(sq.Eq{"PostsPriority.Priority": model.PostPriorityUrgent}, "true").
Else("false")
query = query.
Column(sq.Alias(urgencyCase, "IsUrgent")).
LeftJoin("PostsPriority ON PostsPriority.PostId = Threads.PostId")
}
err := s.GetReplicaX().GetBuilder(&thread, query)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Thread", threadMembership.PostId)
}
return nil, errors.Wrapf(err, "failed to get thread for user id=%s, post id=%s", threadMembership.UserId, threadMembership.PostId)
}
thread.LastViewedAt = threadMembership.LastViewed
thread.UnreadMentions = threadMembership.UnreadMentions
users := []*model.User{}
if extended {
var err error
users, err = s.User().GetProfileByIds(context.Background(), thread.Participants, &store.UserGetByIdsOpts{}, true)
if err != nil {
return nil, errors.Wrapf(err, "failed to get thread for user id=%s", threadMembership.UserId)
}
} else {
for _, userId := range thread.Participants {
users = append(users, &model.User{Id: userId})
}
}
usersMap := make(map[string]*model.User)
for _, user := range users {
usersMap[user.Id] = user
}
return thread.toThreadResponse(usersMap), nil
}
// MarkAllAsReadByChannels marks thread membership for the given users in the given channels
// as read. This is used by the application layer to keep threads up-to-date when CRT is disabled
// for the enduser, avoiding an influx of unread threads when first turning the feature on.
func (s *SqlThreadStore) MarkAllAsReadByChannels(userID string, channelIDs []string) error {
if len(channelIDs) == 0 {
return nil
}
now := model.GetMillis()
var query sq.UpdateBuilder
if s.DriverName() == model.DatabaseDriverPostgres {
query = s.getQueryBuilder().Update("ThreadMemberships").From("Threads")
} else {
query = s.getQueryBuilder().Update("ThreadMemberships", "Threads")
}
query = query.Set("LastViewed", now).
Set("UnreadMentions", 0).
Set("LastUpdated", now).
Where(sq.Eq{"ThreadMemberships.UserId": userID}).
Where(sq.Expr("Threads.PostId = ThreadMemberships.PostId")).
Where(sq.Eq{"Threads.ChannelId": channelIDs}).
Where(sq.Expr("Threads.LastReplyAt > ThreadMemberships.LastViewed"))
if _, err := s.GetMasterX().ExecBuilder(query); err != nil {
return errors.Wrapf(err, "failed to mark all threads as read by channels for user id=%s", userID)
}
return nil
}
func (s *SqlThreadStore) MarkAllAsRead(userId string, threadIds []string) error {
timestamp := model.GetMillis()
query := s.getQueryBuilder().
Update("ThreadMemberships").
Where(sq.Eq{"UserId": userId}).
Where(sq.Eq{"PostId": threadIds}).
Set("LastViewed", timestamp).
Set("UnreadMentions", 0).
Set("LastUpdated", model.GetMillis())
_, err := s.GetMasterX().ExecBuilder(query)
if err != nil {
return errors.Wrapf(err, "failed to mark %d threads as read for user id=%s", len(threadIds), userId)
}
return nil
}
// MarkAllAsReadByTeam marks all threads for the given user in the given team as read from the
// current time.
func (s *SqlThreadStore) MarkAllAsReadByTeam(userId, teamId string) error {
timestamp := model.GetMillis()
var query sq.UpdateBuilder
if s.DriverName() == model.DatabaseDriverPostgres {
query = s.getQueryBuilder().Update("ThreadMemberships").From("Threads")
} else {
query = s.getQueryBuilder().Update("ThreadMemberships", "Threads")
}
query = query.
Where("Threads.PostId = ThreadMemberships.PostId").
Where(sq.Eq{"ThreadMemberships.UserId": userId}).
Where(sq.Or{sq.Eq{"Threads.ThreadTeamId": teamId}, sq.Eq{"Threads.ThreadTeamId": ""}}).
Set("LastViewed", timestamp).
Set("UnreadMentions", 0).
Set("LastUpdated", timestamp)
_, err := s.GetMasterX().ExecBuilder(query)
if err != nil {
return errors.Wrapf(err, "failed to update thread read state for user id=%s", userId)
}
return nil
}
// MarkAsRead marks the given thread for the given user as unread from the given timestamp.
func (s *SqlThreadStore) MarkAsRead(userId, threadId string, timestamp int64) error {
query := s.getQueryBuilder().
Update("ThreadMemberships").
Where(sq.Eq{"UserId": userId}).
Where(sq.Eq{"PostId": threadId}).
Set("LastViewed", timestamp).
Set("LastUpdated", model.GetMillis())
_, err := s.GetMasterX().ExecBuilder(query)
if err != nil {
return errors.Wrapf(err, "failed to update thread read state for user id=%s thread_id=%v", userId, threadId)
}
return nil
}
func (s *SqlThreadStore) saveMembership(ex sqlxExecutor, membership *model.ThreadMembership) (*model.ThreadMembership, error) {
query := s.getQueryBuilder().
Insert("ThreadMemberships").
Columns("PostId", "UserId", "Following", "LastViewed", "LastUpdated", "UnreadMentions").
Values(membership.PostId, membership.UserId, membership.Following, membership.LastViewed, membership.LastUpdated, membership.UnreadMentions)
_, err := ex.ExecBuilder(query)
if err != nil {
return nil, errors.Wrapf(err, "failed to save thread membership with postid=%s userid=%s", membership.PostId, membership.UserId)
}
return membership, nil
}
func (s *SqlThreadStore) UpdateMembership(membership *model.ThreadMembership) (*model.ThreadMembership, error) {
return s.updateMembership(s.GetMasterX(), membership)
}
func (s *SqlThreadStore) updateMembership(ex sqlxExecutor, membership *model.ThreadMembership) (*model.ThreadMembership, error) {
query := s.getQueryBuilder().
Update("ThreadMemberships").
Set("Following", membership.Following).
Set("LastViewed", membership.LastViewed).
Set("LastUpdated", membership.LastUpdated).
Set("UnreadMentions", membership.UnreadMentions).
Where(sq.And{
sq.Eq{"PostId": membership.PostId},
sq.Eq{"UserId": membership.UserId},
})
_, err := ex.ExecBuilder(query)
if err != nil {
return nil, errors.Wrapf(err, "failed to update thread membership with postid=%s userid=%s", membership.PostId, membership.UserId)
}
return membership, nil
}
func (s *SqlThreadStore) GetMembershipsForUser(userId, teamId string) ([]*model.ThreadMembership, error) {
memberships := []*model.ThreadMembership{}
query := s.getQueryBuilder().
Select("ThreadMemberships.*").
Join("Threads ON Threads.PostId = ThreadMemberships.PostId").
From("ThreadMemberships").
Where(sq.Or{sq.Eq{"Threads.ThreadTeamId": teamId}, sq.Eq{"Threads.ThreadTeamId": ""}}).
Where(sq.Eq{"ThreadMemberships.UserId": userId})
err := s.GetReplicaX().SelectBuilder(&memberships, query)
if err != nil {
return nil, errors.Wrapf(err, "failed to get thread membership with userid=%s", userId)
}
return memberships, nil
}
func (s *SqlThreadStore) GetMembershipForUser(userId, postId string) (*model.ThreadMembership, error) {
return s.getMembershipForUser(s.GetReplicaX(), userId, postId)
}
func (s *SqlThreadStore) getMembershipForUser(ex sqlxExecutor, userId, postId string) (*model.ThreadMembership, error) {
var membership model.ThreadMembership
query := s.getQueryBuilder().
Select("*").
From("ThreadMemberships").
Where(sq.And{
sq.Eq{"PostId": postId},
sq.Eq{"UserId": userId},
})
err := ex.GetBuilder(&membership, query)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Thread", postId)
}
return nil, errors.Wrapf(err, "failed to get thread membership with userid=%s postid=%s", userId, postId)
}
return &membership, nil
}
func (s *SqlThreadStore) DeleteMembershipForUser(userId string, postId string) error {
query := s.getQueryBuilder().
Delete("ThreadMemberships").
Where(sq.And{
sq.Eq{"PostId": postId},
sq.Eq{"UserId": userId},
})
_, err := s.GetMasterX().ExecBuilder(query)
if err != nil {
return errors.Wrap(err, "failed to delete thread membership")
}
return nil
}
// MaintainMembership creates or updates a thread membership for the given user
// and post. This method is used to update the state of a membership in response
// to some events like:
// - post creation (mentions handling)
// - channel marked unread
// - user explicitly following a thread
func (s *SqlThreadStore) MaintainMembership(userId, postId string, opts store.ThreadMembershipOpts) (_ *model.ThreadMembership, err error) {
trx, err := s.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(trx, &err)
membership, err := s.getMembershipForUser(trx, userId, postId)
now := utils.MillisFromTime(time.Now())
// if membership exists, update it if:
// a. user started/stopped following a thread
// b. mention count changed
// c. user viewed a thread
if err == nil {
followingNeedsUpdate := (opts.UpdateFollowing && (membership.Following != opts.Following))
if followingNeedsUpdate || opts.IncrementMentions || opts.UpdateViewedTimestamp {
if followingNeedsUpdate {
membership.Following = opts.Following
}
if opts.UpdateViewedTimestamp {
membership.LastViewed = now
membership.UnreadMentions = 0
} else if opts.IncrementMentions {
membership.UnreadMentions += 1
}
membership.LastUpdated = now
if _, err = s.updateMembership(trx, membership); err != nil {
return nil, err
}
}
if err = trx.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return membership, err
}
var nfErr *store.ErrNotFound
if !errors.As(err, &nfErr) {
return nil, errors.Wrap(err, "failed to get thread membership")
}
membership = &model.ThreadMembership{
PostId: postId,
UserId: userId,
Following: opts.Following,
LastUpdated: now,
}
if opts.IncrementMentions {
membership.UnreadMentions = 1
}
if opts.UpdateViewedTimestamp {
membership.LastViewed = now
}
membership, err = s.saveMembership(trx, membership)
if err != nil {
return nil, err
}
if opts.UpdateParticipants {
if s.DriverName() == model.DatabaseDriverPostgres {
userIdParam, err2 := jsonArray([]string{userId}).Value()
if err2 != nil {
return nil, err2
}
if s.IsBinaryParamEnabled() {
userIdParam = AppendBinaryFlag(userIdParam.([]byte))
}
if _, err2 := trx.ExecRaw(`UPDATE Threads
SET participants = participants || $1::jsonb
WHERE postid=$2
AND NOT participants ? $3`, userIdParam, postId, userId); err2 != nil {
return nil, err2
}
} else {
// CONCAT('$[', JSON_LENGTH(Participants), ']') just generates $[n]
// which is the positional syntax required for appending.
if _, err2 := trx.Exec(`UPDATE Threads
SET Participants = JSON_ARRAY_INSERT(Participants, CONCAT('$[', JSON_LENGTH(Participants), ']'), ?)
WHERE PostId=?
AND NOT JSON_CONTAINS(Participants, ?)`, userId, postId, strconv.Quote(userId)); err2 != nil {
return nil, err2
}
}
}
if err = trx.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return membership, err
}
// PermanentDeleteBatchForRetentionPolicies deletes a batch of records which are affected by
// the global or a granular retention policy.
// See `genericPermanentDeleteBatchForRetentionPolicies` for details.
func (s *SqlThreadStore) PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
builder := s.getQueryBuilder().
Select("Threads.PostId").
From("Threads")
return genericPermanentDeleteBatchForRetentionPolicies(RetentionPolicyBatchDeletionInfo{
BaseBuilder: builder,
Table: "Threads",
TimeColumn: "LastReplyAt",
PrimaryKeys: []string{"PostId"},
ChannelIDTable: "Threads",
NowMillis: now,
GlobalPolicyEndTime: globalPolicyEndTime,
Limit: limit,
}, s.SqlStore, cursor)
}
// PermanentDeleteBatchThreadMembershipsForRetentionPolicies deletes a batch of records
// which are affected by the global or a granular retention policy.
// See `genericPermanentDeleteBatchForRetentionPolicies` for details.
func (s *SqlThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
builder := s.getQueryBuilder().
Select("ThreadMemberships.PostId").
From("ThreadMemberships").
InnerJoin("Threads ON ThreadMemberships.PostId = Threads.PostId")
return genericPermanentDeleteBatchForRetentionPolicies(RetentionPolicyBatchDeletionInfo{
BaseBuilder: builder,
Table: "ThreadMemberships",
TimeColumn: "LastUpdated",
PrimaryKeys: []string{"PostId"},
ChannelIDTable: "Threads",
NowMillis: now,
GlobalPolicyEndTime: globalPolicyEndTime,
Limit: limit,
}, s.SqlStore, cursor)
}
// DeleteOrphanedRows removes orphaned rows from Threads and ThreadMemberships
func (s *SqlThreadStore) DeleteOrphanedRows(limit int) (deleted int64, err error) {
var threadsQuery string
// We need the extra level of nesting to deal with MySQL's locking
if s.DriverName() == model.DatabaseDriverMysql {
// MySQL fails to do a proper antijoin if the selecting column
// and the joining column are different. In that case, doing a subquery
// leads to a faster plan because MySQL materializes the sub-query
// and does a covering index scan on Threads table. More details on the PR with
// this commit.
threadsQuery = `
DELETE FROM Threads WHERE PostId IN (
SELECT * FROM (
SELECT Threads.PostId FROM Threads
WHERE Threads.ChannelId NOT IN (SELECT Id FROM Channels USE INDEX(PRIMARY))
LIMIT ?
) AS A
)`
} else {
threadsQuery = `
DELETE FROM Threads WHERE PostId IN (
SELECT * FROM (
SELECT Threads.PostId FROM Threads
LEFT JOIN Channels ON Threads.ChannelId = Channels.Id
WHERE Channels.Id IS NULL
LIMIT ?
) AS A
)`
}
// We only delete a thread membership if the entire thread no longer exists,
// not if the root post has been deleted
const threadMembershipsQuery = `
DELETE FROM ThreadMemberships WHERE PostId IN (
SELECT * FROM (
SELECT ThreadMemberships.PostId FROM ThreadMemberships
LEFT JOIN Threads ON ThreadMemberships.PostId = Threads.PostId
WHERE Threads.PostId IS NULL
LIMIT ?
) AS A
)`
result, err := s.GetMasterX().Exec(threadsQuery, limit)
if err != nil {
return
}
rpcDeleted, err := result.RowsAffected()
if err != nil {
return
}
result, err = s.GetMasterX().Exec(threadMembershipsQuery, limit)
if err != nil {
return
}
rptDeleted, err := result.RowsAffected()
if err != nil {
return
}
deleted = rpcDeleted + rptDeleted
return
}
// return number of unread replies for a single thread
func (s *SqlThreadStore) GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error) {
query := s.getQueryBuilder().
Select("COUNT(Posts.Id)").
From("Posts").
Where(sq.And{
sq.Eq{"Posts.RootId": threadMembership.PostId},
sq.Gt{"Posts.CreateAt": threadMembership.LastViewed},
sq.Eq{"Posts.DeleteAt": 0},
})
var unreadReplies int64
err := s.GetReplicaX().GetBuilder(&unreadReplies, query)
if err != nil {
return 0, errors.Wrapf(err, "failed to count unread reply count for post id=%s", threadMembership.PostId)
}
return unreadReplies, nil
}
// Top threads in all public channels and private channels userID is a member of. Returns a list of threads ranked by interactions.
func (s *SqlThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
var args []any
query := `select
threads_list.PostId,
threads_list.ReplyCount,
threads_list.ChannelId,
threads_list.DisplayName,
threads_list.Name,
threads_list.Participants,
p.UserId
from((
SELECT
t.PostId,
t.ReplyCount,
t.ChannelId,
t.Participants,
c.DisplayName,
c.Name
FROM
Threads t
LEFT JOIN PublicChannels c ON t.ChannelId = c.Id
WHERE
t.threaddeleteat IS NULL
AND t.LastReplyAt > ?
AND c.TeamId = ?
GROUP BY
t.PostId,
c.DisplayName,
c.Name,
t.Participants
)
UNION
ALL (
SELECT
t.PostId,
t.ReplyCount,
t.ChannelId,
t.Participants,
c.DisplayName,
c.Name
FROM
Threads t
LEFT JOIN ChannelMembers cm ON t.ChannelId = cm.ChannelId
LEFT JOIN Channels c ON t.ChannelId = c.Id
WHERE
t.threaddeleteat IS NULL
AND cm.UserId = ?
AND c.Type = 'P'
AND c.TeamId = ?
AND t.LastReplyAt > ?
GROUP BY
t.PostId,
c.DisplayName,
c.Name,
t.Participants
)) as threads_list
LEFT JOIN Posts as p on p.Id = threads_list.PostId
ORDER BY ReplyCount DESC
limit ? offset ?`
args = append(args, since, teamID, userID, teamID, since, limit+1, offset)
topThreads := make([]*model.TopThread, 0)
err := s.GetReplicaX().Select(&topThreads, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get top threads=%s", teamID)
}
topThreads, err = postProcessTopThreads(topThreads, s, teamID)
if err != nil {
return nil, err
}
return model.GetTopThreadListWithPagination(topThreads, limit), nil
}
func (s *SqlThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
var args []any
// gets all threads within the team which user follows.
query := `select
threads_list.PostId,
threads_list.ReplyCount,
threads_list.ChannelId,
threads_list.DisplayName,
threads_list.Name,
threads_list.Participants,
p.UserId
from((
SELECT
t.PostId,
t.ReplyCount,
t.ChannelId,
t.Participants,
c.DisplayName,
c.Name
FROM
Threads t
LEFT JOIN PublicChannels c ON t.ChannelId = c.Id
LEFT JOIN ThreadMemberships as tm on t.PostId = tm.PostId
WHERE
t.threaddeleteat IS NULL
AND t.LastReplyAt > ?
AND c.TeamId = ?
AND tm.UserId = ?
AND tm.Following = TRUE
GROUP BY
t.PostId,
c.DisplayName,
c.Name,
t.Participants
)
UNION
ALL (
SELECT
t.PostId,
t.ReplyCount,
t.ChannelId,
t.Participants,
c.DisplayName,
c.Name
FROM
Threads t
LEFT JOIN ChannelMembers cm ON t.ChannelId = cm.ChannelId
LEFT JOIN Channels c ON t.ChannelId = c.Id
LEFT JOIN ThreadMemberships as tm on t.PostId = tm.PostId
WHERE
cm.UserId = ?
AND c.Type = 'P'
AND c.TeamId = ?
AND t.threaddeleteat IS NULL
AND t.LastReplyAt > ?
AND tm.UserId = ?
AND tm.Following = TRUE
GROUP BY
t.PostId,
c.DisplayName,
c.Name,
t.Participants
)) as threads_list
LEFT JOIN Posts as p on p.Id = threads_list.PostId
ORDER BY ReplyCount DESC
limit ? offset ?`
args = append(args, since, teamID, userID, userID, teamID, since, userID, limit+1, offset)
topThreads := make([]*model.TopThread, 0)
err := s.GetReplicaX().Select(&topThreads, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get top threads=%s", teamID)
}
topThreads, err = postProcessTopThreads(topThreads, s, teamID)
if err != nil {
return nil, err
}
return model.GetTopThreadListWithPagination(topThreads, limit), nil
}
func userContains(userIDs []string, searchedUserID string) bool {
for _, userID := range userIDs {
if userID == searchedUserID {
return true
}
}
return false
}
func postProcessTopThreads(topThreads []*model.TopThread, s *SqlThreadStore, teamID string) ([]*model.TopThread, error) {
// create list of userIDs
var userIDs []string
for _, topThread := range topThreads {
userID := topThread.UserId
if !userContains(userIDs, userID) {
userIDs = append(userIDs, userID)
}
}
usersMap := map[string]*model.User{}
users, err := s.User().GetProfileByIds(context.Background(), userIDs, &store.UserGetByIdsOpts{}, true)
if err != nil {
return nil, errors.Wrapf(err, "failed to get users for top threads in team=%s", teamID)
}
for _, user := range users {
usersMap[user.Id] = user
}
// resolve user, root post for each top thread
for _, topThread := range topThreads {
postCreator := usersMap[topThread.UserId]
topThread.UserInformation = &model.InsightUserInformation{
Id: postCreator.Id,
LastPictureUpdate: postCreator.LastPictureUpdate,
FirstName: postCreator.FirstName,
LastName: postCreator.LastName,
Username: postCreator.Username,
NickName: postCreator.Nickname,
}
post, err := s.Post().GetSingle(topThread.PostId, false)
if err != nil {
return nil, errors.Wrapf(err, "failed to get extended post for post id=%s", topThread.PostId)
}
topThread.Post = post
}
return topThreads, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SqlTokenStore struct {
*SqlStore
}
func newSqlTokenStore(sqlStore *SqlStore) store.TokenStore {
return &SqlTokenStore{sqlStore}
}
func (s SqlTokenStore) Save(token *model.Token) error {
if err := token.IsValid(); err != nil {
return err
}
query, args, err := s.getQueryBuilder().
Insert("Tokens").
Columns("Token", "CreateAt", "Type", "Extra").
Values(token.Token, token.CreateAt, token.Type, token.Extra).
ToSql()
if err != nil {
return errors.Wrap(err, "token_tosql")
}
if _, err := s.GetMasterX().Exec(query, args...); err != nil {
return errors.Wrap(err, "failed to save Token")
}
return nil
}
func (s SqlTokenStore) Delete(token string) error {
if _, err := s.GetMasterX().Exec("DELETE FROM Tokens WHERE Token = ?", token); err != nil {
return errors.Wrapf(err, "failed to delete Token with value %s", token)
}
return nil
}
func (s SqlTokenStore) GetByToken(tokenString string) (*model.Token, error) {
var token model.Token
if err := s.GetReplicaX().Get(&token, "SELECT * FROM Tokens WHERE Token = ?", tokenString); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Token", fmt.Sprintf("Token=%s", tokenString))
}
return nil, errors.Wrapf(err, "failed to get Token with value %s", tokenString)
}
return &token, nil
}
func (s SqlTokenStore) Cleanup(expiryTime int64) {
if _, err := s.GetMasterX().Exec("DELETE FROM Tokens WHERE CreateAt < ?", expiryTime); err != nil {
mlog.Error("Unable to cleanup token store.")
}
}
func (s SqlTokenStore) GetAllTokensByType(tokenType string) ([]*model.Token, error) {
tokens := []*model.Token{}
query, args, err := s.getQueryBuilder().
Select("*").
From("Tokens").
Where(sq.Eq{"Type": tokenType}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "could not build sql query to get all tokens by type")
}
if err := s.GetReplicaX().Select(&tokens, query, args...); err != nil {
return nil, errors.Wrapf(err, "failed to get all tokens of Type=%s", tokenType)
}
return tokens, nil
}
func (s SqlTokenStore) RemoveAllTokensByType(tokenType string) error {
if _, err := s.GetMasterX().Exec("DELETE FROM Tokens WHERE Type = ?", tokenType); err != nil {
return errors.Wrapf(err, "failed to remove all Tokens with Type=%s", tokenType)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"strconv"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
// SqlLicenseStore encapsulates the database writes and reads for
// model.LicenseRecord objects.
type SqlTrueUpReviewStore struct {
*SqlStore
}
func newSqlTrueUpReviewStore(sqlStore *SqlStore) store.TrueUpReviewStore {
return &SqlTrueUpReviewStore{sqlStore}
}
func trueUpReviewStatusColumns() []string {
return []string{
"DueDate",
"Completed",
}
}
func (s *SqlTrueUpReviewStore) GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error) {
query := s.getQueryBuilder().
Select("*").
From("TrueUpReviewHistory").
Where(sq.Eq{"DueDate": dueDate})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_trueUpReviewStatusRecord_tosql")
}
var trueUpReviewStatus model.TrueUpReviewStatus
if err := s.GetReplicaX().Get(&trueUpReviewStatus, queryString, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("TrueUpReviewStatus", strconv.FormatInt(dueDate, 10))
}
return nil, err
}
return &trueUpReviewStatus, nil
}
func (s *SqlTrueUpReviewStore) CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
builder := s.getQueryBuilder().Insert("TrueUpReviewHistory").Columns(trueUpReviewStatusColumns()...).Values(reviewStatus.ToSlice()...)
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "create_trueUpReviewStatusRecord_tosql")
}
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrap(err, "fail to create true up review status record")
}
return reviewStatus, nil
}
func (s *SqlTrueUpReviewStore) Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
query := s.getQueryBuilder().
Update("TrueUpReviewHistory").
Set("Completed", reviewStatus.Completed).
Where(sq.Eq{"DueDate": reviewStatus.DueDate})
if _, err := s.GetMasterX().ExecBuilder(query); err != nil {
return nil, errors.Wrapf(err, "failed to update true up review status with DueDate=%d", reviewStatus.DueDate)
}
return reviewStatus, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlUploadSessionStore struct {
*SqlStore
}
func newSqlUploadSessionStore(sqlStore *SqlStore) store.UploadSessionStore {
return &SqlUploadSessionStore{
SqlStore: sqlStore,
}
}
func (us SqlUploadSessionStore) Save(session *model.UploadSession) (*model.UploadSession, error) {
if session == nil {
return nil, errors.New("SqlUploadSessionStore.Save: session should not be nil")
}
session.PreSave()
if err := session.IsValid(); err != nil {
return nil, errors.Wrap(err, "SqlUploadSessionStore.Save: validation failed")
}
query, args, err := us.getQueryBuilder().
Insert("UploadSessions").
Columns("Id", "Type", "CreateAt", "UserId", "ChannelId", "Filename", "Path", "FileSize", "FileOffset", "RemoteId", "ReqFileId").
Values(session.Id, session.Type, session.CreateAt, session.UserId, session.ChannelId, session.Filename, session.Path, session.FileSize, session.FileOffset, session.RemoteId, session.ReqFileId).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "SqlUploadSessionStore.Save: failed to build query")
}
if _, err := us.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrap(err, "SqlUploadSessionStore.Save: failed to insert")
}
return session, nil
}
func (us SqlUploadSessionStore) Update(session *model.UploadSession) error {
if session == nil {
return errors.New("SqlUploadSessionStore.Update: session should not be nil")
}
if err := session.IsValid(); err != nil {
return errors.Wrap(err, "SqlUploadSessionStore.Update: validation failed")
}
query, args, err := us.getQueryBuilder().
Update("UploadSessions").
Set("Type", session.Type).
Set("CreateAt", session.CreateAt).
Set("UserId", session.UserId).
Set("ChannelId", session.ChannelId).
Set("Filename", session.Filename).
Set("Path", session.Path).
Set("FileSize", session.FileSize).
Set("FileOffset", session.FileOffset).
Set("RemoteId", session.RemoteId).
Set("ReqFileId", session.ReqFileId).
Where(sq.Eq{"Id": session.Id}).
ToSql()
if err != nil {
return errors.Wrap(err, "SqlUploadSessionStore.Update: failed to build query")
}
if _, err := us.GetMasterX().Exec(query, args...); err != nil {
if err == sql.ErrNoRows {
return store.NewErrNotFound("UploadSession", session.Id)
}
return errors.Wrapf(err, "SqlUploadSessionStore.Update: failed to update session with id=%s", session.Id)
}
return nil
}
func (us SqlUploadSessionStore) Get(ctx context.Context, id string) (*model.UploadSession, error) {
if !model.IsValidId(id) {
return nil, errors.New("SqlUploadSessionStore.Get: id is not valid")
}
query, args, err := us.getQueryBuilder().
Select("*").
From("UploadSessions").
Where(sq.Eq{"Id": id}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "SqlUploadSessionStore.Get: failed to build query")
}
var session model.UploadSession
if err := us.DBXFromContext(ctx).Get(&session, query, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("UploadSession", id)
}
return nil, errors.Wrapf(err, "SqlUploadSessionStore.Get: failed to select session with id=%s", id)
}
return &session, nil
}
func (us SqlUploadSessionStore) GetForUser(userId string) ([]*model.UploadSession, error) {
query, args, err := us.getQueryBuilder().
Select("*").
From("UploadSessions").
Where(sq.Eq{"UserId": userId}).
OrderBy("CreateAt ASC").
ToSql()
if err != nil {
return nil, errors.Wrap(err, "SqlUploadSessionStore.GetForUser: failed to build query")
}
sessions := []*model.UploadSession{}
if err := us.GetReplicaX().Select(&sessions, query, args...); err != nil {
return nil, errors.Wrap(err, "SqlUploadSessionStore.GetForUser: failed to select")
}
return sessions, nil
}
func (us SqlUploadSessionStore) Delete(id string) error {
if !model.IsValidId(id) {
return errors.New("SqlUploadSessionStore.Delete: id is not valid")
}
query, args, err := us.getQueryBuilder().
Delete("UploadSessions").
Where(sq.Eq{"Id": id}).
ToSql()
if err != nil {
return errors.Wrap(err, "SqlUploadSessionStore.Delete: failed to build query")
}
if _, err := us.GetMasterX().Exec(query, args...); err != nil {
return errors.Wrap(err, "SqlUploadSessionStore.Delete: failed to delete")
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlUserAccessTokenStore struct {
*SqlStore
}
func newSqlUserAccessTokenStore(sqlStore *SqlStore) store.UserAccessTokenStore {
return &SqlUserAccessTokenStore{sqlStore}
}
func (s SqlUserAccessTokenStore) Save(token *model.UserAccessToken) (*model.UserAccessToken, error) {
token.PreSave()
if err := token.IsValid(); err != nil {
return nil, err
}
query, args, err := s.getQueryBuilder().Insert("UserAccessTokens").
Columns("Id", "Token", "UserId", "Description", "IsActive").
Values(token.Id, token.Token, token.UserId, token.Description, token.IsActive).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "UserAccessToken_tosql")
}
if _, err := s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrap(err, "failed to save UserAccessToken")
}
return token, nil
}
func (s SqlUserAccessTokenStore) Delete(tokenId string) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
if err := s.deleteSessionsAndTokensById(transaction, tokenId); err == nil {
if err := transaction.Commit(); err != nil {
// don't need to rollback here since the transaction is already closed
return errors.Wrap(err, "commit_transaction")
}
}
return nil
}
func (s SqlUserAccessTokenStore) deleteSessionsAndTokensById(transaction *sqlxTxWrapper, tokenId string) error {
query := ""
if s.DriverName() == model.DatabaseDriverPostgres {
query = "DELETE FROM Sessions s USING UserAccessTokens o WHERE o.Token = s.Token AND o.Id = ?"
} else if s.DriverName() == model.DatabaseDriverMysql {
query = "DELETE s.* FROM Sessions s INNER JOIN UserAccessTokens o ON o.Token = s.Token WHERE o.Id = ?"
}
if _, err := transaction.Exec(query, tokenId); err != nil {
return errors.Wrapf(err, "failed to delete Sessions with UserAccessToken id=%s", tokenId)
}
return s.deleteTokensById(transaction, tokenId)
}
func (s SqlUserAccessTokenStore) deleteTokensById(transaction *sqlxTxWrapper, tokenId string) error {
if _, err := transaction.Exec("DELETE FROM UserAccessTokens WHERE Id = ?", tokenId); err != nil {
return errors.Wrapf(err, "failed to delete UserAccessToken id=%s", tokenId)
}
return nil
}
func (s SqlUserAccessTokenStore) DeleteAllForUser(userId string) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
if err := s.deleteSessionsandTokensByUser(transaction, userId); err != nil {
return err
}
if err := transaction.Commit(); err != nil {
// don't need to rollback here since the transaction is already closed
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (s SqlUserAccessTokenStore) deleteSessionsandTokensByUser(transaction *sqlxTxWrapper, userId string) error {
query := ""
if s.DriverName() == model.DatabaseDriverPostgres {
query = "DELETE FROM Sessions s USING UserAccessTokens o WHERE o.Token = s.Token AND o.UserId = ?"
} else if s.DriverName() == model.DatabaseDriverMysql {
query = "DELETE s.* FROM Sessions s INNER JOIN UserAccessTokens o ON o.Token = s.Token WHERE o.UserId = ?"
}
if _, err := transaction.Exec(query, userId); err != nil {
return errors.Wrapf(err, "failed to delete Sessions with UserAccessToken userId=%s", userId)
}
return s.deleteTokensByUser(transaction, userId)
}
func (s SqlUserAccessTokenStore) deleteTokensByUser(transaction *sqlxTxWrapper, userId string) error {
if _, err := transaction.Exec("DELETE FROM UserAccessTokens WHERE UserId = ?", userId); err != nil {
return errors.Wrapf(err, "failed to delete UserAccessToken userId=%s", userId)
}
return nil
}
func (s SqlUserAccessTokenStore) Get(tokenId string) (*model.UserAccessToken, error) {
var token model.UserAccessToken
if err := s.GetReplicaX().Get(&token, "SELECT * FROM UserAccessTokens WHERE Id = ?", tokenId); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("UserAccessToken", tokenId)
}
return nil, errors.Wrapf(err, "failed to get UserAccessToken with id=%s", tokenId)
}
return &token, nil
}
func (s SqlUserAccessTokenStore) GetAll(offset, limit int) ([]*model.UserAccessToken, error) {
tokens := []*model.UserAccessToken{}
if err := s.GetReplicaX().Select(&tokens, "SELECT * FROM UserAccessTokens LIMIT ? OFFSET ?", limit, offset); err != nil {
return nil, errors.Wrap(err, "failed to find UserAccessTokens")
}
return tokens, nil
}
func (s SqlUserAccessTokenStore) GetByToken(tokenString string) (*model.UserAccessToken, error) {
var token model.UserAccessToken
if err := s.GetReplicaX().Get(&token, "SELECT * FROM UserAccessTokens WHERE Token = ?", tokenString); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("UserAccessToken", fmt.Sprintf("token=%s", tokenString))
}
return nil, errors.Wrapf(err, "failed to get UserAccessToken with token=%s", tokenString)
}
return &token, nil
}
func (s SqlUserAccessTokenStore) GetByUser(userId string, offset, limit int) ([]*model.UserAccessToken, error) {
tokens := []*model.UserAccessToken{}
if err := s.GetReplicaX().Select(&tokens, "SELECT * FROM UserAccessTokens WHERE UserId = ? LIMIT ? OFFSET ?", userId, limit, offset); err != nil {
return nil, errors.Wrapf(err, "failed to find UserAccessTokens with userId=%s", userId)
}
return tokens, nil
}
func (s SqlUserAccessTokenStore) Search(term string) ([]*model.UserAccessToken, error) {
term = sanitizeSearchTerm(term, "\\")
tokens := []*model.UserAccessToken{}
params := []any{term, term, term}
query := `
SELECT
uat.*
FROM UserAccessTokens uat
INNER JOIN Users u
ON uat.UserId = u.Id
WHERE uat.Id LIKE ? OR uat.UserId LIKE ? OR u.Username LIKE ?`
if err := s.GetReplicaX().Select(&tokens, query, params...); err != nil {
return nil, errors.Wrapf(err, "failed to find UserAccessTokens by term with value '%s'", term)
}
return tokens, nil
}
func (s SqlUserAccessTokenStore) UpdateTokenEnable(tokenId string) error {
if _, err := s.GetMasterX().Exec("UPDATE UserAccessTokens SET IsActive = TRUE WHERE Id = ?", tokenId); err != nil {
return errors.Wrapf(err, "failed to update UserAccessTokens with id=%s", tokenId)
}
return nil
}
func (s SqlUserAccessTokenStore) UpdateTokenDisable(tokenId string) (err error) {
transaction, err := s.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
if err := s.deleteSessionsAndDisableToken(transaction, tokenId); err != nil {
return err
}
if err := transaction.Commit(); err != nil {
// don't need to rollback here since the transaction is already closed
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (s SqlUserAccessTokenStore) deleteSessionsAndDisableToken(transaction *sqlxTxWrapper, tokenId string) error {
query := ""
if s.DriverName() == model.DatabaseDriverPostgres {
query = "DELETE FROM Sessions s USING UserAccessTokens o WHERE o.Token = s.Token AND o.Id = ?"
} else if s.DriverName() == model.DatabaseDriverMysql {
query = "DELETE s.* FROM Sessions s INNER JOIN UserAccessTokens o ON o.Token = s.Token WHERE o.Id = ?"
}
if _, err := transaction.Exec(query, tokenId); err != nil {
return errors.Wrapf(err, "failed to delete Sessions with UserAccessToken id=%s", tokenId)
}
return s.updateTokenDisable(transaction, tokenId)
}
func (s SqlUserAccessTokenStore) updateTokenDisable(transaction *sqlxTxWrapper, tokenId string) error {
if _, err := transaction.Exec("UPDATE UserAccessTokens SET IsActive = FALSE WHERE Id = ?", tokenId); err != nil {
return errors.Wrapf(err, "failed to update UserAccessToken with id=%s", tokenId)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"sort"
"strings"
"unicode/utf8"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
MaxGroupChannelsForProfiles = 50
)
var (
UserSearchTypeNamesNoFullName = []string{"Username", "Nickname"}
UserSearchTypeNames = []string{"Username", "FirstName", "LastName", "Nickname"}
UserSearchTypeAllNoFullName = []string{"Username", "Nickname", "Email"}
UserSearchTypeAll = []string{"Username", "FirstName", "LastName", "Nickname", "Email"}
)
type SqlUserStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
// usersQuery is a starting point for all queries that return one or more Users.
usersQuery sq.SelectBuilder
}
func (us *SqlUserStore) ClearCaches() {}
func (us SqlUserStore) InvalidateProfileCacheForUser(userId string) {}
func newSqlUserStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.UserStore {
us := &SqlUserStore{
SqlStore: sqlStore,
metrics: metrics,
}
// note: we are providing field names explicitly here to maintain order of columns (needed when using raw queries)
us.usersQuery = us.getQueryBuilder().
Select("u.Id", "u.CreateAt", "u.UpdateAt", "u.DeleteAt", "u.Username", "u.Password", "u.AuthData", "u.AuthService", "u.Email", "u.EmailVerified", "u.Nickname", "u.FirstName", "u.LastName", "u.Position", "u.Roles", "u.AllowMarketing", "u.Props", "u.NotifyProps", "u.LastPasswordUpdate", "u.LastPictureUpdate", "u.FailedAttempts", "u.Locale", "u.Timezone", "u.MfaActive", "u.MfaSecret",
"b.UserId IS NOT NULL AS IsBot", "COALESCE(b.Description, '') AS BotDescription", "COALESCE(b.LastIconUpdate, 0) AS BotLastIconUpdate", "u.RemoteId").
From("Users u").
LeftJoin("Bots b ON ( b.UserId = u.Id )")
return us
}
func (us SqlUserStore) validateAutoResponderMessageSize(notifyProps model.StringMap) error {
if notifyProps != nil {
maxPostSize := us.Post().GetMaxPostSize()
msg := notifyProps[model.AutoResponderMessageNotifyProp]
msgSize := utf8.RuneCountInString(msg)
if msgSize > maxPostSize {
mlog.Warn("auto_responder_message has size restrictions", mlog.Int("max_characters", maxPostSize), mlog.Int("received_size", msgSize))
return errors.New("Auto responder message size can't be more than the allowed Post size")
}
}
return nil
}
func (us SqlUserStore) insert(user *model.User) (sql.Result, error) {
if err := us.validateAutoResponderMessageSize(user.NotifyProps); err != nil {
return nil, err
}
query := `INSERT INTO Users
(Id, CreateAt, UpdateAt, DeleteAt, Username, Password, AuthData, AuthService,
Email, EmailVerified, Nickname, FirstName, LastName, Position, Roles, AllowMarketing,
Props, NotifyProps, LastPasswordUpdate, LastPictureUpdate, FailedAttempts,
Locale, Timezone, MfaActive, MfaSecret, RemoteId)
VALUES
(:Id, :CreateAt, :UpdateAt, :DeleteAt, :Username, :Password, :AuthData, :AuthService,
:Email, :EmailVerified, :Nickname, :FirstName, :LastName, :Position, :Roles, :AllowMarketing,
:Props, :NotifyProps, :LastPasswordUpdate, :LastPictureUpdate, :FailedAttempts,
:Locale, :Timezone, :MfaActive, :MfaSecret, :RemoteId)`
user.Props = wrapBinaryParamStringMap(us.IsBinaryParamEnabled(), user.Props)
return us.GetMasterX().NamedExec(query, user)
}
func (us SqlUserStore) InsertUsers(users []*model.User) error {
for _, user := range users {
_, err := us.insert(user)
if err != nil {
return err
}
}
return nil
}
func (us SqlUserStore) Save(user *model.User) (*model.User, error) {
if user.Id != "" && !user.IsRemote() {
return nil, store.NewErrInvalidInput("User", "id", user.Id)
}
user.PreSave()
if err := user.IsValid(); err != nil {
return nil, err
}
if _, err := us.insert(user); err != nil {
if IsUniqueConstraintError(err, []string{"Email", "users_email_key", "idx_users_email_unique"}) {
return nil, store.NewErrInvalidInput("User", "email", user.Email)
}
if IsUniqueConstraintError(err, []string{"Username", "users_username_key", "idx_users_username_unique"}) {
return nil, store.NewErrInvalidInput("User", "username", user.Username)
}
return nil, errors.Wrapf(err, "failed to save User with userId=%s", user.Id)
}
return user, nil
}
func (us SqlUserStore) DeactivateGuests() ([]string, error) {
curTime := model.GetMillis()
updateQuery := us.getQueryBuilder().Update("Users").
Set("UpdateAt", curTime).
Set("DeleteAt", curTime).
Where(sq.Eq{"Roles": "system_guest"}).
Where(sq.Eq{"DeleteAt": 0})
queryString, args, err := updateQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "deactivate_guests_tosql")
}
_, err = us.GetMasterX().Exec(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to update Users with roles=system_guest")
}
selectQuery := us.getQueryBuilder().Select("Id").From("Users").Where(sq.Eq{"DeleteAt": curTime})
queryString, args, err = selectQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "deactivate_guests_tosql")
}
userIds := []string{}
err = us.GetMasterX().Select(&userIds, queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
return userIds, nil
}
func (us SqlUserStore) Update(user *model.User, trustedUpdateData bool) (*model.UserUpdate, error) {
user.PreUpdate()
if err := user.IsValid(); err != nil {
return nil, err
}
if err := us.validateAutoResponderMessageSize(user.NotifyProps); err != nil {
return nil, err
}
oldUser := model.User{}
err := us.GetMasterX().Get(&oldUser, "SELECT * FROM Users WHERE Id=?", user.Id)
if err != nil {
return nil, errors.Wrapf(err, "failed to get User with userId=%s", user.Id)
}
if oldUser.Id == "" {
return nil, store.NewErrInvalidInput("User", "id", user.Id)
}
user.CreateAt = oldUser.CreateAt
user.AuthData = oldUser.AuthData
user.AuthService = oldUser.AuthService
user.Password = oldUser.Password
user.LastPasswordUpdate = oldUser.LastPasswordUpdate
user.LastPictureUpdate = oldUser.LastPictureUpdate
user.EmailVerified = oldUser.EmailVerified
user.FailedAttempts = oldUser.FailedAttempts
user.MfaSecret = oldUser.MfaSecret
user.MfaActive = oldUser.MfaActive
if !trustedUpdateData {
user.Roles = oldUser.Roles
user.DeleteAt = oldUser.DeleteAt
}
if user.IsOAuthUser() {
if !trustedUpdateData {
user.Email = oldUser.Email
}
} else if user.IsLDAPUser() && !trustedUpdateData {
if user.Username != oldUser.Username || user.Email != oldUser.Email {
return nil, store.NewErrInvalidInput("User", "id", user.Id)
}
} else if user.Email != oldUser.Email {
user.EmailVerified = false
}
if user.Username != oldUser.Username {
user.UpdateMentionKeysFromUsername(oldUser.Username)
}
query := `UPDATE Users
SET CreateAt=:CreateAt, UpdateAt=:UpdateAt, DeleteAt=:DeleteAt, Username=:Username, Password=:Password,
AuthData=:AuthData, AuthService=:AuthService,Email=:Email, EmailVerified=:EmailVerified,
Nickname=:Nickname, FirstName=:FirstName, LastName=:LastName, Position=:Position, Roles=:Roles,
AllowMarketing=:AllowMarketing, Props=:Props, NotifyProps=:NotifyProps,
LastPasswordUpdate=:LastPasswordUpdate, LastPictureUpdate=:LastPictureUpdate,
FailedAttempts=:FailedAttempts,Locale=:Locale, Timezone=:Timezone, MfaActive=:MfaActive,
MfaSecret=:MfaSecret, RemoteId=:RemoteId
WHERE Id=:Id`
user.Props = wrapBinaryParamStringMap(us.IsBinaryParamEnabled(), user.Props)
res, err := us.GetMasterX().NamedExec(query, user)
if err != nil {
if IsUniqueConstraintError(err, []string{"Email", "users_email_key", "idx_users_email_unique"}) {
return nil, store.NewErrConflict("Email", err, user.Email)
}
if IsUniqueConstraintError(err, []string{"Username", "users_username_key", "idx_users_username_unique"}) {
return nil, store.NewErrConflict("Username", err, user.Username)
}
return nil, errors.Wrapf(err, "failed to update User with userId=%s", user.Id)
}
count, err := res.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "failed to get rows_affected")
}
if count > 1 {
return nil, fmt.Errorf("multiple users were update: userId=%s, count=%d", user.Id, count)
}
user.Sanitize(map[string]bool{})
oldUser.Sanitize(map[string]bool{})
return &model.UserUpdate{New: user.DeepCopy(), Old: &oldUser}, nil
}
func (us SqlUserStore) UpdateNotifyProps(userID string, props map[string]string) error {
if err := us.validateAutoResponderMessageSize(props); err != nil {
return err
}
buf, err := json.Marshal(props)
if err != nil {
return errors.Wrap(err, "failed marshalling session props")
}
if us.IsBinaryParamEnabled() {
buf = AppendBinaryFlag(buf)
}
if _, err := us.GetMasterX().Exec(`UPDATE Users
SET NotifyProps = ?
WHERE Id = ?`, buf, userID); err != nil {
return errors.Wrapf(err, "failed to update User with userId=%s", userID)
}
return nil
}
func (us SqlUserStore) UpdateLastPictureUpdate(userId string) error {
curTime := model.GetMillis()
if _, err := us.GetMasterX().Exec("UPDATE Users SET LastPictureUpdate = ?, UpdateAt = ? WHERE Id = ?", curTime, curTime, userId); err != nil {
return errors.Wrapf(err, "failed to update User with userId=%s", userId)
}
return nil
}
func (us SqlUserStore) ResetLastPictureUpdate(userId string) error {
curTime := model.GetMillis()
if _, err := us.GetMasterX().Exec("UPDATE Users SET LastPictureUpdate = ?, UpdateAt = ? WHERE Id = ?", 0, curTime, userId); err != nil {
return errors.Wrapf(err, "failed to update User with userId=%s", userId)
}
return nil
}
func (us SqlUserStore) UpdateUpdateAt(userId string) (int64, error) {
curTime := model.GetMillis()
if _, err := us.GetMasterX().Exec("UPDATE Users SET UpdateAt = ? WHERE Id = ?", curTime, userId); err != nil {
return curTime, errors.Wrapf(err, "failed to update User with userId=%s", userId)
}
return curTime, nil
}
func (us SqlUserStore) UpdatePassword(userId, hashedPassword string) error {
updateAt := model.GetMillis()
if _, err := us.GetMasterX().Exec("UPDATE Users SET Password = ?, LastPasswordUpdate = ?, UpdateAt = ?, AuthData = NULL, AuthService = '', FailedAttempts = 0 WHERE Id = ?", hashedPassword, updateAt, updateAt, userId); err != nil {
return errors.Wrapf(err, "failed to update User with userId=%s", userId)
}
return nil
}
func (us SqlUserStore) UpdateFailedPasswordAttempts(userId string, attempts int) error {
if _, err := us.GetMasterX().Exec("UPDATE Users SET FailedAttempts = ? WHERE Id = ?", attempts, userId); err != nil {
return errors.Wrapf(err, "failed to update User with userId=%s", userId)
}
return nil
}
func (us SqlUserStore) UpdateAuthData(userId string, service string, authData *string, email string, resetMfa bool) (string, error) {
updateAt := model.GetMillis()
updateQuery := us.getQueryBuilder().Update("Users").
Set("Password", "").
Set("LastPasswordUpdate", updateAt).
Set("UpdateAt", updateAt).
Set("FailedAttempts", 0).
Set("AuthService", service).
Set("AuthData", authData).
Where(sq.Eq{"Id": userId})
if email != "" {
updateQuery = updateQuery.Set("Email", sq.Expr("lower(?)", email))
}
if resetMfa {
updateQuery = updateQuery.Set("MfaActive", false).
Set("MfaSecret", "")
}
queryString, args, err := updateQuery.ToSql()
if err != nil {
return "", errors.Wrap(err, "update_auth_data_tosql")
}
if _, err := us.GetMasterX().Exec(queryString, args...); err != nil {
if IsUniqueConstraintError(err, []string{"Email", "users_email_key", "idx_users_email_unique", "AuthData", "users_authdata_key"}) {
return "", store.NewErrInvalidInput("User", "id", userId)
}
return "", errors.Wrapf(err, "failed to update User with userId=%s", userId)
}
return userId, nil
}
// ResetAuthDataToEmailForUsers resets the AuthData of users whose AuthService
// is |service| to their Email. If userIDs is non-empty, only the users whose
// IDs are in userIDs will be affected. If dryRun is true, only the number
// of users who *would* be affected is returned; otherwise, the number of
// users who actually were affected is returned.
func (us SqlUserStore) ResetAuthDataToEmailForUsers(service string, userIDs []string, includeDeleted bool, dryRun bool) (int, error) {
whereEquals := sq.Eq{"AuthService": service}
if len(userIDs) > 0 {
whereEquals["Id"] = userIDs
}
if !includeDeleted {
whereEquals["DeleteAt"] = 0
}
if dryRun {
builder := us.getQueryBuilder().
Select("COUNT(*)").
From("Users").
Where(whereEquals)
query, args, err := builder.ToSql()
if err != nil {
return 0, errors.Wrap(err, "select_count_users_tosql")
}
var numAffected int
err = us.GetReplicaX().Get(&numAffected, query, args...)
return numAffected, err
}
builder := us.getQueryBuilder().
Update("Users").
Set("AuthData", sq.Expr("Email")).
Where(whereEquals)
query, args, err := builder.ToSql()
if err != nil {
return 0, errors.Wrap(err, "update_users_tosql")
}
result, err := us.GetMasterX().Exec(query, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to update users' AuthData")
}
numAffected, err := result.RowsAffected()
return int(numAffected), err
}
func (us SqlUserStore) UpdateMfaSecret(userId, secret string) error {
updateAt := model.GetMillis()
if _, err := us.GetMasterX().Exec("UPDATE Users SET MfaSecret = ?, UpdateAt = ? WHERE Id = ?", secret, updateAt, userId); err != nil {
return errors.Wrapf(err, "failed to update User with userId=%s", userId)
}
return nil
}
func (us SqlUserStore) UpdateMfaActive(userId string, active bool) error {
updateAt := model.GetMillis()
if _, err := us.GetMasterX().Exec("UPDATE Users SET MfaActive = ?, UpdateAt = ? WHERE Id = ?", active, updateAt, userId); err != nil {
return errors.Wrapf(err, "failed to update User with userId=%s", userId)
}
return nil
}
// GetMany returns a list of users for the provided list of ids
func (us SqlUserStore) GetMany(ctx context.Context, ids []string) ([]*model.User, error) {
query := us.usersQuery.Where(sq.Eq{"Id": ids})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "users_get_many_tosql")
}
users := []*model.User{}
if err := us.SqlStore.DBXFromContext(ctx).Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "users_get_many_select")
}
return users, nil
}
func (us SqlUserStore) Get(ctx context.Context, id string) (*model.User, error) {
query := us.usersQuery.Where("Id = ?", id)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "users_get_tosql")
}
row := us.SqlStore.DBXFromContext(ctx).QueryRow(queryString, args...)
var user model.User
var props, notifyProps, timezone []byte
err = row.Scan(&user.Id, &user.CreateAt, &user.UpdateAt, &user.DeleteAt, &user.Username,
&user.Password, &user.AuthData, &user.AuthService, &user.Email, &user.EmailVerified,
&user.Nickname, &user.FirstName, &user.LastName, &user.Position, &user.Roles,
&user.AllowMarketing, &props, ¬ifyProps, &user.LastPasswordUpdate, &user.LastPictureUpdate,
&user.FailedAttempts, &user.Locale, &timezone, &user.MfaActive, &user.MfaSecret,
&user.IsBot, &user.BotDescription, &user.BotLastIconUpdate, &user.RemoteId)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("User", id)
}
return nil, errors.Wrapf(err, "failed to get User with userId=%s", id)
}
if err = json.Unmarshal(props, &user.Props); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal user props")
}
if err = json.Unmarshal(notifyProps, &user.NotifyProps); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal user notify props")
}
if err = json.Unmarshal(timezone, &user.Timezone); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal user timezone")
}
return &user, nil
}
func (us SqlUserStore) GetAll() ([]*model.User, error) {
query := us.usersQuery.OrderBy("Username ASC")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_all_users_tosql")
}
data := []*model.User{}
if err := us.GetReplicaX().Select(&data, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
return data, nil
}
func (us SqlUserStore) GetAllAfter(limit int, afterId string) ([]*model.User, error) {
query := us.usersQuery.
Where("Id > ?", afterId).
OrderBy("Id ASC").
Limit(uint64(limit))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_all_after_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
return users, nil
}
func (us SqlUserStore) GetEtagForAllProfiles() string {
var updateAt int64
err := us.GetReplicaX().Get(&updateAt, "SELECT UpdateAt FROM Users ORDER BY UpdateAt DESC LIMIT 1")
if err != nil {
return fmt.Sprintf("%v.%v", model.CurrentVersion, model.GetMillis())
}
return fmt.Sprintf("%v.%v", model.CurrentVersion, updateAt)
}
func (us SqlUserStore) GetAllProfiles(options *model.UserGetOptions) ([]*model.User, error) {
isPostgreSQL := us.DriverName() == model.DatabaseDriverPostgres
query := us.usersQuery.
OrderBy("u.Username ASC").
Offset(uint64(options.Page * options.PerPage)).Limit(uint64(options.PerPage))
query = applyViewRestrictionsFilter(query, options.ViewRestrictions, true)
query = applyRoleFilter(query, options.Role, isPostgreSQL)
query = applyMultiRoleFilters(query, options.Roles, []string{}, []string{}, isPostgreSQL)
if options.Inactive {
query = query.Where("u.DeleteAt != 0")
} else if options.Active {
query = query.Where("u.DeleteAt = 0")
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_all_profiles_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to get User profiles")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func applyRoleFilter(query sq.SelectBuilder, role string, isPostgreSQL bool) sq.SelectBuilder {
if role == "" {
return query
}
if isPostgreSQL {
roleParam := fmt.Sprintf("%%%s%%", sanitizeSearchTerm(role, "\\"))
return query.Where("u.Roles LIKE LOWER(?)", roleParam)
}
roleParam := fmt.Sprintf("%%%s%%", sanitizeSearchTerm(role, "*"))
return query.Where("u.Roles LIKE ? ESCAPE '*'", roleParam)
}
func applyMultiRoleFilters(query sq.SelectBuilder, systemRoles []string, teamRoles []string, channelRoles []string, isPostgreSQL bool) sq.SelectBuilder {
sqOr := sq.Or{}
if len(systemRoles) > 0 && systemRoles[0] != "" {
for _, role := range systemRoles {
queryRole := wildcardSearchTerm(role)
switch role {
case model.SystemUserRoleId:
// If querying for a `system_user` ensure that the user is only a system_user.
sqOr = append(sqOr, sq.Eq{"u.Roles": role})
case model.SystemGuestRoleId, model.SystemAdminRoleId, model.SystemUserManagerRoleId, model.SystemReadOnlyAdminRoleId, model.SystemManagerRoleId:
// If querying for any other roles search using a wildcard.
if isPostgreSQL {
sqOr = append(sqOr, sq.ILike{"u.Roles": queryRole})
} else {
sqOr = append(sqOr, sq.Like{"u.Roles": queryRole})
}
}
}
}
if len(channelRoles) > 0 && channelRoles[0] != "" {
for _, channelRole := range channelRoles {
switch channelRole {
case model.ChannelAdminRoleId:
if isPostgreSQL {
sqOr = append(sqOr, sq.And{sq.Eq{"cm.SchemeAdmin": true}, sq.NotILike{"u.Roles": wildcardSearchTerm(model.SystemAdminRoleId)}})
} else {
sqOr = append(sqOr, sq.And{sq.Eq{"cm.SchemeAdmin": true}, sq.NotLike{"u.Roles": wildcardSearchTerm(model.SystemAdminRoleId)}})
}
case model.ChannelUserRoleId:
if isPostgreSQL {
sqOr = append(sqOr, sq.And{sq.Eq{"cm.SchemeUser": true}, sq.Eq{"cm.SchemeAdmin": false}, sq.NotILike{"u.Roles": wildcardSearchTerm(model.SystemAdminRoleId)}})
} else {
sqOr = append(sqOr, sq.And{sq.Eq{"cm.SchemeUser": true}, sq.Eq{"cm.SchemeAdmin": false}, sq.NotLike{"u.Roles": wildcardSearchTerm(model.SystemAdminRoleId)}})
}
case model.ChannelGuestRoleId:
sqOr = append(sqOr, sq.Eq{"cm.SchemeGuest": true})
}
}
}
if len(teamRoles) > 0 && teamRoles[0] != "" {
for _, teamRole := range teamRoles {
switch teamRole {
case model.TeamAdminRoleId:
if isPostgreSQL {
sqOr = append(sqOr, sq.And{sq.Eq{"tm.SchemeAdmin": true}, sq.NotILike{"u.Roles": wildcardSearchTerm(model.SystemAdminRoleId)}})
} else {
sqOr = append(sqOr, sq.And{sq.Eq{"tm.SchemeAdmin": true}, sq.NotLike{"u.Roles": wildcardSearchTerm(model.SystemAdminRoleId)}})
}
case model.TeamUserRoleId:
if isPostgreSQL {
sqOr = append(sqOr, sq.And{sq.Eq{"tm.SchemeUser": true}, sq.Eq{"tm.SchemeAdmin": false}, sq.NotILike{"u.Roles": wildcardSearchTerm(model.SystemAdminRoleId)}})
} else {
sqOr = append(sqOr, sq.And{sq.Eq{"tm.SchemeUser": true}, sq.Eq{"tm.SchemeAdmin": false}, sq.NotLike{"u.Roles": wildcardSearchTerm(model.SystemAdminRoleId)}})
}
case model.TeamGuestRoleId:
sqOr = append(sqOr, sq.Eq{"tm.SchemeGuest": true})
}
}
}
if len(sqOr) > 0 {
return query.Where(sqOr)
}
return query
}
func applyChannelGroupConstrainedFilter(query sq.SelectBuilder, channelId string) sq.SelectBuilder {
if channelId == "" {
return query
}
return query.
Where(`u.Id IN (
SELECT
GroupMembers.UserId
FROM
Channels
JOIN GroupChannels ON GroupChannels.ChannelId = Channels.Id
JOIN UserGroups ON UserGroups.Id = GroupChannels.GroupId
JOIN GroupMembers ON GroupMembers.GroupId = UserGroups.Id
WHERE
Channels.Id = ?
AND GroupChannels.DeleteAt = 0
AND UserGroups.DeleteAt = 0
AND GroupMembers.DeleteAt = 0
GROUP BY
GroupMembers.UserId
)`, channelId)
}
func applyTeamGroupConstrainedFilter(query sq.SelectBuilder, teamId string) sq.SelectBuilder {
if teamId == "" {
return query
}
return query.
Where(`u.Id IN (
SELECT
GroupMembers.UserId
FROM
Teams
JOIN GroupTeams ON GroupTeams.TeamId = Teams.Id
JOIN UserGroups ON UserGroups.Id = GroupTeams.GroupId
JOIN GroupMembers ON GroupMembers.GroupId = UserGroups.Id
WHERE
Teams.Id = ?
AND GroupTeams.DeleteAt = 0
AND UserGroups.DeleteAt = 0
AND GroupMembers.DeleteAt = 0
GROUP BY
GroupMembers.UserId
)`, teamId)
}
func (us SqlUserStore) GetEtagForProfiles(teamId string) string {
var updateAt int64
err := us.GetReplicaX().Get(&updateAt, "SELECT UpdateAt FROM Users, TeamMembers WHERE TeamMembers.TeamId = ? AND Users.Id = TeamMembers.UserId ORDER BY UpdateAt DESC LIMIT 1", teamId)
if err != nil {
return fmt.Sprintf("%v.%v", model.CurrentVersion, model.GetMillis())
}
return fmt.Sprintf("%v.%v", model.CurrentVersion, updateAt)
}
func (us SqlUserStore) GetProfiles(options *model.UserGetOptions) ([]*model.User, error) {
isPostgreSQL := us.DriverName() == model.DatabaseDriverPostgres
query := us.usersQuery.
Join("TeamMembers tm ON ( tm.UserId = u.Id AND tm.DeleteAt = 0 )").
Where("tm.TeamId = ?", options.InTeamId).
OrderBy("u.Username ASC").
Offset(uint64(options.Page * options.PerPage)).Limit(uint64(options.PerPage))
query = applyViewRestrictionsFilter(query, options.ViewRestrictions, true)
query = applyRoleFilter(query, options.Role, isPostgreSQL)
query = applyMultiRoleFilters(query, options.Roles, options.TeamRoles, options.ChannelRoles, isPostgreSQL)
if options.Inactive {
query = query.Where("u.DeleteAt != 0")
} else if options.Active {
query = query.Where("u.DeleteAt = 0")
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_etag_for_profiles_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func (us SqlUserStore) InvalidateProfilesInChannelCacheByUser(userId string) {}
func (us SqlUserStore) InvalidateProfilesInChannelCache(channelId string) {}
func (us SqlUserStore) GetProfilesInChannel(options *model.UserGetOptions) ([]*model.User, error) {
query := us.usersQuery.
Join("ChannelMembers cm ON ( cm.UserId = u.Id )").
Where("cm.ChannelId = ?", options.InChannelId).
OrderBy("u.Username ASC").
Offset(uint64(options.Page * options.PerPage)).Limit(uint64(options.PerPage))
if options.Inactive {
query = query.Where("u.DeleteAt != 0")
} else if options.Active {
query = query.Where("u.DeleteAt = 0")
}
query = applyMultiRoleFilters(query, options.Roles, options.TeamRoles, options.ChannelRoles, us.DriverName() == model.DatabaseDriverPostgres)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_profiles_in_channel_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func (us SqlUserStore) GetProfilesInChannelByStatus(options *model.UserGetOptions) ([]*model.User, error) {
query := us.usersQuery.
Join("ChannelMembers cm ON ( cm.UserId = u.Id )").
LeftJoin("Status s ON ( s.UserId = u.Id )").
Where("cm.ChannelId = ?", options.InChannelId).
OrderBy(`
CASE s.Status
WHEN 'online' THEN 1
WHEN 'away' THEN 2
WHEN 'dnd' THEN 3
ELSE 4
END
`).
OrderBy("u.Username ASC").
Offset(uint64(options.Page * options.PerPage)).Limit(uint64(options.PerPage))
if options.Inactive && !options.Active {
query = query.Where("u.DeleteAt != 0")
} else if options.Active && !options.Inactive {
query = query.Where("u.DeleteAt = 0")
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_profiles_in_channel_by_status_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func (us SqlUserStore) GetProfilesInChannelByAdmin(options *model.UserGetOptions) ([]*model.User, error) {
query := us.usersQuery.
Join("ChannelMembers cm ON ( cm.UserId = u.Id )").
Where("cm.ChannelId = ?", options.InChannelId).
OrderBy(`cm.SchemeAdmin DESC`).
OrderBy("u.Username ASC").
Offset(uint64(options.Page * options.PerPage)).Limit(uint64(options.PerPage))
if options.Inactive && !options.Active {
query = query.Where("u.DeleteAt != 0")
} else if options.Active && !options.Inactive {
query = query.Where("u.DeleteAt = 0")
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_profiles_in_channel_by_admin_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func (us SqlUserStore) GetAllProfilesInChannel(ctx context.Context, channelID string, allowFromCache bool) (map[string]*model.User, error) {
query := us.usersQuery.
Join("ChannelMembers cm ON ( cm.UserId = u.Id )").
Where("cm.ChannelId = ?", channelID).
Where("u.DeleteAt = 0").
OrderBy("u.Username ASC")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_all_profiles_in_channel_tosql")
}
users := []*model.User{}
rows, err := us.SqlStore.DBXFromContext(ctx).Query(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
defer rows.Close()
for rows.Next() {
var user model.User
var props, notifyProps, timezone []byte
if err = rows.Scan(&user.Id, &user.CreateAt, &user.UpdateAt, &user.DeleteAt, &user.Username, &user.Password, &user.AuthData, &user.AuthService, &user.Email, &user.EmailVerified, &user.Nickname, &user.FirstName, &user.LastName, &user.Position, &user.Roles, &user.AllowMarketing, &props, ¬ifyProps, &user.LastPasswordUpdate, &user.LastPictureUpdate, &user.FailedAttempts, &user.Locale, &timezone, &user.MfaActive, &user.MfaSecret, &user.IsBot, &user.BotDescription, &user.BotLastIconUpdate, &user.RemoteId); err != nil {
return nil, errors.Wrap(err, "failed to scan values from rows into User entity")
}
if err = json.Unmarshal(props, &user.Props); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal user props")
}
if err = json.Unmarshal(notifyProps, &user.NotifyProps); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal user notify props")
}
if err = json.Unmarshal(timezone, &user.Timezone); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal user timezone")
}
users = append(users, &user)
}
err = rows.Err()
if err != nil {
return nil, errors.Wrap(err, "error while iterating over rows")
}
userMap := make(map[string]*model.User)
for _, u := range users {
u.Sanitize(map[string]bool{})
userMap[u.Id] = u
}
return userMap, nil
}
func (us SqlUserStore) GetProfilesNotInChannel(teamId string, channelId string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
query := us.usersQuery.
Join("TeamMembers tm ON ( tm.UserId = u.Id AND tm.DeleteAt = 0 AND tm.TeamId = ? )", teamId).
LeftJoin("ChannelMembers cm ON ( cm.UserId = u.Id AND cm.ChannelId = ? )", channelId).
Where("cm.UserId IS NULL").
OrderBy("u.Username ASC").
Offset(uint64(offset)).Limit(uint64(limit))
query = applyViewRestrictionsFilter(query, viewRestrictions, true)
if groupConstrained {
query = applyChannelGroupConstrainedFilter(query, channelId)
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_profiles_not_in_channel_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func (us SqlUserStore) GetProfilesWithoutTeam(options *model.UserGetOptions) ([]*model.User, error) {
isPostgreSQL := us.DriverName() == model.DatabaseDriverPostgres
query := us.usersQuery.
Where(`(
SELECT
COUNT(0)
FROM
TeamMembers
WHERE
TeamMembers.UserId = u.Id
AND TeamMembers.DeleteAt = 0
) = 0`).
OrderBy("u.Username ASC").
Offset(uint64(options.Page * options.PerPage)).Limit(uint64(options.PerPage))
query = applyViewRestrictionsFilter(query, options.ViewRestrictions, true)
query = applyRoleFilter(query, options.Role, isPostgreSQL)
if options.Inactive {
query = query.Where("u.DeleteAt != 0")
} else if options.Active {
query = query.Where("u.DeleteAt = 0")
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_profiles_without_team_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func (us SqlUserStore) GetProfilesByUsernames(usernames []string, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
query := us.usersQuery
query = applyViewRestrictionsFilter(query, viewRestrictions, true)
query = query.
Where(map[string]any{
"Username": usernames,
}).
OrderBy("u.Username ASC")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_profiles_by_usernames")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
return users, nil
}
type UserWithLastActivityAt struct {
model.User
LastActivityAt int64
}
func (us SqlUserStore) GetRecentlyActiveUsersForTeam(teamId string, offset, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
query := us.usersQuery.
Column("s.LastActivityAt").
Join("TeamMembers tm ON (tm.UserId = u.Id AND tm.TeamId = ?)", teamId).
Join("Status s ON (s.UserId = u.Id)").
OrderBy("s.LastActivityAt DESC").
OrderBy("u.Username ASC").
Offset(uint64(offset)).Limit(uint64(limit))
query = applyViewRestrictionsFilter(query, viewRestrictions, true)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_recently_active_users_for_team_tosql")
}
users := []*UserWithLastActivityAt{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
userList := []*model.User{}
for _, userWithLastActivityAt := range users {
u := userWithLastActivityAt.User
u.Sanitize(map[string]bool{})
u.LastActivityAt = userWithLastActivityAt.LastActivityAt
userList = append(userList, &u)
}
return userList, nil
}
func (us SqlUserStore) GetNewUsersForTeam(teamId string, offset, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
query := us.usersQuery.
Join("TeamMembers tm ON (tm.UserId = u.Id AND tm.TeamId = ?)", teamId).
OrderBy("u.CreateAt DESC").
OrderBy("u.Username ASC").
Offset(uint64(offset)).Limit(uint64(limit))
query = applyViewRestrictionsFilter(query, viewRestrictions, true)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_new_users_for_team_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func (us SqlUserStore) GetProfileByIds(ctx context.Context, userIds []string, options *store.UserGetByIdsOpts, allowFromCache bool) ([]*model.User, error) {
if options == nil {
options = &store.UserGetByIdsOpts{}
}
users := []*model.User{}
query := us.usersQuery.
Where(map[string]any{
"u.Id": userIds,
}).
OrderBy("u.Username ASC")
if options.Since > 0 {
query = query.Where(sq.Gt(map[string]any{
"u.UpdateAt": options.Since,
}))
}
query = applyViewRestrictionsFilter(query, options.ViewRestrictions, true)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_profile_by_ids_tosql")
}
if err := us.SqlStore.DBXFromContext(ctx).Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
return users, nil
}
type UserWithChannel struct {
model.User
ChannelId string
}
func (us SqlUserStore) GetProfileByGroupChannelIdsForUser(userId string, channelIds []string) (map[string][]*model.User, error) {
if len(channelIds) > MaxGroupChannelsForProfiles {
channelIds = channelIds[0:MaxGroupChannelsForProfiles]
}
isMemberQuery := fmt.Sprintf(`
EXISTS(
SELECT
1
FROM
ChannelMembers
WHERE
UserId = '%s'
AND
ChannelId = cm.ChannelId
)`, userId)
query := us.getQueryBuilder().
Select("u.*, cm.ChannelId").
From("Users u").
Join("ChannelMembers cm ON u.Id = cm.UserId").
Join("Channels c ON cm.ChannelId = c.Id").
Where(sq.Eq{"c.Type": model.ChannelTypeGroup, "cm.ChannelId": channelIds}).
Where(isMemberQuery).
Where(sq.NotEq{"u.Id": userId}).
OrderBy("u.Username ASC")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_profiles_by_group_channel_ids_for_user_tosql")
}
usersWithChannel := []*UserWithChannel{}
if err := us.GetReplicaX().Select(&usersWithChannel, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
usersByChannelId := map[string][]*model.User{}
for _, user := range usersWithChannel {
if val, ok := usersByChannelId[user.ChannelId]; ok {
usersByChannelId[user.ChannelId] = append(val, &user.User)
} else {
usersByChannelId[user.ChannelId] = []*model.User{&user.User}
}
}
return usersByChannelId, nil
}
func (us SqlUserStore) GetSystemAdminProfiles() (map[string]*model.User, error) {
query := us.usersQuery.
Where("Roles LIKE ?", "%system_admin%").
OrderBy("u.Username ASC")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_system_admin_profiles_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
userMap := make(map[string]*model.User)
for _, u := range users {
u.Sanitize(map[string]bool{})
userMap[u.Id] = u
}
return userMap, nil
}
func (us SqlUserStore) GetByEmail(email string) (*model.User, error) {
query := us.usersQuery.Where("Email = lower(?)", email)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_by_email_tosql")
}
user := model.User{}
if err := us.GetReplicaX().Get(&user, queryString, args...); err != nil {
if err == sql.ErrNoRows {
return nil, errors.Wrap(store.NewErrNotFound("User", fmt.Sprintf("email=%s", email)), "failed to find User")
}
return nil, errors.Wrapf(err, "failed to get User with email=%s", email)
}
return &user, nil
}
func (us SqlUserStore) GetByAuth(authData *string, authService string) (*model.User, error) {
if authData == nil || *authData == "" {
return nil, store.NewErrInvalidInput("User", "<authData>", "empty or nil")
}
query := us.usersQuery.
Where("u.AuthData = ?", authData).
Where("u.AuthService = ?", authService)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_by_auth_tosql")
}
user := model.User{}
if err := us.GetReplicaX().Get(&user, queryString, args...); err == sql.ErrNoRows {
return nil, store.NewErrNotFound("User", fmt.Sprintf("authData=%s, authService=%s", *authData, authService))
} else if err != nil {
return nil, errors.Wrapf(err, "failed to find User with authData=%s and authService=%s", *authData, authService)
}
return &user, nil
}
func (us SqlUserStore) GetAllUsingAuthService(authService string) ([]*model.User, error) {
query := us.usersQuery.
Where("u.AuthService = ?", authService).
OrderBy("u.Username ASC")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_all_using_auth_service_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Users with authService=%s", authService)
}
return users, nil
}
func (us SqlUserStore) GetAllNotInAuthService(authServices []string) ([]*model.User, error) {
query := us.usersQuery.
Where(sq.NotEq{"u.AuthService": authServices}).
OrderBy("u.Username ASC")
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_all_not_in_auth_service_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Users with authServices in %v", authServices)
}
return users, nil
}
func (us SqlUserStore) GetByUsername(username string) (*model.User, error) {
query := us.usersQuery.Where("u.Username = lower(?)", username)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_by_username_tosql")
}
user := model.User{}
if err := us.GetReplicaX().Get(&user, queryString, args...); err != nil {
if err == sql.ErrNoRows {
return nil, errors.Wrap(store.NewErrNotFound("User", fmt.Sprintf("username=%s", username)), "failed to find User")
}
return nil, errors.Wrapf(err, "failed to find User with username=%s", username)
}
return &user, nil
}
func (us SqlUserStore) GetForLogin(loginId string, allowSignInWithUsername, allowSignInWithEmail bool) (*model.User, error) {
query := us.usersQuery
if allowSignInWithUsername && allowSignInWithEmail {
query = query.Where("Username = lower(?) OR Email = lower(?)", loginId, loginId)
} else if allowSignInWithUsername {
query = query.Where("Username = lower(?)", loginId)
} else if allowSignInWithEmail {
query = query.Where("Email = lower(?)", loginId)
} else {
return nil, errors.New("sign in with username and email are disabled")
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_for_login_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
if len(users) == 0 {
return nil, errors.New("user not found")
}
if len(users) > 1 {
return nil, errors.New("multiple users found")
}
return users[0], nil
}
func (us SqlUserStore) VerifyEmail(userId, email string) (string, error) {
curTime := model.GetMillis()
if _, err := us.GetMasterX().Exec("UPDATE Users SET Email = lower(?), EmailVerified = true, UpdateAt = ? WHERE Id = ?", email, curTime, userId); err != nil {
return "", errors.Wrapf(err, "failed to update Users with userId=%s and email=%s", userId, email)
}
return userId, nil
}
func (us SqlUserStore) PermanentDelete(userId string) error {
if _, err := us.GetMasterX().Exec("DELETE FROM Users WHERE Id = ?", userId); err != nil {
return errors.Wrapf(err, "failed to delete User with userId=%s", userId)
}
return nil
}
func (us SqlUserStore) Count(options model.UserCountOptions) (int64, error) {
isPostgreSQL := us.DriverName() == model.DatabaseDriverPostgres
query := us.getQueryBuilder().Select("COUNT(DISTINCT u.Id)").From("Users AS u")
if !options.IncludeDeleted {
query = query.Where("u.DeleteAt = 0")
}
if options.IncludeBotAccounts {
if options.ExcludeRegularUsers {
query = query.Join("Bots ON u.Id = Bots.UserId")
}
} else {
query = query.LeftJoin("Bots ON u.Id = Bots.UserId").Where("Bots.UserId IS NULL")
if options.ExcludeRegularUsers {
// Currently this doesn't make sense because it will always return 0
return int64(0), errors.New("query with IncludeBotAccounts=false and excludeRegularUsers=true always return 0")
}
}
if options.TeamId != "" {
query = query.LeftJoin("TeamMembers AS tm ON u.Id = tm.UserId").Where("tm.TeamId = ? AND tm.DeleteAt = 0", options.TeamId)
} else if options.ChannelId != "" {
query = query.LeftJoin("ChannelMembers AS cm ON u.Id = cm.UserId").Where("cm.ChannelId = ?", options.ChannelId)
}
query = applyViewRestrictionsFilter(query, options.ViewRestrictions, false)
query = applyMultiRoleFilters(query, options.Roles, options.TeamRoles, options.ChannelRoles, isPostgreSQL)
if isPostgreSQL {
query = query.PlaceholderFormat(sq.Dollar)
}
queryString, args, err := query.ToSql()
if err != nil {
return int64(0), errors.Wrap(err, "count_tosql")
}
var count int64
err = us.GetReplicaX().Get(&count, queryString, args...)
if err != nil {
return int64(0), errors.Wrap(err, "failed to count Users")
}
return count, nil
}
func (us SqlUserStore) AnalyticsActiveCount(timePeriod int64, options model.UserCountOptions) (int64, error) {
time := model.GetMillis() - timePeriod
query := us.getQueryBuilder().Select("COUNT(*)").From("Status AS s").Where("LastActivityAt > ?", time)
if !options.IncludeBotAccounts {
query = query.LeftJoin("Bots ON s.UserId = Bots.UserId").Where("Bots.UserId IS NULL")
}
if !options.IncludeDeleted {
query = query.LeftJoin("Users ON s.UserId = Users.Id").Where("Users.DeleteAt = 0")
}
queryStr, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "analytics_active_count_tosql")
}
var v int64
err = us.GetReplicaX().Get(&v, queryStr, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to count Users")
}
return v, nil
}
func (us SqlUserStore) AnalyticsActiveCountForPeriod(startTime int64, endTime int64, options model.UserCountOptions) (int64, error) {
query := us.getQueryBuilder().Select("COUNT(*)").From("Status AS s").Where("LastActivityAt > ? AND LastActivityAt <= ?", startTime, endTime)
if !options.IncludeBotAccounts {
query = query.LeftJoin("Bots ON s.UserId = Bots.UserId").Where("Bots.UserId IS NULL")
}
if !options.IncludeDeleted {
query = query.LeftJoin("Users ON s.UserId = Users.Id").Where("Users.DeleteAt = 0")
}
queryStr, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "Failed to build query.")
}
var v int64
err = us.GetReplicaX().Get(&v, queryStr, args...)
if err != nil {
return 0, errors.Wrap(err, "Unable to get the active users during the requested period.")
}
return v, nil
}
func (us SqlUserStore) GetUnreadCount(userId string, isCRTEnabled bool) (int64, error) {
var mentionCountColumn = "cm.MentionCount"
if isCRTEnabled {
mentionCountColumn = "cm.MentionCountRoot"
}
query := `
SELECT SUM(` + mentionCountColumn + `)
FROM Channels c
INNER JOIN ChannelMembers cm
ON cm.ChannelId = c.Id
AND cm.UserId = ?
AND c.DeleteAt = 0
`
var count int64
err := us.GetReplicaX().Get(&count, query, userId)
if err != nil {
return count, errors.Wrapf(err, "failed to count unread Channels for userId=%s", userId)
}
return count, nil
}
func (us SqlUserStore) GetUnreadCountForChannel(userId string, channelId string) (int64, error) {
var count int64
err := us.GetReplicaX().Get(&count, "SELECT SUM(CASE WHEN c.Type = ? THEN (c.TotalMsgCount - cm.MsgCount) ELSE cm.MentionCount END) FROM Channels c INNER JOIN ChannelMembers cm ON c.Id = cm.ChannelId AND cm.ChannelId = ? AND cm.UserId = ?", model.ChannelTypeDirect, channelId, userId)
if err != nil {
return 0, errors.Wrapf(err, "failed to get unread count for channelId=%s and userId=%s", channelId, userId)
}
return count, nil
}
func (us SqlUserStore) GetAnyUnreadPostCountForChannel(userId string, channelId string) (int64, error) {
var count int64
err := us.GetReplicaX().Get(&count, "SELECT SUM(c.TotalMsgCount - cm.MsgCount) FROM Channels c INNER JOIN ChannelMembers cm ON c.Id = cm.ChannelId AND cm.ChannelId = ? AND cm.UserId = ?", channelId, userId)
if err != nil {
return count, errors.Wrapf(err, "failed to get any unread count for channelId=%s and userId=%s", channelId, userId)
}
return count, nil
}
func (us SqlUserStore) Search(teamId string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
query := us.usersQuery.
OrderBy("Username ASC").
Limit(uint64(options.Limit))
if teamId != "" {
query = query.Join("TeamMembers tm ON ( tm.UserId = u.Id AND tm.DeleteAt = 0 AND tm.TeamId = ? )", teamId)
}
return us.performSearch(query, term, options)
}
func (us SqlUserStore) SearchWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, error) {
query := us.usersQuery.
Where(`(
SELECT
COUNT(0)
FROM
TeamMembers
WHERE
TeamMembers.UserId = u.Id
AND TeamMembers.DeleteAt = 0
) = 0`).
OrderBy("u.Username ASC").
Limit(uint64(options.Limit))
return us.performSearch(query, term, options)
}
func (us SqlUserStore) SearchNotInTeam(notInTeamId string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
query := us.usersQuery.
LeftJoin("TeamMembers tm ON ( tm.UserId = u.Id AND tm.DeleteAt = 0 AND tm.TeamId = ? )", notInTeamId).
Where("tm.UserId IS NULL").
OrderBy("u.Username ASC").
Limit(uint64(options.Limit))
if options.GroupConstrained {
query = applyTeamGroupConstrainedFilter(query, notInTeamId)
}
return us.performSearch(query, term, options)
}
func (us SqlUserStore) SearchNotInChannel(teamId string, channelId string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
query := us.usersQuery.
LeftJoin("ChannelMembers cm ON ( cm.UserId = u.Id AND cm.ChannelId = ? )", channelId).
Where("cm.UserId IS NULL").
OrderBy("Username ASC").
Limit(uint64(options.Limit))
if teamId != "" {
query = query.Join("TeamMembers tm ON ( tm.UserId = u.Id AND tm.DeleteAt = 0 AND tm.TeamId = ? )", teamId)
}
if options.GroupConstrained {
query = applyChannelGroupConstrainedFilter(query, channelId)
}
return us.performSearch(query, term, options)
}
func (us SqlUserStore) SearchInChannel(channelId string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
query := us.usersQuery.
Join("ChannelMembers cm ON ( cm.UserId = u.Id AND cm.ChannelId = ? )", channelId).
OrderBy("Username ASC").
Limit(uint64(options.Limit))
return us.performSearch(query, term, options)
}
func (us SqlUserStore) SearchInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
query := us.usersQuery.
Join("GroupMembers gm ON ( gm.UserId = u.Id AND gm.GroupId = ? AND gm.DeleteAt = 0 )", groupID).
OrderBy("Username ASC").
Limit(uint64(options.Limit))
return us.performSearch(query, term, options)
}
func (us SqlUserStore) SearchNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
query := us.usersQuery.
LeftJoin("GroupMembers gm ON ( gm.UserId = u.Id AND gm.GroupId = ? )", groupID).
Where("(gm.UserId IS NULL OR gm.deleteat != 0)").
OrderBy("Username ASC").
Limit(uint64(options.Limit))
return us.performSearch(query, term, options)
}
func generateSearchQuery(query sq.SelectBuilder, terms []string, fields []string, isPostgreSQL bool) sq.SelectBuilder {
for _, term := range terms {
searchFields := []string{}
termArgs := []any{}
for _, field := range fields {
if isPostgreSQL {
searchFields = append(searchFields, fmt.Sprintf("lower(%s) LIKE lower(?) escape '*' ", field))
} else {
searchFields = append(searchFields, fmt.Sprintf("%s LIKE ? escape '*' ", field))
}
termArgs = append(termArgs, fmt.Sprintf("%s%%", strings.TrimLeft(term, "@")))
}
query = query.Where(fmt.Sprintf("(%s)", strings.Join(searchFields, " OR ")), termArgs...)
}
return query
}
func (us SqlUserStore) performSearch(query sq.SelectBuilder, term string, options *model.UserSearchOptions) ([]*model.User, error) {
term = sanitizeSearchTerm(term, "*")
var searchType []string
if options.AllowEmails {
if options.AllowFullNames {
searchType = UserSearchTypeAll
} else {
searchType = UserSearchTypeAllNoFullName
}
} else {
if options.AllowFullNames {
searchType = UserSearchTypeNames
} else {
searchType = UserSearchTypeNamesNoFullName
}
}
isPostgreSQL := us.DriverName() == model.DatabaseDriverPostgres
query = applyRoleFilter(query, options.Role, isPostgreSQL)
query = applyMultiRoleFilters(query, options.Roles, options.TeamRoles, options.ChannelRoles, isPostgreSQL)
if !options.AllowInactive {
query = query.Where("u.DeleteAt = 0")
}
if strings.TrimSpace(term) != "" {
query = generateSearchQuery(query, strings.Fields(term), searchType, isPostgreSQL)
}
query = applyViewRestrictionsFilter(query, options.ViewRestrictions, true)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "perform_search_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find Users with term=%s and searchType=%v", term, searchType)
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func (us SqlUserStore) AnalyticsGetInactiveUsersCount() (int64, error) {
var count int64
err := us.GetReplicaX().Get(&count, "SELECT COUNT(Id) FROM Users WHERE DeleteAt > 0")
if err != nil {
return int64(0), errors.Wrap(err, "failed to count inactive Users")
}
return count, nil
}
func (us SqlUserStore) AnalyticsGetExternalUsers(hostDomain string) (bool, error) {
var count int64
err := us.GetReplicaX().Get(&count, "SELECT COUNT(Id) FROM Users WHERE LOWER(Email) NOT LIKE ?", "%@"+strings.ToLower(hostDomain))
if err != nil {
return false, errors.Wrap(err, "failed to count inactive Users")
}
return count > 0, nil
}
func (us SqlUserStore) AnalyticsGetGuestCount() (int64, error) {
var count int64
err := us.GetReplicaX().Get(&count, "SELECT count(*) FROM Users WHERE Roles LIKE ? and DeleteAt = 0", "%system_guest%")
if err != nil {
return int64(0), errors.Wrap(err, "failed to count guest Users")
}
return count, nil
}
func (us SqlUserStore) AnalyticsGetSystemAdminCount() (int64, error) {
var count int64
err := us.GetReplicaX().Get(&count, "SELECT count(*) FROM Users WHERE Roles LIKE ? and DeleteAt = 0", "%system_admin%")
if err != nil {
return int64(0), errors.Wrap(err, "failed to count system admin Users")
}
return count, nil
}
func (us SqlUserStore) GetProfilesNotInTeam(teamId string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
users := []*model.User{}
query := us.usersQuery.
LeftJoin("TeamMembers tm ON ( tm.UserId = u.Id AND tm.DeleteAt = 0 AND tm.TeamId = ? )", teamId).
Where("tm.UserId IS NULL").
OrderBy("u.Username ASC").
Offset(uint64(offset)).Limit(uint64(limit))
query = applyViewRestrictionsFilter(query, viewRestrictions, true)
if groupConstrained {
query = applyTeamGroupConstrainedFilter(query, teamId)
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_profiles_not_in_team_tosql")
}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func (us SqlUserStore) GetEtagForProfilesNotInTeam(teamId string) string {
querystr := `
SELECT
CONCAT(MAX(UpdateAt), '.', COUNT(Id)) as etag
FROM
Users as u
LEFT JOIN TeamMembers tm
ON tm.UserId = u.Id
AND tm.TeamId = ?
AND tm.DeleteAt = 0
WHERE
tm.UserId IS NULL
`
var etag string
err := us.GetReplicaX().Get(&etag, querystr, teamId)
if err != nil {
return fmt.Sprintf("%v.%v", model.CurrentVersion, model.GetMillis())
}
return fmt.Sprintf("%v.%v", model.CurrentVersion, etag)
}
func (us SqlUserStore) ClearAllCustomRoleAssignments() (err error) {
builtInRoles := model.MakeDefaultRoles()
lastUserId := strings.Repeat("0", 26)
for {
var transaction *sqlxTxWrapper
var err error
if transaction, err = us.GetMasterX().Beginx(); err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
users := []*model.User{}
if err := transaction.Select(&users, "SELECT * from Users WHERE Id > ? ORDER BY Id LIMIT 1000", lastUserId); err != nil {
return errors.Wrapf(err, "failed to find Users with id > %s", lastUserId)
}
if len(users) == 0 {
break
}
for _, user := range users {
lastUserId = user.Id
var newRoles []string
for _, role := range strings.Fields(user.Roles) {
for name := range builtInRoles {
if name == role {
newRoles = append(newRoles, role)
break
}
}
}
newRolesString := strings.Join(newRoles, " ")
if newRolesString != user.Roles {
if _, err := transaction.Exec("UPDATE Users SET Roles = ? WHERE Id = ?", newRolesString, user.Id); err != nil {
return errors.Wrap(err, "failed to update Users")
}
}
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
}
return nil
}
func (us SqlUserStore) InferSystemInstallDate() (int64, error) {
var createAt int64
err := us.GetReplicaX().Get(&createAt, "SELECT CreateAt FROM Users WHERE CreateAt IS NOT NULL ORDER BY CreateAt ASC LIMIT 1")
if err != nil {
return 0, errors.Wrap(err, "failed to infer system install date")
}
return createAt, nil
}
func (us SqlUserStore) GetFirstSystemAdminID() (string, error) {
var id string
err := us.GetReplicaX().Get(&id, "SELECT Id FROM Users WHERE Roles LIKE ? ORDER BY CreateAt ASC LIMIT 1", "%system_admin%")
if err != nil {
return "", errors.Wrap(err, "failed to get first system admin")
}
return id, nil
}
func (us SqlUserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
users := []*model.User{}
usersQuery, args, err := us.usersQuery.
Where(sq.Or{
sq.Gt{"u.CreateAt": startTime},
sq.And{
sq.Eq{"u.CreateAt": startTime},
sq.Gt{"u.Id": startFileID},
},
}).
OrderBy("u.CreateAt ASC, u.Id ASC").
Limit(uint64(limit)).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetUsersBatchForIndexing_ToSql1")
}
err = us.GetSearchReplicaX().Select(&users, usersQuery, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
userIds := []string{}
for _, user := range users {
userIds = append(userIds, user.Id)
}
channelMembers := []*model.ChannelMember{}
channelMembersQuery, args, err := us.getQueryBuilder().
Select(`
cm.ChannelId,
cm.UserId,
cm.Roles,
cm.LastViewedAt,
cm.MsgCount,
cm.MentionCount,
cm.MentionCountRoot,
cm.NotifyProps,
cm.LastUpdateAt,
cm.SchemeUser,
cm.SchemeAdmin,
(cm.SchemeGuest IS NOT NULL AND cm.SchemeGuest) as SchemeGuest
`).
From("ChannelMembers cm").
Join("Channels c ON cm.ChannelId = c.Id").
Where(sq.And{
sq.Eq{
"cm.UserId": userIds,
},
sq.Or{
sq.Eq{"c.Type": model.ChannelTypeOpen},
sq.Eq{"c.Type": model.ChannelTypeDirect},
sq.Eq{"c.Type": model.ChannelTypeGroup},
},
}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetUsersBatchForIndexing_ToSql2")
}
err = us.GetSearchReplicaX().Select(&channelMembers, channelMembersQuery, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find ChannelMembers")
}
teamMembers := []*model.TeamMember{}
teamMembersQuery, args, err := us.getQueryBuilder().
Select("TeamId, UserId, Roles, DeleteAt, (SchemeGuest IS NOT NULL AND SchemeGuest) as SchemeGuest, SchemeUser, SchemeAdmin").
From("TeamMembers").
Where(sq.Eq{"UserId": userIds, "DeleteAt": 0}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetUsersBatchForIndexing_ToSql3")
}
err = us.GetSearchReplicaX().Select(&teamMembers, teamMembersQuery, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find TeamMembers")
}
userMap := map[string]*model.UserForIndexing{}
for _, user := range users {
userMap[user.Id] = &model.UserForIndexing{
Id: user.Id,
Username: user.Username,
Nickname: user.Nickname,
FirstName: user.FirstName,
LastName: user.LastName,
Roles: user.Roles,
CreateAt: user.CreateAt,
DeleteAt: user.DeleteAt,
TeamsIds: []string{},
ChannelsIds: []string{},
}
}
for _, c := range channelMembers {
if userMap[c.UserId] != nil {
userMap[c.UserId].ChannelsIds = append(userMap[c.UserId].ChannelsIds, c.ChannelId)
}
}
for _, t := range teamMembers {
if userMap[t.UserId] != nil {
userMap[t.UserId].TeamsIds = append(userMap[t.UserId].TeamsIds, t.TeamId)
}
}
usersForIndexing := []*model.UserForIndexing{}
for _, user := range userMap {
usersForIndexing = append(usersForIndexing, user)
}
sort.Slice(usersForIndexing, func(i, j int) bool {
return usersForIndexing[i].CreateAt < usersForIndexing[j].CreateAt
})
return usersForIndexing, nil
}
func (us SqlUserStore) GetTeamGroupUsers(teamID string) ([]*model.User, error) {
query := applyTeamGroupConstrainedFilter(us.usersQuery, teamID)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_team_group_users_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func (us SqlUserStore) GetChannelGroupUsers(channelID string) ([]*model.User, error) {
query := applyChannelGroupConstrainedFilter(us.usersQuery, channelID)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_channel_group_users_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find Users")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
func applyViewRestrictionsFilter(query sq.SelectBuilder, restrictions *model.ViewUsersRestrictions, distinct bool) sq.SelectBuilder {
if restrictions == nil {
return query
}
// If you have no access to teams or channels, return and empty result.
if restrictions.Teams != nil && len(restrictions.Teams) == 0 && restrictions.Channels != nil && len(restrictions.Channels) == 0 {
return query.Where("1 = 0")
}
teams := make([]any, len(restrictions.Teams))
for i, v := range restrictions.Teams {
teams[i] = v
}
channels := make([]any, len(restrictions.Channels))
for i, v := range restrictions.Channels {
channels[i] = v
}
resultQuery := query
if restrictions.Teams != nil && len(restrictions.Teams) > 0 {
resultQuery = resultQuery.Join(fmt.Sprintf("TeamMembers rtm ON ( rtm.UserId = u.Id AND rtm.DeleteAt = 0 AND rtm.TeamId IN (%s))", sq.Placeholders(len(teams))), teams...)
}
if restrictions.Channels != nil && len(restrictions.Channels) > 0 {
resultQuery = resultQuery.Join(fmt.Sprintf("ChannelMembers rcm ON ( rcm.UserId = u.Id AND rcm.ChannelId IN (%s))", sq.Placeholders(len(channels))), channels...)
}
if distinct {
return resultQuery.Distinct()
}
return resultQuery
}
func (us SqlUserStore) PromoteGuestToUser(userId string) (err error) {
transaction, err := us.GetMasterX().Beginx()
if err != nil {
return errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
user, err := us.Get(context.Background(), userId)
if err != nil {
return err
}
roles := user.GetRoles()
for idx, role := range roles {
if role == "system_guest" {
roles[idx] = "system_user"
}
}
curTime := model.GetMillis()
query := us.getQueryBuilder().Update("Users").
Set("Roles", strings.Join(roles, " ")).
Set("UpdateAt", curTime).
Where(sq.Eq{"Id": userId})
queryString, args, err := query.ToSql()
if err != nil {
return errors.Wrap(err, "promote_guest_to_user_tosql")
}
if _, err = transaction.Exec(queryString, args...); err != nil {
return errors.Wrapf(err, "failed to update User with userId=%s", userId)
}
query = us.getQueryBuilder().Update("ChannelMembers").
Set("SchemeUser", true).
Set("SchemeGuest", false).
Where(sq.Eq{"UserId": userId})
queryString, args, err = query.ToSql()
if err != nil {
return errors.Wrap(err, "promote_guest_to_user_tosql")
}
if _, err = transaction.Exec(queryString, args...); err != nil {
return errors.Wrapf(err, "failed to update ChannelMembers with userId=%s", userId)
}
query = us.getQueryBuilder().Update("TeamMembers").
Set("SchemeUser", true).
Set("SchemeGuest", false).
Where(sq.Eq{"UserId": userId})
queryString, args, err = query.ToSql()
if err != nil {
return errors.Wrap(err, "promote_guest_to_user_tosql")
}
if _, err := transaction.Exec(queryString, args...); err != nil {
return errors.Wrapf(err, "failed to update TeamMembers with userId=%s", userId)
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "commit_transaction")
}
return nil
}
func (us SqlUserStore) DemoteUserToGuest(userID string) (_ *model.User, err error) {
transaction, err := us.GetMasterX().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
user, err := us.Get(context.Background(), userID)
if err != nil {
return nil, err
}
roles := user.GetRoles()
newRoles := []string{}
for _, role := range roles {
if role == model.SystemUserRoleId {
newRoles = append(newRoles, model.SystemGuestRoleId)
} else if role != model.SystemAdminRoleId {
newRoles = append(newRoles, role)
}
}
curTime := model.GetMillis()
newRolesDBStr := strings.Join(newRoles, " ")
query := us.getQueryBuilder().Update("Users").
Set("Roles", newRolesDBStr).
Set("UpdateAt", curTime).
Where(sq.Eq{"Id": userID})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "demote_user_to_guest_tosql")
}
if _, err = transaction.Exec(queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to update User with userId=%s", userID)
}
user.Roles = newRolesDBStr
user.UpdateAt = curTime
query = us.getQueryBuilder().Update("ChannelMembers").
Set("SchemeUser", false).
Set("SchemeAdmin", false).
Set("SchemeGuest", true).
Where(sq.Eq{"UserId": userID})
queryString, args, err = query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "demote_user_to_guest_tosql")
}
if _, err = transaction.Exec(queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to update ChannelMembers with userId=%s", userID)
}
query = us.getQueryBuilder().Update("TeamMembers").
Set("SchemeUser", false).
Set("SchemeAdmin", false).
Set("SchemeGuest", true).
Where(sq.Eq{"UserId": userID})
queryString, args, err = query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "demote_user_to_guest_tosql")
}
if _, err := transaction.Exec(queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to update TeamMembers with userId=%s", userID)
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return user, nil
}
func (us SqlUserStore) AutocompleteUsersInChannel(teamId, channelId, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error) {
var usersInChannel, usersNotInChannel []*model.User
g := errgroup.Group{}
g.Go(func() (err error) {
usersInChannel, err = us.SearchInChannel(channelId, term, options)
return err
})
g.Go(func() (err error) {
usersNotInChannel, err = us.SearchNotInChannel(teamId, channelId, term, options)
return err
})
err := g.Wait()
if err != nil {
return nil, err
}
return &model.UserAutocompleteInChannel{
InChannel: usersInChannel,
OutOfChannel: usersNotInChannel,
}, nil
}
// GetKnownUsers returns the list of user ids of users with any direct
// relationship with a user. That means any user sharing any channel, including
// direct and group channels.
func (us SqlUserStore) GetKnownUsers(userId string) ([]string, error) {
userIds := []string{}
usersQuery, args, err := us.getQueryBuilder().
Select("DISTINCT ocm.UserId").
From("ChannelMembers AS cm").
Join("ChannelMembers AS ocm ON ocm.ChannelId = cm.ChannelId").
Where(sq.NotEq{"ocm.UserId": userId}).
Where(sq.Eq{"cm.UserId": userId}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetKnownUsers_ToSql")
}
err = us.GetSearchReplicaX().Select(&userIds, usersQuery, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to find ChannelMembers")
}
return userIds, nil
}
// IsEmpty returns whether or not the Users table is empty.
func (us SqlUserStore) IsEmpty(excludeBots bool) (bool, error) {
var hasRows bool
builder := us.getQueryBuilder().
Select("1").
Prefix("SELECT EXISTS (").
From("Users")
if excludeBots {
builder = builder.LeftJoin("Bots ON Users.Id = Bots.UserId").Where("Bots.UserId IS NULL")
}
builder = builder.Suffix(")")
query, args, err := builder.ToSql()
if err != nil {
return false, errors.Wrapf(err, "users_is_empty_to_sql")
}
if err = us.GetReplicaX().Get(&hasRows, query, args...); err != nil {
return false, errors.Wrap(err, "failed to check if table is empty")
}
return !hasRows, nil
}
func (us SqlUserStore) GetUsersWithInvalidEmails(page int, perPage int, restrictedDomains string) ([]*model.User, error) {
domainArray := strings.Split(restrictedDomains, ",")
query := us.usersQuery.
LeftJoin("Bots ON u.Id = Bots.UserId").
Where("Bots.UserId IS NULL").
Where("u.Roles != 'system_guest'").
Where("u.DeleteAt = 0").
Where("(u.AuthService = '' OR u.AuthService IS NULL)")
for _, d := range domainArray {
if d != "" {
query = query.Where("u.Email NOT LIKE LOWER(?)", wildcardSearchTerm(d))
}
}
query = query.Offset(uint64(page * perPage)).Limit(uint64(perPage))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "users_get_many_tosql")
}
users := []*model.User{}
if err := us.GetReplicaX().Select(&users, queryString, args...); err != nil {
return nil, errors.Wrap(err, "users_get_many_select")
}
for _, u := range users {
u.Sanitize(map[string]bool{})
}
return users, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlUserTermsOfServiceStore struct {
*SqlStore
}
func newSqlUserTermsOfServiceStore(sqlStore *SqlStore) store.UserTermsOfServiceStore {
return SqlUserTermsOfServiceStore{sqlStore}
}
func (s SqlUserTermsOfServiceStore) GetByUser(userId string) (*model.UserTermsOfService, error) {
var userTermsOfService model.UserTermsOfService
query := `
SELECT *
FROM UserTermsOfService
WHERE UserId = ?
`
if err := s.GetReplicaX().Get(&userTermsOfService, query, userId); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("UserTermsOfService", "userId="+userId)
}
return nil, errors.Wrapf(err, "failed to get UserTermsOfService with userId=%s", userId)
}
return &userTermsOfService, nil
}
func (s SqlUserTermsOfServiceStore) Save(userTermsOfService *model.UserTermsOfService) (*model.UserTermsOfService, error) {
userTermsOfService.PreSave()
if err := userTermsOfService.IsValid(); err != nil {
return nil, err
}
query := `
UPDATE UserTermsOfService
SET UserId = :UserId, TermsOfServiceId = :TermsOfServiceId, CreateAt = :CreateAt
WHERE UserId = :UserId
`
result, err := s.GetMasterX().NamedExec(query, userTermsOfService)
if err != nil {
return nil, errors.Wrapf(err, "failed to update UserTermsOfService with userId=%s and termsOfServiceId=%s", userTermsOfService.UserId, userTermsOfService.TermsOfServiceId)
}
updatedRows, err := result.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "failed to retrieve the number of affected rows for the update of UserTermsOfService")
}
if updatedRows == 0 {
query := `
INSERT INTO UserTermsOfService
(UserId, TermsOfServiceId, CreateAt)
VALUES
(:UserId, :TermsOfServiceId, :CreateAt)
`
if _, err := s.GetMasterX().NamedExec(query, userTermsOfService); err != nil {
return nil, errors.Wrapf(err, "failed to save UserTermsOfService with userId=%s and termsOfServiceId=%s", userTermsOfService.UserId, userTermsOfService.TermsOfServiceId)
}
}
return userTermsOfService, nil
}
func (s SqlUserTermsOfServiceStore) Delete(userId, termsOfServiceId string) error {
query := `
DELETE
FROM UserTermsOfService
WHERE UserId = ? AND TermsOfServiceId = ?
`
if _, err := s.GetMasterX().Exec(query, userId, termsOfServiceId); err != nil {
return errors.Wrapf(err, "failed to delete UserTermsOfService with userId=%s and termsOfServiceId=%s", userId, termsOfServiceId)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"io"
"net/url"
"strconv"
"strings"
"unicode"
"github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/go-sql-driver/mysql"
)
var escapeLikeSearchChar = []string{
"%",
"_",
}
func sanitizeSearchTerm(term string, escapeChar string) string {
term = strings.Replace(term, escapeChar, "", -1)
for _, c := range escapeLikeSearchChar {
term = strings.Replace(term, c, escapeChar+c, -1)
}
return term
}
// Converts a list of strings into a list of query parameters and a named parameter map that can
// be used as part of a SQL query.
func MapStringsToQueryParams(list []string, paramPrefix string) (string, map[string]any) {
var keys strings.Builder
params := make(map[string]any, len(list))
for i, entry := range list {
if keys.Len() > 0 {
keys.WriteString(",")
}
key := paramPrefix + strconv.Itoa(i)
keys.WriteString(":" + key)
params[key] = entry
}
return "(" + keys.String() + ")", params
}
// finalizeTransactionX ensures a transaction is closed after use, rolling back if not already committed.
func finalizeTransactionX(transaction *sqlxTxWrapper, perr *error) {
// Rollback returns sql.ErrTxDone if the transaction was already closed.
if err := transaction.Rollback(); err != nil && err != sql.ErrTxDone {
*perr = merror.Append(*perr, err)
}
}
func deferClose(c io.Closer, perr *error) {
err := c.Close()
*perr = merror.Append(*perr, err)
}
// removeNonAlphaNumericUnquotedTerms removes all unquoted words that only contain
// non-alphanumeric chars from given line
func removeNonAlphaNumericUnquotedTerms(line, separator string) string {
words := strings.Split(line, separator)
filteredResult := make([]string, 0, len(words))
for _, w := range words {
if isQuotedWord(w) || containsAlphaNumericChar(w) {
filteredResult = append(filteredResult, strings.TrimSpace(w))
}
}
return strings.Join(filteredResult, separator)
}
// containsAlphaNumericChar returns true in case any letter or digit is present, false otherwise
func containsAlphaNumericChar(s string) bool {
for _, r := range s {
if unicode.IsLetter(r) || unicode.IsDigit(r) {
return true
}
}
return false
}
// isQuotedWord return true if the input string is quoted, false otherwise. Ex :-
//
// "quoted string" - will return true
// unquoted string - will return false
func isQuotedWord(s string) bool {
if len(s) < 2 {
return false
}
return s[0] == '"' && s[len(s)-1] == '"'
}
// constructMySQLJSONArgs returns the arg list to pass to a query along with
// the string of placeholders which is needed to be to the JSON_SET function.
// Use this function in this way:
// UPDATE Table
// SET Col = JSON_SET(Col, `+argString+`)
// WHERE Id=?`, args...)
// after appending the Id param to the args slice.
func constructMySQLJSONArgs(props map[string]string) ([]any, string) {
if len(props) == 0 {
return nil, ""
}
// Unpack the keys and values to pass to MySQL.
args := make([]any, 0, len(props))
for k, v := range props {
args = append(args, "$."+k, v)
}
// We calculate the number of ? to set in the query string.
argString := strings.Repeat("?, ", len(props)*2)
// Strip off the trailing comma.
argString = strings.TrimSuffix(argString, ", ")
return args, argString
}
func makeStringArgs(params []string) []any {
args := make([]any, len(params))
for i, name := range params {
args[i] = name
}
return args
}
func constructArrayArgs(ids []string) (string, []any) {
var placeholder strings.Builder
values := make([]any, 0, len(ids))
for _, entry := range ids {
if placeholder.Len() > 0 {
placeholder.WriteString(",")
}
placeholder.WriteString("?")
values = append(values, entry)
}
return "(" + placeholder.String() + ")", values
}
func wrapBinaryParamStringMap(ok bool, props model.StringMap) model.StringMap {
if props == nil {
props = make(model.StringMap)
}
props[model.BinaryParamKey] = strconv.FormatBool(ok)
return props
}
// morphWriter is a target to pass to the logger instance of morph.
// For now, everything is just logged at a debug level. If we need to log
// errors/warnings from the library also, that needs to be seen later.
type morphWriter struct {
}
func (l *morphWriter) Write(in []byte) (int, error) {
mlog.Debug(string(in))
return len(in), nil
}
func DSNHasBinaryParam(dsn string) (bool, error) {
url, err := url.Parse(dsn)
if err != nil {
return false, err
}
return url.Query().Get("binary_parameters") == "yes", nil
}
// AppendBinaryFlag updates the byte slice to work using binary_parameters=yes.
func AppendBinaryFlag(buf []byte) []byte {
return append([]byte{0x01}, buf...)
}
// AppendMultipleStatementsFlag attached dsn parameters to MySQL dsn in order to make migrations work.
func AppendMultipleStatementsFlag(dataSource string) (string, error) {
config, err := mysql.ParseDSN(dataSource)
if err != nil {
return "", err
}
if config.Params == nil {
config.Params = map[string]string{}
}
config.Params["multiStatements"] = "true"
return config.FormatDSN(), nil
}
// ResetReadTimeout removes the timeout constraint from the MySQL dsn.
func ResetReadTimeout(dataSource string) (string, error) {
config, err := mysql.ParseDSN(dataSource)
if err != nil {
return "", err
}
config.ReadTimeout = 0
return config.FormatDSN(), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type SqlWebhookStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
}
func (s SqlWebhookStore) ClearCaches() {
}
func newSqlWebhookStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.WebhookStore {
return &SqlWebhookStore{
SqlStore: sqlStore,
metrics: metrics,
}
}
func (s SqlWebhookStore) InvalidateWebhookCache(webhookId string) {
}
func (s SqlWebhookStore) SaveIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error) {
if webhook.Id != "" {
return nil, store.NewErrInvalidInput("IncomingWebhook", "id", webhook.Id)
}
webhook.PreSave()
if err := webhook.IsValid(); err != nil {
return nil, err
}
if _, err := s.GetMasterX().NamedExec(`INSERT INTO IncomingWebhooks
(Id, CreateAt, UpdateAt, DeleteAt, UserId, ChannelId, TeamId, DisplayName, Description, Username, IconURL, ChannelLocked)
VALUES
(:Id, :CreateAt, :UpdateAt, :DeleteAt, :UserId, :ChannelId, :TeamId, :DisplayName, :Description, :Username, :IconURL, :ChannelLocked)`, webhook); err != nil {
return nil, errors.Wrapf(err, "failed to save IncomingWebhook with id=%s", webhook.Id)
}
return webhook, nil
}
func (s SqlWebhookStore) UpdateIncoming(hook *model.IncomingWebhook) (*model.IncomingWebhook, error) {
hook.UpdateAt = model.GetMillis()
_, err := s.GetMasterX().NamedExec(`UPDATE IncomingWebhooks SET
CreateAt=:CreateAt, UpdateAt=:UpdateAt, DeleteAt=:DeleteAt, ChannelId=:ChannelId, TeamId=:TeamId, DisplayName=:DisplayName,
Description=:Description, Username=:Username, IconURL=:IconURL, ChannelLocked=:ChannelLocked
WHERE Id=:Id`, hook)
if err != nil {
return nil, errors.Wrapf(err, "failed to update IncomingWebhook with id=%s", hook.Id)
}
return hook, nil
}
func (s SqlWebhookStore) GetIncoming(id string, allowFromCache bool) (*model.IncomingWebhook, error) {
var webhook model.IncomingWebhook
if err := s.GetReplicaX().Get(&webhook, "SELECT * FROM IncomingWebhooks WHERE Id = ? AND DeleteAt = 0", id); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("IncomingWebhook", id)
}
return nil, errors.Wrapf(err, "failed to get IncomingWebhook with id=%s", id)
}
return &webhook, nil
}
func (s SqlWebhookStore) DeleteIncoming(webhookId string, time int64) error {
_, err := s.GetMasterX().Exec("UPDATE IncomingWebhooks SET DeleteAt = ?, UpdateAt = ? WHERE Id = ?", time, time, webhookId)
if err != nil {
return errors.Wrapf(err, "failed to update IncomingWebhook with id=%s", webhookId)
}
return nil
}
func (s SqlWebhookStore) PermanentDeleteIncomingByUser(userId string) error {
_, err := s.GetMasterX().Exec("DELETE FROM IncomingWebhooks WHERE UserId = ?", userId)
if err != nil {
return errors.Wrapf(err, "failed to delete IncomingWebhook with userId=%s", userId)
}
return nil
}
func (s SqlWebhookStore) PermanentDeleteIncomingByChannel(channelId string) error {
_, err := s.GetMasterX().Exec("DELETE FROM IncomingWebhooks WHERE ChannelId = ?", channelId)
if err != nil {
return errors.Wrapf(err, "failed to delete IncomingWebhook with channelId=%s", channelId)
}
return nil
}
func (s SqlWebhookStore) GetIncomingList(offset, limit int) ([]*model.IncomingWebhook, error) {
return s.GetIncomingListByUser("", offset, limit)
}
func (s SqlWebhookStore) GetIncomingListByUser(userId string, offset, limit int) ([]*model.IncomingWebhook, error) {
webhooks := []*model.IncomingWebhook{}
query := s.getQueryBuilder().
Select("*").
From("IncomingWebhooks").
Where(sq.Eq{"DeleteAt": int(0)}).Limit(uint64(limit)).Offset(uint64(offset))
if userId != "" {
query = query.Where(sq.Eq{"UserId": userId})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "incoming_webhook_tosql")
}
if err := s.GetReplicaX().Select(&webhooks, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find IncomingWebhooks")
}
return webhooks, nil
}
func (s SqlWebhookStore) GetIncomingByTeamByUser(teamId string, userId string, offset, limit int) ([]*model.IncomingWebhook, error) {
webhooks := []*model.IncomingWebhook{}
query := s.getQueryBuilder().
Select("*").
From("IncomingWebhooks").
Where(sq.And{
sq.Eq{"TeamId": teamId},
sq.Eq{"DeleteAt": int(0)},
}).Limit(uint64(limit)).Offset(uint64(offset))
if userId != "" {
query = query.Where(sq.Eq{"UserId": userId})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "incoming_webhook_tosql")
}
if err := s.GetReplicaX().Select(&webhooks, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find IncomingWebhook with teamId=%s", teamId)
}
return webhooks, nil
}
func (s SqlWebhookStore) GetIncomingByTeam(teamId string, offset, limit int) ([]*model.IncomingWebhook, error) {
return s.GetIncomingByTeamByUser(teamId, "", offset, limit)
}
func (s SqlWebhookStore) GetIncomingByChannel(channelId string) ([]*model.IncomingWebhook, error) {
webhooks := []*model.IncomingWebhook{}
if err := s.GetReplicaX().Select(&webhooks, "SELECT * FROM IncomingWebhooks WHERE ChannelId = ? AND DeleteAt = 0", channelId); err != nil {
return nil, errors.Wrapf(err, "failed to find IncomingWebhooks with channelId=%s", channelId)
}
return webhooks, nil
}
func (s SqlWebhookStore) SaveOutgoing(webhook *model.OutgoingWebhook) (*model.OutgoingWebhook, error) {
if webhook.Id != "" {
return nil, store.NewErrInvalidInput("OutgoingWebhook", "id", webhook.Id)
}
webhook.PreSave()
if err := webhook.IsValid(); err != nil {
return nil, err
}
if _, err := s.GetMasterX().NamedExec(`INSERT INTO OutgoingWebhooks
(Id, Token, CreateAt, UpdateAt, DeleteAt, CreatorId, ChannelId, TeamId, TriggerWords, TriggerWhen,
CallbackURLs, DisplayName, Description, ContentType, Username, IconURL)
VALUES
(:Id, :Token, :CreateAt, :UpdateAt, :DeleteAt, :CreatorId, :ChannelId, :TeamId, :TriggerWords, :TriggerWhen,
:CallbackURLs, :DisplayName, :Description, :ContentType, :Username, :IconURL)`, webhook); err != nil {
return nil, errors.Wrapf(err, "failed to save OutgoingWebhook with id=%s", webhook.Id)
}
return webhook, nil
}
func (s SqlWebhookStore) GetOutgoing(id string) (*model.OutgoingWebhook, error) {
var webhook model.OutgoingWebhook
if err := s.GetReplicaX().Get(&webhook, "SELECT * FROM OutgoingWebhooks WHERE Id = ? AND DeleteAt = 0", id); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("OutgoingWebhook", id)
}
return nil, errors.Wrapf(err, "failed to get OutgoingWebhook with id=%s", id)
}
return &webhook, nil
}
func (s SqlWebhookStore) GetOutgoingListByUser(userId string, offset, limit int) ([]*model.OutgoingWebhook, error) {
webhooks := []*model.OutgoingWebhook{}
query := s.getQueryBuilder().
Select("*").
From("OutgoingWebhooks").
Where(sq.And{
sq.Eq{"DeleteAt": int(0)},
}).Limit(uint64(limit)).Offset(uint64(offset))
if userId != "" {
query = query.Where(sq.Eq{"CreatorId": userId})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "outgoing_webhook_tosql")
}
if err := s.GetReplicaX().Select(&webhooks, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find OutgoingWebhooks")
}
return webhooks, nil
}
func (s SqlWebhookStore) GetOutgoingList(offset, limit int) ([]*model.OutgoingWebhook, error) {
return s.GetOutgoingListByUser("", offset, limit)
}
func (s SqlWebhookStore) GetOutgoingByChannelByUser(channelId string, userId string, offset, limit int) ([]*model.OutgoingWebhook, error) {
webhooks := []*model.OutgoingWebhook{}
query := s.getQueryBuilder().
Select("*").
From("OutgoingWebhooks").
Where(sq.And{
sq.Eq{"ChannelId": channelId},
sq.Eq{"DeleteAt": int(0)},
})
if userId != "" {
query = query.Where(sq.Eq{"CreatorId": userId})
}
if limit >= 0 && offset >= 0 {
query = query.Limit(uint64(limit)).Offset(uint64(offset))
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "outgoing_webhook_tosql")
}
if err := s.GetReplicaX().Select(&webhooks, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find OutgoingWebhooks")
}
return webhooks, nil
}
func (s SqlWebhookStore) GetOutgoingByChannel(channelId string, offset, limit int) ([]*model.OutgoingWebhook, error) {
return s.GetOutgoingByChannelByUser(channelId, "", offset, limit)
}
func (s SqlWebhookStore) GetOutgoingByTeamByUser(teamId string, userId string, offset, limit int) ([]*model.OutgoingWebhook, error) {
webhooks := []*model.OutgoingWebhook{}
query := s.getQueryBuilder().
Select("*").
From("OutgoingWebhooks").
Where(sq.And{
sq.Eq{"TeamId": teamId},
sq.Eq{"DeleteAt": int(0)},
})
if userId != "" {
query = query.Where(sq.Eq{"CreatorId": userId})
}
if limit >= 0 && offset >= 0 {
query = query.Limit(uint64(limit)).Offset(uint64(offset))
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "outgoing_webhook_tosql")
}
if err := s.GetReplicaX().Select(&webhooks, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to find OutgoingWebhooks")
}
return webhooks, nil
}
func (s SqlWebhookStore) GetOutgoingByTeam(teamId string, offset, limit int) ([]*model.OutgoingWebhook, error) {
return s.GetOutgoingByTeamByUser(teamId, "", offset, limit)
}
func (s SqlWebhookStore) DeleteOutgoing(webhookId string, time int64) error {
_, err := s.GetMasterX().Exec("Update OutgoingWebhooks SET DeleteAt = ?, UpdateAt = ? WHERE Id = ?", time, time, webhookId)
if err != nil {
return errors.Wrapf(err, "failed to update OutgoingWebhook with id=%s", webhookId)
}
return nil
}
func (s SqlWebhookStore) PermanentDeleteOutgoingByUser(userId string) error {
_, err := s.GetMasterX().Exec("DELETE FROM OutgoingWebhooks WHERE CreatorId = ?", userId)
if err != nil {
return errors.Wrapf(err, "failed to delete OutgoingWebhook with creatorId=%s", userId)
}
return nil
}
func (s SqlWebhookStore) PermanentDeleteOutgoingByChannel(channelId string) error {
_, err := s.GetMasterX().Exec("DELETE FROM OutgoingWebhooks WHERE ChannelId = ?", channelId)
if err != nil {
return errors.Wrapf(err, "failed to delete OutgoingWebhook with channelId=%s", channelId)
}
s.ClearCaches()
return nil
}
func (s SqlWebhookStore) UpdateOutgoing(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, error) {
hook.UpdateAt = model.GetMillis()
_, err := s.GetMasterX().NamedExec(`UPDATE OutgoingWebhooks SET
CreateAt = :CreateAt, UpdateAt = :UpdateAt, DeleteAt = :DeleteAt, Token = :Token, CreatorId = :CreatorId,
ChannelId = :ChannelId, TeamId = :TeamId, TriggerWords = :TriggerWords, TriggerWhen = :TriggerWhen,
CallbackURLs = :CallbackURLs, DisplayName = :DisplayName, Description = :Description,
ContentType = :ContentType, Username = :Username, IconURL = :IconURL WHERE Id = :Id`, hook)
if err != nil {
return nil, errors.Wrapf(err, "failed to update OutgoingWebhook with id=%s", hook.Id)
}
return hook, nil
}
func (s SqlWebhookStore) AnalyticsIncomingCount(teamId string) (int64, error) {
queryBuilder :=
s.getQueryBuilder().
Select("COUNT(*)").
From("IncomingWebhooks").
Where("DeleteAt = 0")
if teamId != "" {
queryBuilder = queryBuilder.Where("TeamId", teamId)
}
queryString, args, err := queryBuilder.ToSql()
if err != nil {
return 0, errors.Wrap(err, "incoming_webhook_tosql")
}
var count int64
if err := s.GetReplicaX().Get(&count, queryString, args...); err != nil {
return 0, errors.Wrap(err, "failed to count IncomingWebhooks")
}
return count, nil
}
func (s SqlWebhookStore) AnalyticsOutgoingCount(teamId string) (int64, error) {
queryBuilder :=
s.getQueryBuilder().
Select("COUNT(*)").
From("OutgoingWebhooks").
Where("DeleteAt = 0")
if teamId != "" {
queryBuilder = queryBuilder.Where("TeamId", teamId)
}
queryString, args, err := queryBuilder.ToSql()
if err != nil {
return 0, errors.Wrap(err, "outgoing_webhook_tosql")
}
var count int64
if err := s.GetReplicaX().Get(&count, queryString, args...); err != nil {
return 0, errors.Wrap(err, "failed to count OutgoingWebhooks")
}
return count, nil
}
//go:generate go run layer_generators/main.go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package store
import (
"context"
"database/sql"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
)
type StoreResult struct {
Data any
// NErr a temporary field used by the new code for the AppError migration. This will later become Err when the entire store is migrated.
NErr error
}
type Store interface {
Team() TeamStore
Channel() ChannelStore
Post() PostStore
RetentionPolicy() RetentionPolicyStore
Thread() ThreadStore
User() UserStore
Bot() BotStore
Audit() AuditStore
ClusterDiscovery() ClusterDiscoveryStore
RemoteCluster() RemoteClusterStore
Compliance() ComplianceStore
Session() SessionStore
OAuth() OAuthStore
System() SystemStore
Webhook() WebhookStore
Command() CommandStore
CommandWebhook() CommandWebhookStore
Preference() PreferenceStore
License() LicenseStore
Token() TokenStore
Emoji() EmojiStore
Status() StatusStore
FileInfo() FileInfoStore
UploadSession() UploadSessionStore
Reaction() ReactionStore
Role() RoleStore
Scheme() SchemeStore
Job() JobStore
UserAccessToken() UserAccessTokenStore
ChannelMemberHistory() ChannelMemberHistoryStore
Plugin() PluginStore
TermsOfService() TermsOfServiceStore
ProductNotices() ProductNoticesStore
Group() GroupStore
UserTermsOfService() UserTermsOfServiceStore
LinkMetadata() LinkMetadataStore
SharedChannel() SharedChannelStore
Draft() DraftStore
MarkSystemRanUnitTests()
Close()
LockToMaster()
UnlockFromMaster()
DropAllTables()
RecycleDBConnections(d time.Duration)
GetDBSchemaVersion() (int, error)
GetAppliedMigrations() ([]model.AppliedMigration, error)
GetDbVersion(numerical bool) (string, error)
// GetInternalMasterDB allows access to the raw master DB
// handle for the multi-product architecture.
GetInternalMasterDB() *sql.DB
// GetInternalReplicaDBs allows access to the raw replica DB
// handles for the multi-product architecture.
GetInternalReplicaDB() *sql.DB
GetInternalReplicaDBs() []*sql.DB
TotalMasterDbConnections() int
TotalReadDbConnections() int
TotalSearchDbConnections() int
ReplicaLagTime() error
ReplicaLagAbs() error
CheckIntegrity() <-chan model.IntegrityCheckResult
SetContext(context context.Context)
Context() context.Context
NotifyAdmin() NotifyAdminStore
PostPriority() PostPriorityStore
PostAcknowledgement() PostAcknowledgementStore
TrueUpReview() TrueUpReviewStore
}
type RetentionPolicyStore interface {
Save(policy *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error)
Patch(patch *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error)
Get(id string) (*model.RetentionPolicyWithTeamAndChannelCounts, error)
GetAll(offset, limit int) ([]*model.RetentionPolicyWithTeamAndChannelCounts, error)
GetCount() (int64, error)
Delete(id string) error
GetChannels(policyId string, offset, limit int) (model.ChannelListWithTeamData, error)
GetChannelsCount(policyId string) (int64, error)
AddChannels(policyId string, channelIds []string) error
RemoveChannels(policyId string, channelIds []string) error
GetTeams(policyId string, offset, limit int) ([]*model.Team, error)
GetTeamsCount(policyId string) (int64, error)
AddTeams(policyId string, teamIds []string) error
RemoveTeams(policyId string, teamIds []string) error
DeleteOrphanedRows(limit int) (int64, error)
GetTeamPoliciesForUser(userID string, offset, limit int) ([]*model.RetentionPolicyForTeam, error)
GetTeamPoliciesCountForUser(userID string) (int64, error)
GetChannelPoliciesForUser(userID string, offset, limit int) ([]*model.RetentionPolicyForChannel, error)
GetChannelPoliciesCountForUser(userID string) (int64, error)
}
type TeamStore interface {
Save(team *model.Team) (*model.Team, error)
Update(team *model.Team) (*model.Team, error)
Get(id string) (*model.Team, error)
GetMany(ids []string) ([]*model.Team, error)
GetByName(name string) (*model.Team, error)
GetByNames(name []string) ([]*model.Team, error)
SearchAll(opts *model.TeamSearch) ([]*model.Team, error)
SearchAllPaged(opts *model.TeamSearch) ([]*model.Team, int64, error)
SearchOpen(opts *model.TeamSearch) ([]*model.Team, error)
SearchPrivate(opts *model.TeamSearch) ([]*model.Team, error)
GetAll() ([]*model.Team, error)
GetAllPage(offset int, limit int, opts *model.TeamSearch) ([]*model.Team, error)
GetAllPrivateTeamListing() ([]*model.Team, error)
GetAllTeamListing() ([]*model.Team, error)
GetTeamsByUserId(userID string) ([]*model.Team, error)
GetByInviteId(inviteID string) (*model.Team, error)
GetByEmptyInviteID() ([]*model.Team, error)
PermanentDelete(teamID string) error
AnalyticsTeamCount(opts *model.TeamSearch) (int64, error)
SaveMultipleMembers(members []*model.TeamMember, maxUsersPerTeam int) ([]*model.TeamMember, error)
SaveMember(member *model.TeamMember, maxUsersPerTeam int) (*model.TeamMember, error)
UpdateMember(member *model.TeamMember) (*model.TeamMember, error)
UpdateMultipleMembers(members []*model.TeamMember) ([]*model.TeamMember, error)
GetMember(ctx context.Context, teamID string, userID string) (*model.TeamMember, error)
GetMembers(teamID string, offset int, limit int, teamMembersGetOptions *model.TeamMembersGetOptions) ([]*model.TeamMember, error)
GetMembersByIds(teamID string, userIds []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, error)
GetTotalMemberCount(teamID string, restrictions *model.ViewUsersRestrictions) (int64, error)
GetActiveMemberCount(teamID string, restrictions *model.ViewUsersRestrictions) (int64, error)
GetTeamsForUser(ctx context.Context, userID, excludeTeamID string, includeDeleted bool) ([]*model.TeamMember, error)
GetTeamsForUserWithPagination(userID string, page, perPage int) ([]*model.TeamMember, error)
GetChannelUnreadsForAllTeams(excludeTeamID, userID string) ([]*model.ChannelUnread, error)
GetChannelUnreadsForTeam(teamID, userID string) ([]*model.ChannelUnread, error)
RemoveMember(teamID string, userID string) error
RemoveMembers(teamID string, userIds []string) error
RemoveAllMembersByTeam(teamID string) error
RemoveAllMembersByUser(userID string) error
UpdateLastTeamIconUpdate(teamID string, curTime int64) error
GetTeamsByScheme(schemeID string, offset int, limit int) ([]*model.Team, error)
MigrateTeamMembers(fromTeamID string, fromUserID string) (map[string]string, error)
ResetAllTeamSchemes() error
ClearAllCustomRoleAssignments() error
AnalyticsGetTeamCountForScheme(schemeID string) (int64, error)
GetAllForExportAfter(limit int, afterID string) ([]*model.TeamForExport, error)
GetTeamMembersForExport(userID string) ([]*model.TeamMemberForExport, error)
UserBelongsToTeams(userID string, teamIds []string) (bool, error)
GetUserTeamIds(userID string, allowFromCache bool) ([]string, error)
InvalidateAllTeamIdsForUser(userID string)
ClearCaches()
// UpdateMembersRole sets all of the given team members to admins and all of the other members of the team to
// non-admin members.
UpdateMembersRole(teamID string, userIDs []string) error
// GroupSyncedTeamCount returns the count of non-deleted group-constrained teams.
GroupSyncedTeamCount() (int64, error)
// GetCommonTeamIDsForTwoUsers returns the intersection of all the teams to which the specified
// users belong.
GetCommonTeamIDsForTwoUsers(userID, otherUserID string) ([]string, error)
GetNewTeamMembersSince(teamID string, since int64, offset int, limit int) (*model.NewTeamMembersList, int64, error)
}
type ChannelStore interface {
Save(channel *model.Channel, maxChannelsPerTeam int64) (*model.Channel, error)
CreateDirectChannel(userID *model.User, otherUserID *model.User, channelOptions ...model.ChannelOption) (*model.Channel, error)
SaveDirectChannel(channel *model.Channel, member1 *model.ChannelMember, member2 *model.ChannelMember) (*model.Channel, error)
Update(channel *model.Channel) (*model.Channel, error)
UpdateSidebarChannelCategoryOnMove(channel *model.Channel, newTeamID string) error
ClearSidebarOnTeamLeave(userID, teamID string) error
Get(id string, allowFromCache bool) (*model.Channel, error)
GetMany(ids []string, allowFromCache bool) (model.ChannelList, error)
InvalidateChannel(id string)
InvalidateChannelByName(teamID, name string)
Delete(channelID string, timestamp int64) error
Restore(channelID string, timestamp int64) error
SetDeleteAt(channelID string, deleteAt int64, updateAt int64) error
PermanentDelete(channelID string) error
PermanentDeleteByTeam(teamID string) error
GetByName(team_id string, name string, allowFromCache bool) (*model.Channel, error)
GetByNames(team_id string, names []string, allowFromCache bool) ([]*model.Channel, error)
GetByNameIncludeDeleted(team_id string, name string, allowFromCache bool) (*model.Channel, error)
GetDeletedByName(team_id string, name string) (*model.Channel, error)
GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error)
GetChannels(teamID, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, error)
GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error)
GetChannelsByUser(userID string, includeDeleted bool, lastDeleteAt, pageSize int, fromChannelID string) (model.ChannelList, error)
GetAllChannelMembersById(id string) ([]string, error)
GetAllChannels(page, perPage int, opts ChannelSearchOpts) (model.ChannelListWithTeamData, error)
GetAllChannelsCount(opts ChannelSearchOpts) (int64, error)
GetMoreChannels(teamID string, userID string, offset int, limit int) (model.ChannelList, error)
GetPrivateChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, error)
GetPublicChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, error)
GetPublicChannelsByIdsForTeam(teamID string, channelIds []string) (model.ChannelList, error)
GetChannelCounts(teamID string, userID string) (*model.ChannelCounts, error)
GetTeamChannels(teamID string) (model.ChannelList, error)
GetAll(teamID string) ([]*model.Channel, error)
GetChannelsByIds(channelIds []string, includeDeleted bool) ([]*model.Channel, error)
GetChannelsWithTeamDataByIds(channelIds []string, includeDeleted bool) ([]*model.ChannelWithTeamData, error)
GetForPost(postID string) (*model.Channel, error)
SaveMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error)
SaveMember(member *model.ChannelMember) (*model.ChannelMember, error)
UpdateMember(member *model.ChannelMember) (*model.ChannelMember, error)
UpdateMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error)
// UpdateMemberNotifyProps patches the notifyProps field with the given props map.
// It replaces existing fields and creates new ones which don't exist.
UpdateMemberNotifyProps(channelID, userID string, props map[string]string) (*model.ChannelMember, error)
GetMembers(channelID string, offset, limit int) (model.ChannelMembers, error)
GetMember(ctx context.Context, channelID string, userID string) (*model.ChannelMember, error)
GetChannelMembersTimezones(channelID string) ([]model.StringMap, error)
GetAllChannelMembersForUser(userID string, allowFromCache bool, includeDeleted bool) (map[string]string, error)
InvalidateAllChannelMembersForUser(userID string)
IsUserInChannelUseCache(userID string, channelID string) bool
GetAllChannelMembersNotifyPropsForChannel(channelID string, allowFromCache bool) (map[string]model.StringMap, error)
InvalidateCacheForChannelMembersNotifyProps(channelID string)
GetMemberForPost(postID string, userID string) (*model.ChannelMember, error)
InvalidateMemberCount(channelID string)
GetMemberCountFromCache(channelID string) int64
GetFileCount(channelID string) (int64, error)
GetMemberCount(channelID string, allowFromCache bool) (int64, error)
GetMemberCountsByGroup(ctx context.Context, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, error)
InvalidatePinnedPostCount(channelID string)
GetPinnedPostCount(channelID string, allowFromCache bool) (int64, error)
InvalidateGuestCount(channelID string)
GetGuestCount(channelID string, allowFromCache bool) (int64, error)
GetPinnedPosts(channelID string) (*model.PostList, error)
RemoveMember(channelID string, userID string) error
RemoveMembers(channelID string, userIds []string) error
PermanentDeleteMembersByUser(userID string) error
PermanentDeleteMembersByChannel(channelID string) error
UpdateLastViewedAt(channelIds []string, userID string) (map[string]int64, error)
UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount, mentionCountRoot, urgentMentionCount int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error)
CountPostsAfter(channelID string, timestamp int64, userID string) (int, int, error)
CountUrgentPostsAfter(channelID string, timestamp int64, userID string) (int, error)
IncrementMentionCount(channelID string, userIDs []string, isRoot, isUrgent bool) error
AnalyticsTypeCount(teamID string, channelType model.ChannelType) (int64, error)
GetMembersForUser(teamID string, userID string) (model.ChannelMembers, error)
GetTeamMembersForChannel(channelID string) ([]string, error)
GetMembersForUserWithPagination(userID string, page, perPage int) (model.ChannelMembersWithTeamData, error)
GetMembersForUserWithCursor(userID, teamID string, opts *ChannelMemberGraphQLSearchOpts) (model.ChannelMembers, error)
Autocomplete(userID, term string, includeDeleted, isGuest bool) (model.ChannelListWithTeamData, error)
AutocompleteInTeam(teamID, userID, term string, includeDeleted, isGuest bool) (model.ChannelList, error)
AutocompleteInTeamForSearch(teamID string, userID string, term string, includeDeleted bool) (model.ChannelList, error)
SearchAllChannels(term string, opts ChannelSearchOpts) (model.ChannelListWithTeamData, int64, error)
SearchInTeam(teamID string, term string, includeDeleted bool) (model.ChannelList, error)
SearchArchivedInTeam(teamID string, term string, userID string) (model.ChannelList, error)
SearchForUserInTeam(userID string, teamID string, term string, includeDeleted bool) (model.ChannelList, error)
SearchMore(userID string, teamID string, term string) (model.ChannelList, error)
SearchGroupChannels(userID, term string) (model.ChannelList, error)
GetMembersByIds(channelID string, userIds []string) (model.ChannelMembers, error)
GetMembersByChannelIds(channelIds []string, userID string) (model.ChannelMembers, error)
GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error)
AnalyticsDeletedTypeCount(teamID string, channelType model.ChannelType) (int64, error)
GetChannelUnread(channelID, userID string) (*model.ChannelUnread, error)
ClearCaches()
ClearMembersForUserCache()
GetChannelsByScheme(schemeID string, offset int, limit int) (model.ChannelList, error)
MigrateChannelMembers(fromChannelID string, fromUserID string) (map[string]string, error)
ResetAllChannelSchemes() error
ClearAllCustomRoleAssignments() error
CreateInitialSidebarCategories(userID string, opts *SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error)
GetSidebarCategoriesForTeamForUser(userID, teamID string) (*model.OrderedSidebarCategories, error)
GetSidebarCategories(userID string, opts *SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error)
GetSidebarCategory(categoryID string) (*model.SidebarCategoryWithChannels, error)
GetSidebarCategoryOrder(userID, teamID string) ([]string, error)
CreateSidebarCategory(userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error)
UpdateSidebarCategoryOrder(userID, teamID string, categoryOrder []string) error
UpdateSidebarCategories(userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error)
UpdateSidebarChannelsByPreferences(preferences model.Preferences) error
DeleteSidebarChannelsByPreferences(preferences model.Preferences) error
DeleteSidebarCategory(categoryID string) error
GetAllChannelsForExportAfter(limit int, afterID string) ([]*model.ChannelForExport, error)
GetAllDirectChannelsForExportAfter(limit int, afterID string) ([]*model.DirectChannelForExport, error)
GetChannelMembersForExport(userID string, teamID string) ([]*model.ChannelMemberForExport, error)
RemoveAllDeactivatedMembers(channelID string) error
GetChannelsBatchForIndexing(startTime int64, startChannelID string, limit int) ([]*model.Channel, error)
UserBelongsToChannels(userID string, channelIds []string) (bool, error)
// UpdateMembersRole sets all of the given team members to admins and all of the other members of the team to
// non-admin members.
UpdateMembersRole(channelID string, userIDs []string) error
// GroupSyncedChannelCount returns the count of non-deleted group-constrained channels.
GroupSyncedChannelCount() (int64, error)
SetShared(channelId string, shared bool) error
// GetTeamForChannel returns the team for a given channelID.
GetTeamForChannel(channelID string) (*model.Team, error)
// Insights - channels
GetTopChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopChannelList, error)
GetTopChannelsForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopChannelList, error)
PostCountsByDuration(channelIDs []string, sinceUnixMillis int64, userID *string, duration model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, error)
// Insights - inactive channels
GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error)
GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error)
}
type ChannelMemberHistoryStore interface {
LogJoinEvent(userID string, channelID string, joinTime int64) error
LogLeaveEvent(userID string, channelID string, leaveTime int64) error
GetUsersInChannelDuring(startTime int64, endTime int64, channelID string) ([]*model.ChannelMemberHistoryResult, error)
PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)
DeleteOrphanedRows(limit int) (deleted int64, err error)
PermanentDeleteBatch(endTime int64, limit int64) (int64, error)
GetChannelsLeftSince(userID string, since int64) ([]string, error)
}
type ThreadStore interface {
GetThreadFollowers(threadID string, fetchOnlyActive bool) ([]string, error)
Get(id string) (*model.Thread, error)
GetTotalUnreadThreads(userId, teamID string, opts model.GetUserThreadsOpts) (int64, error)
GetTotalThreads(userId, teamID string, opts model.GetUserThreadsOpts) (int64, error)
GetTotalUnreadMentions(userId, teamID string, opts model.GetUserThreadsOpts) (int64, error)
GetTotalUnreadUrgentMentions(userId, teamID string, opts model.GetUserThreadsOpts) (int64, error)
GetThreadsForUser(userId, teamID string, opts model.GetUserThreadsOpts) ([]*model.ThreadResponse, error)
GetThreadForUser(threadMembership *model.ThreadMembership, extended, postPriorityIsEnabled bool) (*model.ThreadResponse, error)
GetTeamsUnreadForUser(userID string, teamIDs []string, includeUrgentMentionCount bool) (map[string]*model.TeamUnread, error)
MarkAllAsRead(userID string, threadIds []string) error
MarkAllAsReadByTeam(userID, teamID string) error
MarkAllAsReadByChannels(userID string, channelIDs []string) error
MarkAsRead(userID, threadID string, timestamp int64) error
UpdateMembership(membership *model.ThreadMembership) (*model.ThreadMembership, error)
GetMembershipsForUser(userId, teamID string) ([]*model.ThreadMembership, error)
GetMembershipForUser(userId, postID string) (*model.ThreadMembership, error)
DeleteMembershipForUser(userId, postID string) error
MaintainMembership(userID, postID string, opts ThreadMembershipOpts) (*model.ThreadMembership, error)
PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)
PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)
DeleteOrphanedRows(limit int) (deleted int64, err error)
GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error)
// Insights - threads
GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error)
GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error)
}
type PostStore interface {
SaveMultiple(posts []*model.Post) ([]*model.Post, int, error)
Save(post *model.Post) (*model.Post, error)
Update(newPost *model.Post, oldPost *model.Post) (*model.Post, error)
Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error)
GetSingle(id string, inclDeleted bool) (*model.Post, error)
Delete(postID string, timestamp int64, deleteByID string) error
PermanentDeleteByUser(userID string) error
PermanentDeleteByChannel(channelID string) error
GetPosts(options model.GetPostsOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error)
GetFlaggedPosts(userID string, offset int, limit int) (*model.PostList, error)
// @openTracingParams userID, teamID, offset, limit
GetFlaggedPostsForTeam(userID, teamID string, offset int, limit int) (*model.PostList, error)
GetFlaggedPostsForChannel(userID, channelID string, offset int, limit int) (*model.PostList, error)
GetPostsBefore(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error)
GetPostsAfter(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error)
GetPostsSince(options model.GetPostsSinceOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error)
GetPostsByThread(threadID string, since int64) ([]*model.Post, error)
GetPostAfterTime(channelID string, timestamp int64, collapsedThreads bool) (*model.Post, error)
GetPostIdAfterTime(channelID string, timestamp int64, collapsedThreads bool) (string, error)
GetPostIdBeforeTime(channelID string, timestamp int64, collapsedThreads bool) (string, error)
GetEtag(channelID string, allowFromCache bool, collapsedThreads bool) string
Search(teamID string, userID string, params *model.SearchParams) (*model.PostList, error)
AnalyticsUserCountsWithPostsByDay(teamID string) (model.AnalyticsRows, error)
AnalyticsPostCountsByDay(options *model.AnalyticsPostCountsOptions) (model.AnalyticsRows, error)
AnalyticsPostCount(options *model.PostCountOptions) (int64, error)
ClearCaches()
InvalidateLastPostTimeCache(channelID string)
GetPostsCreatedAt(channelID string, timestamp int64) ([]*model.Post, error)
Overwrite(post *model.Post) (*model.Post, error)
OverwriteMultiple(posts []*model.Post) ([]*model.Post, int, error)
GetPostsByIds(postIds []string) ([]*model.Post, error)
GetEditHistoryForPost(postId string) ([]*model.Post, error)
GetPostsBatchForIndexing(startTime int64, startPostID string, limit int) ([]*model.PostForIndexing, error)
PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)
DeleteOrphanedRows(limit int) (deleted int64, err error)
PermanentDeleteBatch(endTime int64, limit int64) (int64, error)
GetOldest() (*model.Post, error)
GetMaxPostSize() int
GetParentsForExportAfter(limit int, afterID string) ([]*model.PostForExport, error)
GetRepliesForExport(parentID string) ([]*model.ReplyForExport, error)
GetDirectPostParentsForExportAfter(limit int, afterID string) ([]*model.DirectPostForExport, error)
SearchPostsForUser(paramsList []*model.SearchParams, userID, teamID string, page, perPage int) (*model.PostSearchResults, error)
GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error)
LogRecentSearch(userID string, searchQuery []byte, createAt int64) error
GetOldestEntityCreationTime() (int64, error)
HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error)
GetPostsSinceForSync(options model.GetPostsSinceForSyncOptions, cursor model.GetPostsSinceForSyncCursor, limit int) ([]*model.Post, model.GetPostsSinceForSyncCursor, error)
SetPostReminder(reminder *model.PostReminder) error
GetPostReminders(now int64) ([]*model.PostReminder, error)
GetPostReminderMetadata(postID string) (*PostReminderMetadata, error)
// GetNthRecentPostTime returns the CreateAt time of the nth most recent post.
GetNthRecentPostTime(n int64) (int64, error)
// Insights - top DMs
GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error)
}
type UserStore interface {
Save(user *model.User) (*model.User, error)
Update(user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error)
UpdateNotifyProps(userID string, props map[string]string) error
UpdateLastPictureUpdate(userID string) error
ResetLastPictureUpdate(userID string) error
UpdatePassword(userID, newPassword string) error
UpdateUpdateAt(userID string) (int64, error)
UpdateAuthData(userID string, service string, authData *string, email string, resetMfa bool) (string, error)
ResetAuthDataToEmailForUsers(service string, userIDs []string, includeDeleted bool, dryRun bool) (int, error)
UpdateMfaSecret(userID, secret string) error
UpdateMfaActive(userID string, active bool) error
Get(ctx context.Context, id string) (*model.User, error)
GetMany(ctx context.Context, ids []string) ([]*model.User, error)
GetAll() ([]*model.User, error)
ClearCaches()
InvalidateProfilesInChannelCacheByUser(userID string)
InvalidateProfilesInChannelCache(channelID string)
GetProfilesInChannel(options *model.UserGetOptions) ([]*model.User, error)
GetProfilesInChannelByStatus(options *model.UserGetOptions) ([]*model.User, error)
GetProfilesInChannelByAdmin(options *model.UserGetOptions) ([]*model.User, error)
GetAllProfilesInChannel(ctx context.Context, channelID string, allowFromCache bool) (map[string]*model.User, error)
GetProfilesNotInChannel(teamID string, channelId string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error)
GetProfilesWithoutTeam(options *model.UserGetOptions) ([]*model.User, error)
GetProfilesByUsernames(usernames []string, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error)
GetAllProfiles(options *model.UserGetOptions) ([]*model.User, error)
GetProfiles(options *model.UserGetOptions) ([]*model.User, error)
GetProfileByIds(ctx context.Context, userIds []string, options *UserGetByIdsOpts, allowFromCache bool) ([]*model.User, error)
GetProfileByGroupChannelIdsForUser(userID string, channelIds []string) (map[string][]*model.User, error)
InvalidateProfileCacheForUser(userID string)
GetByEmail(email string) (*model.User, error)
GetByAuth(authData *string, authService string) (*model.User, error)
GetAllUsingAuthService(authService string) ([]*model.User, error)
GetAllNotInAuthService(authServices []string) ([]*model.User, error)
GetByUsername(username string) (*model.User, error)
GetForLogin(loginID string, allowSignInWithUsername, allowSignInWithEmail bool) (*model.User, error)
VerifyEmail(userID, email string) (string, error)
GetEtagForAllProfiles() string
GetEtagForProfiles(teamID string) string
UpdateFailedPasswordAttempts(userID string, attempts int) error
GetSystemAdminProfiles() (map[string]*model.User, error)
PermanentDelete(userID string) error
AnalyticsActiveCount(timestamp int64, options model.UserCountOptions) (int64, error)
AnalyticsActiveCountForPeriod(startTime int64, endTime int64, options model.UserCountOptions) (int64, error)
GetUnreadCount(userID string, isCRTEnabled bool) (int64, error)
GetUnreadCountForChannel(userID string, channelID string) (int64, error)
GetAnyUnreadPostCountForChannel(userID string, channelID string) (int64, error)
GetRecentlyActiveUsersForTeam(teamID string, offset, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error)
GetNewUsersForTeam(teamID string, offset, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error)
Search(teamID string, term string, options *model.UserSearchOptions) ([]*model.User, error)
SearchNotInTeam(notInTeamID string, term string, options *model.UserSearchOptions) ([]*model.User, error)
SearchInChannel(channelID string, term string, options *model.UserSearchOptions) ([]*model.User, error)
SearchNotInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) ([]*model.User, error)
SearchWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, error)
SearchInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error)
SearchNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error)
AnalyticsGetInactiveUsersCount() (int64, error)
AnalyticsGetExternalUsers(hostDomain string) (bool, error)
AnalyticsGetSystemAdminCount() (int64, error)
AnalyticsGetGuestCount() (int64, error)
GetProfilesNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error)
GetEtagForProfilesNotInTeam(teamID string) string
ClearAllCustomRoleAssignments() error
InferSystemInstallDate() (int64, error)
GetAllAfter(limit int, afterID string) ([]*model.User, error)
GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error)
Count(options model.UserCountOptions) (int64, error)
GetTeamGroupUsers(teamID string) ([]*model.User, error)
GetChannelGroupUsers(channelID string) ([]*model.User, error)
PromoteGuestToUser(userID string) error
DemoteUserToGuest(userID string) (*model.User, error)
DeactivateGuests() ([]string, error)
AutocompleteUsersInChannel(teamID, channelID, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error)
GetKnownUsers(userID string) ([]string, error)
IsEmpty(excludeBots bool) (bool, error)
GetUsersWithInvalidEmails(page int, perPage int, restrictedDomains string) ([]*model.User, error)
InsertUsers(users []*model.User) error
GetFirstSystemAdminID() (string, error)
}
type BotStore interface {
Get(userID string, includeDeleted bool) (*model.Bot, error)
GetAll(options *model.BotGetOptions) ([]*model.Bot, error)
Save(bot *model.Bot) (*model.Bot, error)
Update(bot *model.Bot) (*model.Bot, error)
PermanentDelete(userID string) error
}
type SessionStore interface {
Get(ctx context.Context, sessionIDOrToken string) (*model.Session, error)
Save(session *model.Session) (*model.Session, error)
GetSessions(userID string) ([]*model.Session, error)
GetSessionsWithActiveDeviceIds(userID string) ([]*model.Session, error)
GetSessionsExpired(thresholdMillis int64, mobileOnly bool, unnotifiedOnly bool) ([]*model.Session, error)
UpdateExpiredNotify(sessionid string, notified bool) error
Remove(sessionIDOrToken string) error
RemoveAllSessions() error
PermanentDeleteSessionsByUser(teamID string) error
UpdateExpiresAt(sessionID string, timestamp int64) error
UpdateLastActivityAt(sessionID string, timestamp int64) error
UpdateRoles(userID string, roles string) (string, error)
UpdateDeviceId(id string, deviceID string, expiresAt int64) (string, error)
UpdateProps(session *model.Session) error
AnalyticsSessionCount() (int64, error)
Cleanup(expiryTime int64, batchSize int64) error
}
type AuditStore interface {
Save(audit *model.Audit) error
Get(user_id string, offset int, limit int) (model.Audits, error)
PermanentDeleteByUser(userID string) error
}
type ClusterDiscoveryStore interface {
Save(discovery *model.ClusterDiscovery) error
Delete(discovery *model.ClusterDiscovery) (bool, error)
Exists(discovery *model.ClusterDiscovery) (bool, error)
GetAll(discoveryType, clusterName string) ([]*model.ClusterDiscovery, error)
SetLastPingAt(discovery *model.ClusterDiscovery) error
Cleanup() error
}
type RemoteClusterStore interface {
Save(rc *model.RemoteCluster) (*model.RemoteCluster, error)
Update(rc *model.RemoteCluster) (*model.RemoteCluster, error)
Delete(remoteClusterId string) (bool, error)
Get(remoteClusterId string) (*model.RemoteCluster, error)
GetAll(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error)
UpdateTopics(remoteClusterId string, topics string) (*model.RemoteCluster, error)
SetLastPingAt(remoteClusterId string) error
}
type ComplianceStore interface {
Save(compliance *model.Compliance) (*model.Compliance, error)
Update(compliance *model.Compliance) (*model.Compliance, error)
Get(id string) (*model.Compliance, error)
GetAll(offset, limit int) (model.Compliances, error)
ComplianceExport(compliance *model.Compliance, cursor model.ComplianceExportCursor, limit int) ([]*model.CompliancePost, model.ComplianceExportCursor, error)
MessageExport(ctx context.Context, cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error)
}
type OAuthStore interface {
SaveApp(app *model.OAuthApp) (*model.OAuthApp, error)
UpdateApp(app *model.OAuthApp) (*model.OAuthApp, error)
GetApp(id string) (*model.OAuthApp, error)
GetAppByUser(userID string, offset, limit int) ([]*model.OAuthApp, error)
GetApps(offset, limit int) ([]*model.OAuthApp, error)
GetAuthorizedApps(userID string, offset, limit int) ([]*model.OAuthApp, error)
DeleteApp(id string) error
SaveAuthData(authData *model.AuthData) (*model.AuthData, error)
GetAuthData(code string) (*model.AuthData, error)
RemoveAuthData(code string) error
RemoveAuthDataByClientId(clientId string, userId string) error
PermanentDeleteAuthDataByUser(userID string) error
SaveAccessData(accessData *model.AccessData) (*model.AccessData, error)
UpdateAccessData(accessData *model.AccessData) (*model.AccessData, error)
GetAccessData(token string) (*model.AccessData, error)
GetAccessDataByUserForApp(userID, clientId string) ([]*model.AccessData, error)
GetAccessDataByRefreshToken(token string) (*model.AccessData, error)
GetPreviousAccessData(userID, clientId string) (*model.AccessData, error)
RemoveAccessData(token string) error
RemoveAllAccessData() error
}
type SystemStore interface {
Save(system *model.System) error
SaveOrUpdate(system *model.System) error
Update(system *model.System) error
Get() (model.StringMap, error)
GetByName(name string) (*model.System, error)
PermanentDeleteByName(name string) (*model.System, error)
InsertIfExists(system *model.System) (*model.System, error)
SaveOrUpdateWithWarnMetricHandling(system *model.System) error
}
type WebhookStore interface {
SaveIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error)
GetIncoming(id string, allowFromCache bool) (*model.IncomingWebhook, error)
GetIncomingList(offset, limit int) ([]*model.IncomingWebhook, error)
GetIncomingListByUser(userID string, offset, limit int) ([]*model.IncomingWebhook, error)
GetIncomingByTeam(teamID string, offset, limit int) ([]*model.IncomingWebhook, error)
GetIncomingByTeamByUser(teamID string, userID string, offset, limit int) ([]*model.IncomingWebhook, error)
UpdateIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error)
GetIncomingByChannel(channelID string) ([]*model.IncomingWebhook, error)
DeleteIncoming(webhookID string, timestamp int64) error
PermanentDeleteIncomingByChannel(channelID string) error
PermanentDeleteIncomingByUser(userID string) error
SaveOutgoing(webhook *model.OutgoingWebhook) (*model.OutgoingWebhook, error)
GetOutgoing(id string) (*model.OutgoingWebhook, error)
GetOutgoingByChannel(channelID string, offset, limit int) ([]*model.OutgoingWebhook, error)
GetOutgoingByChannelByUser(channelID string, userID string, offset, limit int) ([]*model.OutgoingWebhook, error)
GetOutgoingList(offset, limit int) ([]*model.OutgoingWebhook, error)
GetOutgoingListByUser(userID string, offset, limit int) ([]*model.OutgoingWebhook, error)
GetOutgoingByTeam(teamID string, offset, limit int) ([]*model.OutgoingWebhook, error)
GetOutgoingByTeamByUser(teamID string, userID string, offset, limit int) ([]*model.OutgoingWebhook, error)
DeleteOutgoing(webhookID string, timestamp int64) error
PermanentDeleteOutgoingByChannel(channelID string) error
PermanentDeleteOutgoingByUser(userID string) error
UpdateOutgoing(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, error)
AnalyticsIncomingCount(teamID string) (int64, error)
AnalyticsOutgoingCount(teamID string) (int64, error)
InvalidateWebhookCache(webhook string)
ClearCaches()
}
type CommandStore interface {
Save(webhook *model.Command) (*model.Command, error)
GetByTrigger(teamID string, trigger string) (*model.Command, error)
Get(id string) (*model.Command, error)
GetByTeam(teamID string) ([]*model.Command, error)
Delete(commandID string, timestamp int64) error
PermanentDeleteByTeam(teamID string) error
PermanentDeleteByUser(userID string) error
Update(hook *model.Command) (*model.Command, error)
AnalyticsCommandCount(teamID string) (int64, error)
}
type CommandWebhookStore interface {
Save(webhook *model.CommandWebhook) (*model.CommandWebhook, error)
Get(id string) (*model.CommandWebhook, error)
TryUse(id string, limit int) error
Cleanup()
}
type PreferenceStore interface {
Save(preferences model.Preferences) error
GetCategory(userID string, category string) (model.Preferences, error)
GetCategoryAndName(category string, nane string) (model.Preferences, error)
Get(userID string, category string, name string) (*model.Preference, error)
GetAll(userID string) (model.Preferences, error)
Delete(userID, category, name string) error
DeleteCategory(userID string, category string) error
DeleteCategoryAndName(category string, name string) error
PermanentDeleteByUser(userID string) error
DeleteOrphanedRows(limit int) (deleted int64, err error)
CleanupFlagsBatch(limit int64) (int64, error)
}
type LicenseStore interface {
Save(license *model.LicenseRecord) (*model.LicenseRecord, error)
Get(id string) (*model.LicenseRecord, error)
GetAll() ([]*model.LicenseRecord, error)
}
type TokenStore interface {
Save(recovery *model.Token) error
Delete(token string) error
GetByToken(token string) (*model.Token, error)
Cleanup(expiryTime int64)
GetAllTokensByType(tokenType string) ([]*model.Token, error)
RemoveAllTokensByType(tokenType string) error
}
type EmojiStore interface {
Save(emoji *model.Emoji) (*model.Emoji, error)
Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error)
GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error)
GetMultipleByName(names []string) ([]*model.Emoji, error)
GetList(offset, limit int, sort string) ([]*model.Emoji, error)
Delete(emoji *model.Emoji, timestamp int64) error
Search(name string, prefixOnly bool, limit int) ([]*model.Emoji, error)
}
type StatusStore interface {
SaveOrUpdate(status *model.Status) error
Get(userID string) (*model.Status, error)
GetByIds(userIds []string) ([]*model.Status, error)
ResetAll() error
GetTotalActiveUsersCount() (int64, error)
UpdateLastActivityAt(userID string, lastActivityAt int64) error
UpdateExpiredDNDStatuses() ([]*model.Status, error)
}
type FileInfoStore interface {
Save(info *model.FileInfo) (*model.FileInfo, error)
Upsert(info *model.FileInfo) (*model.FileInfo, error)
Get(id string) (*model.FileInfo, error)
GetFromMaster(id string) (*model.FileInfo, error)
GetByIds(ids []string) ([]*model.FileInfo, error)
GetByPath(path string) (*model.FileInfo, error)
GetForPost(postID string, readFromMaster, includeDeleted, allowFromCache bool) ([]*model.FileInfo, error)
GetForUser(userID string) ([]*model.FileInfo, error)
GetWithOptions(page, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, error)
InvalidateFileInfosForPostCache(postID string, deleted bool)
AttachToPost(fileID string, postID string, channelID, creatorID string) error
DeleteForPost(postID string) (string, error)
PermanentDelete(fileID string) error
PermanentDeleteBatch(endTime int64, limit int64) (int64, error)
PermanentDeleteByUser(userID string) (int64, error)
SetContent(fileID, content string) error
Search(paramsList []*model.SearchParams, userID, teamID string, page, perPage int) (*model.FileInfoList, error)
CountAll() (int64, error)
GetFilesBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.FileForIndexing, error)
ClearCaches()
GetStorageUsage(allowFromCache, includeDeleted bool) (int64, error)
// GetUptoNSizeFileTime returns the CreateAt time of the last accessible file with a running-total size upto n bytes.
GetUptoNSizeFileTime(n int64) (int64, error)
}
type UploadSessionStore interface {
Save(session *model.UploadSession) (*model.UploadSession, error)
Update(session *model.UploadSession) error
Get(ctx context.Context, id string) (*model.UploadSession, error)
GetForUser(userID string) ([]*model.UploadSession, error)
Delete(id string) error
}
type ReactionStore interface {
Save(reaction *model.Reaction) (*model.Reaction, error)
Delete(reaction *model.Reaction) (*model.Reaction, error)
GetForPost(postID string, allowFromCache bool) ([]*model.Reaction, error)
GetForPostSince(postId string, since int64, excludeRemoteId string, inclDeleted bool) ([]*model.Reaction, error)
DeleteAllWithEmojiName(emojiName string) error
BulkGetForPosts(postIds []string) ([]*model.Reaction, error)
DeleteOrphanedRows(limit int) (int64, error)
PermanentDeleteBatch(endTime int64, limit int64) (int64, error)
GetTopForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopReactionList, error)
GetTopForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopReactionList, error)
}
type JobStore interface {
Save(job *model.Job) (*model.Job, error)
UpdateOptimistically(job *model.Job, currentStatus string) (bool, error)
UpdateStatus(id string, status string) (*model.Job, error)
UpdateStatusOptimistically(id string, currentStatus string, newStatus string) (bool, error)
Get(id string) (*model.Job, error)
GetAllPage(offset int, limit int) ([]*model.Job, error)
GetAllByType(jobType string) ([]*model.Job, error)
GetAllByTypeAndStatus(jobType string, status string) ([]*model.Job, error)
GetAllByTypePage(jobType string, offset int, limit int) ([]*model.Job, error)
GetAllByTypesPage(jobTypes []string, offset int, limit int) ([]*model.Job, error)
GetAllByStatus(status string) ([]*model.Job, error)
GetNewestJobByStatusAndType(status string, jobType string) (*model.Job, error)
GetNewestJobByStatusesAndType(statuses []string, jobType string) (*model.Job, error)
GetCountByStatusAndType(status string, jobType string) (int64, error)
Delete(id string) (string, error)
Cleanup(expiryTime int64, batchSize int) error
}
type UserAccessTokenStore interface {
Save(token *model.UserAccessToken) (*model.UserAccessToken, error)
DeleteAllForUser(userID string) error
Delete(tokenID string) error
Get(tokenID string) (*model.UserAccessToken, error)
GetAll(offset int, limit int) ([]*model.UserAccessToken, error)
GetByToken(tokenString string) (*model.UserAccessToken, error)
GetByUser(userID string, page, perPage int) ([]*model.UserAccessToken, error)
Search(term string) ([]*model.UserAccessToken, error)
UpdateTokenEnable(tokenID string) error
UpdateTokenDisable(tokenID string) error
}
type PluginStore interface {
SaveOrUpdate(keyVal *model.PluginKeyValue) (*model.PluginKeyValue, error)
CompareAndSet(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error)
CompareAndDelete(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error)
SetWithOptions(pluginID string, key string, value []byte, options model.PluginKVSetOptions) (bool, error)
Get(pluginID, key string) (*model.PluginKeyValue, error)
Delete(pluginID, key string) error
DeleteAllForPlugin(PluginID string) error
DeleteAllExpired() error
List(pluginID string, page, perPage int) ([]string, error)
}
type RoleStore interface {
Save(role *model.Role) (*model.Role, error)
Get(roleID string) (*model.Role, error)
GetAll() ([]*model.Role, error)
GetByName(ctx context.Context, name string) (*model.Role, error)
GetByNames(names []string) ([]*model.Role, error)
Delete(roleID string) (*model.Role, error)
PermanentDeleteAll() error
// HigherScopedPermissions retrieves the higher-scoped permissions of a list of role names. The higher-scope
// (either team scheme or system scheme) is determined based on whether the team has a scheme or not.
ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, error)
// AllChannelSchemeRoles returns all of the roles associated to channel schemes.
AllChannelSchemeRoles() ([]*model.Role, error)
// ChannelRolesUnderTeamRole returns all of the non-deleted roles that are affected by updates to the
// given role.
ChannelRolesUnderTeamRole(roleName string) ([]*model.Role, error)
}
type SchemeStore interface {
Save(scheme *model.Scheme) (*model.Scheme, error)
Get(schemeID string) (*model.Scheme, error)
GetByName(schemeName string) (*model.Scheme, error)
GetAllPage(scope string, offset int, limit int) ([]*model.Scheme, error)
Delete(schemeID string) (*model.Scheme, error)
PermanentDeleteAll() error
CountByScope(scope string) (int64, error)
CountWithoutPermission(scope, permissionID string, roleScope model.RoleScope, roleType model.RoleType) (int64, error)
}
type TermsOfServiceStore interface {
Save(termsOfService *model.TermsOfService) (*model.TermsOfService, error)
GetLatest(allowFromCache bool) (*model.TermsOfService, error)
Get(id string, allowFromCache bool) (*model.TermsOfService, error)
}
type ProductNoticesStore interface {
View(userID string, notices []string) error
Clear(notices []string) error
ClearOldNotices(currentNotices model.ProductNotices) error
GetViews(userID string) ([]model.ProductNoticeViewState, error)
}
type UserTermsOfServiceStore interface {
GetByUser(userID string) (*model.UserTermsOfService, error)
Save(userTermsOfService *model.UserTermsOfService) (*model.UserTermsOfService, error)
Delete(userID, termsOfServiceId string) error
}
type GroupStore interface {
Create(group *model.Group) (*model.Group, error)
CreateWithUserIds(group *model.GroupWithUserIds) (*model.Group, error)
Get(groupID string) (*model.Group, error)
GetByName(name string, opts model.GroupSearchOpts) (*model.Group, error)
GetByIDs(groupIDs []string) ([]*model.Group, error)
GetByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, error)
GetAllBySource(groupSource model.GroupSource) ([]*model.Group, error)
GetByUser(userID string) ([]*model.Group, error)
Update(group *model.Group) (*model.Group, error)
Delete(groupID string) (*model.Group, error)
Restore(groupID string) (*model.Group, error)
GetMemberUsers(groupID string) ([]*model.User, error)
GetMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error)
GetMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, error)
GetMemberCountWithRestrictions(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, error)
GetMemberCount(groupID string) (int64, error)
GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error)
GetMemberUsersInTeam(groupID string, teamID string) ([]*model.User, error)
GetMemberUsersNotInChannel(groupID string, channelID string) ([]*model.User, error)
UpsertMember(groupID string, userID string) (*model.GroupMember, error)
DeleteMember(groupID string, userID string) (*model.GroupMember, error)
PermanentDeleteMembersByUser(userID string) error
CreateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error)
GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error)
GetAllGroupSyncablesByGroupId(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, error)
UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error)
DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error)
// TeamMembersToAdd returns a slice of UserTeamIDPair that need newly created memberships
// based on the groups configurations. The returned list can be optionally scoped to a single given team.
//
// Typically since will be the last successful group sync time.
// If includeRemovedMembers is true, then team members who left or were removed from the team will
// be included; otherwise, they will be excluded.
TeamMembersToAdd(since int64, teamID *string, includeRemovedMembers bool) ([]*model.UserTeamIDPair, error)
// ChannelMembersToAdd returns a slice of UserChannelIDPair that need newly created memberships
// based on the groups configurations. The returned list can be optionally scoped to a single given channel.
//
// Typically since will be the last successful group sync time.
// If includeRemovedMembers is true, then channel members who left or were removed from the channel will
// be included; otherwise, they will be excluded.
ChannelMembersToAdd(since int64, channelID *string, includeRemovedMembers bool) ([]*model.UserChannelIDPair, error)
// TeamMembersToRemove returns all team members that should be removed based on group constraints.
TeamMembersToRemove(teamID *string) ([]*model.TeamMember, error)
// ChannelMembersToRemove returns all channel members that should be removed based on group constraints.
ChannelMembersToRemove(channelID *string) ([]*model.ChannelMember, error)
GetGroupsByChannel(channelID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error)
CountGroupsByChannel(channelID string, opts model.GroupSearchOpts) (int64, error)
GetGroupsByTeam(teamID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error)
GetGroupsAssociatedToChannelsByTeam(teamID string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, error)
CountGroupsByTeam(teamID string, opts model.GroupSearchOpts) (int64, error)
GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error)
TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, error)
CountTeamMembersMinusGroupMembers(teamID string, groupIDs []string) (int64, error)
ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, error)
CountChannelMembersMinusGroupMembers(channelID string, groupIDs []string) (int64, error)
// AdminRoleGroupsForSyncableMember returns the IDs of all of the groups that the user is a member of that are
// configured as SchemeAdmin: true for the given syncable.
AdminRoleGroupsForSyncableMember(userID, syncableID string, syncableType model.GroupSyncableType) ([]string, error)
// PermittedSyncableAdmins returns the IDs of all of the user who are permitted by the group syncable to have
// the admin role for the given syncable.
PermittedSyncableAdmins(syncableID string, syncableType model.GroupSyncableType) ([]string, error)
// GroupCount returns the total count of records in the UserGroups table.
GroupCount() (int64, error)
GroupCountBySource(source model.GroupSource) (int64, error)
// GroupTeamCount returns the total count of records in the GroupTeams table.
GroupTeamCount() (int64, error)
// GroupChannelCount returns the total count of records in the GroupChannels table.
GroupChannelCount() (int64, error)
// GroupMemberCount returns the total count of records in the GroupMembers table.
GroupMemberCount() (int64, error)
// DistinctGroupMemberCount returns the count of records in the GroupMembers table with distinct userID values.
DistinctGroupMemberCount() (int64, error)
DistinctGroupMemberCountForSource(source model.GroupSource) (int64, error)
// GroupCountWithAllowReference returns the count of records in the Groups table with AllowReference set to true.
GroupCountWithAllowReference() (int64, error)
UpsertMembers(groupID string, userIDs []string) ([]*model.GroupMember, error)
DeleteMembers(groupID string, userIDs []string) ([]*model.GroupMember, error)
GetMember(groupID string, userID string) (*model.GroupMember, error)
}
type LinkMetadataStore interface {
Save(linkMetadata *model.LinkMetadata) (*model.LinkMetadata, error)
Get(url string, timestamp int64) (*model.LinkMetadata, error)
}
type NotifyAdminStore interface {
Save(data *model.NotifyAdminData) (*model.NotifyAdminData, error)
GetDataByUserIdAndFeature(userId string, feature model.MattermostFeature) ([]*model.NotifyAdminData, error)
Get(trial bool) ([]*model.NotifyAdminData, error)
DeleteBefore(trial bool, now int64) error
Update(userId string, requiredPlan string, requiredFeature model.MattermostFeature, now int64) error
}
type SharedChannelStore interface {
Save(sc *model.SharedChannel) (*model.SharedChannel, error)
Get(channelId string) (*model.SharedChannel, error)
HasChannel(channelID string) (bool, error)
GetAll(offset, limit int, opts model.SharedChannelFilterOpts) ([]*model.SharedChannel, error)
GetAllCount(opts model.SharedChannelFilterOpts) (int64, error)
Update(sc *model.SharedChannel) (*model.SharedChannel, error)
Delete(channelId string) (bool, error)
SaveRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error)
UpdateRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error)
GetRemote(id string) (*model.SharedChannelRemote, error)
HasRemote(channelID string, remoteId string) (bool, error)
GetRemoteForUser(remoteId string, userId string) (*model.RemoteCluster, error)
GetRemoteByIds(channelId string, remoteId string) (*model.SharedChannelRemote, error)
GetRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error)
UpdateRemoteCursor(id string, cursor model.GetPostsSinceForSyncCursor) error
DeleteRemote(remoteId string) (bool, error)
GetRemotesStatus(channelId string) ([]*model.SharedChannelRemoteStatus, error)
SaveUser(remote *model.SharedChannelUser) (*model.SharedChannelUser, error)
GetSingleUser(userID string, channelID string, remoteID string) (*model.SharedChannelUser, error)
GetUsersForUser(userID string) ([]*model.SharedChannelUser, error)
GetUsersForSync(filter model.GetUsersForSyncFilter) ([]*model.User, error)
UpdateUserLastSyncAt(userID string, channelID string, remoteID string) error
SaveAttachment(remote *model.SharedChannelAttachment) (*model.SharedChannelAttachment, error)
UpsertAttachment(remote *model.SharedChannelAttachment) (string, error)
GetAttachment(fileId string, remoteId string) (*model.SharedChannelAttachment, error)
UpdateAttachmentLastSyncAt(id string, syncTime int64) error
}
type PostPriorityStore interface {
GetForPost(postId string) (*model.PostPriority, error)
GetForPosts(ids []string) ([]*model.PostPriority, error)
}
type DraftStore interface {
Save(d *model.Draft) (*model.Draft, error)
Get(userID, channelID, rootID string, includeDeleted bool) (*model.Draft, error)
Delete(userID, channelID, rootID string) error
GetDraftsForUser(userID, teamID string) ([]*model.Draft, error)
Update(d *model.Draft) (*model.Draft, error)
}
type PostAcknowledgementStore interface {
Get(postID, userID string) (*model.PostAcknowledgement, error)
GetForPost(postID string) ([]*model.PostAcknowledgement, error)
GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error)
Save(postID, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error)
Delete(acknowledgement *model.PostAcknowledgement) error
}
type TrueUpReviewStore interface {
GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error)
CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error)
Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error)
}
// ChannelSearchOpts contains options for searching channels.
//
// NotAssociatedToGroup will exclude channels that have associated, active GroupChannels records.
// IncludeDeleted will include channel records where DeleteAt != 0.
// ExcludeChannelNames will exclude channels from the results by name.
// IncludeSearchById will include searching matches against channel IDs in the results
// Paginate whether to paginate the results.
// Page page requested, if results are paginated.
// PerPage number of results per page, if paginated.
type ChannelSearchOpts struct {
Term string
NotAssociatedToGroup string
IncludeDeleted bool
Deleted bool
ExcludeChannelNames []string
TeamIds []string
GroupConstrained bool
ExcludeGroupConstrained bool
PolicyID string
ExcludePolicyConstrained bool
IncludePolicyID bool
IncludeTeamInfo bool
IncludeSearchById bool
CountOnly bool
Public bool
Private bool
Page *int
PerPage *int
LastDeleteAt int
LastUpdateAt int
}
func (c *ChannelSearchOpts) IsPaginated() bool {
return c.Page != nil && c.PerPage != nil
}
type UserGetByIdsOpts struct {
// IsAdmin tracks whether or not the request is being made by an administrator. Does nothing when provided by a client.
IsAdmin bool
// Restrict to search in a list of teams and channels. Does nothing when provided by a client.
ViewRestrictions *model.ViewUsersRestrictions
// Since filters the users based on their UpdateAt timestamp.
Since int64
}
// ThreadMembershipOpts defines some properties to be passed to
// ThreadStore.MaintainMembership()
type ThreadMembershipOpts struct {
// Following indicates whether or not the user is following the thread.
Following bool
// IncrementMentions indicates whether or not the mentions count for
// the thread should be incremented.
IncrementMentions bool
// UpdateFollowing indicates whether or not the following state should be changed.
UpdateFollowing bool
// UpdateViewedTimestamp indicates whether or not the LastViewed field of the
// membership should be updated.
UpdateViewedTimestamp bool
// UpdateParticipants indicates whether or not the thread's participants list
// should be updated.
UpdateParticipants bool
}
// ChannelMemberGraphQLSearchOpts contains the options for a graphQL query
// to get the channel members.
type ChannelMemberGraphQLSearchOpts struct {
AfterChannel string
AfterUser string
Limit int
LastUpdateAt int
ExcludeTeam bool
}
// PostReminderMetadata contains some info needed to send
// the reminder message to the user.
type PostReminderMetadata struct {
ChannelId string
TeamName string
UserLocale string
Username string
}
// SidebarCategorySearchOpts contains the options for a graphQL query
// to get the sidebar categories.
type SidebarCategorySearchOpts struct {
TeamID string
ExcludeTeam bool
}
// Ensure store service adapter implements `product.StoreService`
var _ product.StoreService = (*StoreServiceAdapter)(nil)
// StoreServiceAdapter provides a simple Store wrapper for use with products.
type StoreServiceAdapter struct {
store Store
}
func NewStoreServiceAdapter(store Store) *StoreServiceAdapter {
return &StoreServiceAdapter{
store: store,
}
}
func (a *StoreServiceAdapter) GetMasterDB() *sql.DB {
return a.store.GetInternalMasterDB()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestAuditStore(t *testing.T, ss store.Store) {
t.Run("", func(t *testing.T) { testAuditStore(t, ss) })
}
func testAuditStore(t *testing.T, ss store.Store) {
audit := &model.Audit{UserId: model.NewId(), IpAddress: "ipaddress", Action: "Action"}
require.NoError(t, ss.Audit().Save(audit))
time.Sleep(100 * time.Millisecond)
require.NoError(t, ss.Audit().Save(audit))
time.Sleep(100 * time.Millisecond)
require.NoError(t, ss.Audit().Save(audit))
time.Sleep(100 * time.Millisecond)
audit.ExtraInfo = "extra"
time.Sleep(100 * time.Millisecond)
require.NoError(t, ss.Audit().Save(audit))
time.Sleep(100 * time.Millisecond)
audits, err := ss.Audit().Get(audit.UserId, 0, 100)
require.NoError(t, err)
assert.Len(t, audits, 4)
assert.Equal(t, "extra", audits[0].ExtraInfo)
audits, err = ss.Audit().Get("missing", 0, 100)
require.NoError(t, err)
assert.Empty(t, audits)
audits, err = ss.Audit().Get("", 0, 100)
require.NoError(t, err)
require.Len(t, audits, 4, "Failed to save and retrieve 4 audit logs")
require.NoError(t, ss.Audit().PermanentDeleteByUser(audit.UserId))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func makeBotWithUser(t *testing.T, ss store.Store, bot *model.Bot) (*model.Bot, *model.User) {
user, err := ss.User().Save(model.UserFromBot(bot))
require.NoError(t, err)
bot.UserId = user.Id
bot, nErr := ss.Bot().Save(bot)
require.NoError(t, nErr)
return bot, user
}
func TestBotStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("Get", func(t *testing.T) { testBotStoreGet(t, ss, s) })
t.Run("GetAll", func(t *testing.T) { testBotStoreGetAll(t, ss, s) })
t.Run("Save", func(t *testing.T) { testBotStoreSave(t, ss) })
t.Run("Update", func(t *testing.T) { testBotStoreUpdate(t, ss) })
t.Run("PermanentDelete", func(t *testing.T) { testBotStorePermanentDelete(t, ss) })
}
func testBotStoreGet(t *testing.T, ss store.Store, s SqlStore) {
deletedBot, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "deleted_bot",
Description: "A deleted bot",
OwnerId: model.NewId(),
LastIconUpdate: model.GetMillis(),
})
deletedBot.DeleteAt = 1
deletedBot, err := ss.Bot().Update(deletedBot)
require.NoError(t, err)
defer func() { require.NoError(t, ss.Bot().PermanentDelete(deletedBot.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(deletedBot.UserId)) }()
permanentlyDeletedBot, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "permanently_deleted_bot",
Description: "A permanently deleted bot",
OwnerId: model.NewId(),
LastIconUpdate: model.GetMillis(),
DeleteAt: 0,
})
require.NoError(t, ss.Bot().PermanentDelete(permanentlyDeletedBot.UserId))
defer func() { require.NoError(t, ss.User().PermanentDelete(permanentlyDeletedBot.UserId)) }()
b1, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "b1",
Description: "The first bot",
OwnerId: model.NewId(),
LastIconUpdate: model.GetMillis(),
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(b1.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(b1.UserId)) }()
b2, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "b2",
Description: "The second bot",
OwnerId: model.NewId(),
LastIconUpdate: 0,
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(b2.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(b2.UserId)) }()
// Artificially set b2.LastIconUpdate to NULL to verify handling of same.
_, sqlErr := s.GetMasterX().Exec("UPDATE Bots SET LastIconUpdate = NULL WHERE UserId = '" + b2.UserId + "'")
require.NoError(t, sqlErr)
t.Run("get non-existent bot", func(t *testing.T) {
_, err := ss.Bot().Get("unknown", false)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
})
t.Run("get deleted bot", func(t *testing.T) {
_, err := ss.Bot().Get(deletedBot.UserId, false)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
})
t.Run("get deleted bot, include deleted", func(t *testing.T) {
bot, err := ss.Bot().Get(deletedBot.UserId, true)
require.NoError(t, err)
require.Equal(t, deletedBot, bot)
})
t.Run("get permanently deleted bot", func(t *testing.T) {
_, err := ss.Bot().Get(permanentlyDeletedBot.UserId, false)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
})
t.Run("get bot 1", func(t *testing.T) {
bot, err := ss.Bot().Get(b1.UserId, false)
require.NoError(t, err)
require.Equal(t, b1, bot)
})
t.Run("get bot 2", func(t *testing.T) {
bot, err := ss.Bot().Get(b2.UserId, false)
require.NoError(t, err)
require.Equal(t, b2, bot)
})
}
func testBotStoreGetAll(t *testing.T, ss store.Store, s SqlStore) {
OwnerId1 := model.NewId()
OwnerId2 := model.NewId()
deletedBot, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "deleted_bot",
Description: "A deleted bot",
OwnerId: OwnerId1,
LastIconUpdate: model.GetMillis(),
})
deletedBot.DeleteAt = 1
deletedBot, err := ss.Bot().Update(deletedBot)
require.NoError(t, err)
defer func() { require.NoError(t, ss.Bot().PermanentDelete(deletedBot.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(deletedBot.UserId)) }()
permanentlyDeletedBot, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "permanently_deleted_bot",
Description: "A permanently deleted bot",
OwnerId: OwnerId1,
LastIconUpdate: model.GetMillis(),
DeleteAt: 0,
})
require.NoError(t, ss.Bot().PermanentDelete(permanentlyDeletedBot.UserId))
defer func() { require.NoError(t, ss.User().PermanentDelete(permanentlyDeletedBot.UserId)) }()
b1, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "b1",
Description: "The first bot",
OwnerId: OwnerId1,
LastIconUpdate: model.GetMillis(),
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(b1.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(b1.UserId)) }()
b2, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "b2",
Description: "The second bot",
OwnerId: OwnerId1,
LastIconUpdate: 0,
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(b2.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(b2.UserId)) }()
// Artificially set b2.LastIconUpdate to NULL to verify handling of same.
_, sqlErr := s.GetMasterX().Exec("UPDATE Bots SET LastIconUpdate = NULL WHERE UserId = '" + b2.UserId + "'")
require.NoError(t, sqlErr)
t.Run("get original bots", func(t *testing.T) {
bot, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
b1,
b2,
}, bot)
})
b3, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "b3",
Description: "The third bot",
OwnerId: OwnerId1,
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(b3.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(b3.UserId)) }()
b4, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "b4",
Description: "The fourth bot",
OwnerId: OwnerId2,
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(b4.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(b4.UserId)) }()
deletedUser := model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
_, err1 := ss.User().Save(&deletedUser)
require.NoError(t, err1, "couldn't save user")
deletedUser.DeleteAt = model.GetMillis()
_, err2 := ss.User().Update(&deletedUser, true)
require.NoError(t, err2, "couldn't delete user")
defer func() { require.NoError(t, ss.User().PermanentDelete(deletedUser.Id)) }()
ob5, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "ob5",
Description: "Orphaned bot 5",
OwnerId: deletedUser.Id,
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(b4.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(b4.UserId)) }()
t.Run("get newly created bot stoo", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
b1,
b2,
b3,
b4,
ob5,
}, bots)
})
t.Run("get orphaned", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10, OnlyOrphaned: true})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
ob5,
}, bots)
})
t.Run("get page=0, per_page=2", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 2})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
b1,
b2,
}, bots)
})
t.Run("get page=1, limit=2", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 1, PerPage: 2})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
b3,
b4,
}, bots)
})
t.Run("get page=5, perpage=1000", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 5, PerPage: 1000})
require.NoError(t, err)
require.Equal(t, []*model.Bot{}, bots)
})
t.Run("get offset=0, limit=2, include deleted", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 2, IncludeDeleted: true})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
deletedBot,
b1,
}, bots)
})
t.Run("get offset=2, limit=2, include deleted", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 1, PerPage: 2, IncludeDeleted: true})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
b2,
b3,
}, bots)
})
t.Run("get offset=0, limit=10, creator id 1", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10, OwnerId: OwnerId1})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
b1,
b2,
b3,
}, bots)
})
t.Run("get offset=0, limit=10, creator id 2", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10, OwnerId: OwnerId2})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
b4,
}, bots)
})
t.Run("get offset=0, limit=10, include deleted, creator id 1", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10, IncludeDeleted: true, OwnerId: OwnerId1})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
deletedBot,
b1,
b2,
b3,
}, bots)
})
t.Run("get offset=0, limit=10, include deleted, creator id 2", func(t *testing.T) {
bots, err := ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10, IncludeDeleted: true, OwnerId: OwnerId2})
require.NoError(t, err)
require.Equal(t, []*model.Bot{
b4,
}, bots)
})
}
func testBotStoreSave(t *testing.T, ss store.Store) {
t.Run("invalid bot", func(t *testing.T) {
bot := &model.Bot{
UserId: model.NewId(),
Username: "invalid bot",
Description: "description",
}
_, err := ss.Bot().Save(bot)
require.Error(t, err)
var appErr *model.AppError
require.True(t, errors.As(err, &appErr))
// require.Equal(t, "model.bot.is_valid.username.app_error", err.Id)
})
t.Run("normal bot", func(t *testing.T) {
bot := &model.Bot{
Username: "normal_bot",
Description: "description",
OwnerId: model.NewId(),
}
user, err := ss.User().Save(model.UserFromBot(bot))
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
bot.UserId = user.Id
returnedNewBot, nErr := ss.Bot().Save(bot)
require.NoError(t, nErr)
defer func() { require.NoError(t, ss.Bot().PermanentDelete(bot.UserId)) }()
// Verify the returned bot matches the saved bot, modulo expected changes
require.NotEqual(t, 0, returnedNewBot.CreateAt)
require.NotEqual(t, 0, returnedNewBot.UpdateAt)
require.Equal(t, returnedNewBot.CreateAt, returnedNewBot.UpdateAt)
bot.UserId = returnedNewBot.UserId
bot.CreateAt = returnedNewBot.CreateAt
bot.UpdateAt = returnedNewBot.UpdateAt
bot.DeleteAt = 0
require.Equal(t, bot, returnedNewBot)
// Verify the actual bot in the database matches the saved bot.
actualNewBot, nErr := ss.Bot().Get(bot.UserId, false)
require.NoError(t, nErr)
require.Equal(t, bot, actualNewBot)
})
}
func testBotStoreUpdate(t *testing.T, ss store.Store) {
t.Run("invalid bot should fail to update", func(t *testing.T) {
existingBot, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "existing_bot",
OwnerId: model.NewId(),
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(existingBot.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(existingBot.UserId)) }()
bot := existingBot.Clone()
bot.Username = "invalid username"
_, err := ss.Bot().Update(bot)
require.Error(t, err)
var appErr *model.AppError
require.True(t, errors.As(err, &appErr))
require.Equal(t, "model.bot.is_valid.username.app_error", appErr.Id)
})
t.Run("existing bot should update", func(t *testing.T) {
existingBot, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "existing_bot",
OwnerId: model.NewId(),
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(existingBot.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(existingBot.UserId)) }()
bot := existingBot.Clone()
bot.OwnerId = model.NewId()
bot.Description = "updated description"
bot.CreateAt = 999999 // Ignored
bot.UpdateAt = 999999 // Ignored
bot.LastIconUpdate = 100000 // Allowed
bot.DeleteAt = 100000 // Allowed
returnedBot, err := ss.Bot().Update(bot)
require.NoError(t, err)
// Verify the returned bot matches the updated bot, modulo expected timestamp changes
require.Equal(t, existingBot.CreateAt, returnedBot.CreateAt)
require.NotEqual(t, bot.UpdateAt, returnedBot.UpdateAt, "update should have advanced UpdateAt")
require.True(t, returnedBot.UpdateAt > bot.UpdateAt, "update should have advanced UpdateAt")
require.NotEqual(t, 99999, returnedBot.UpdateAt, "should have ignored user-provided UpdateAt")
require.Equal(t, bot.LastIconUpdate, returnedBot.LastIconUpdate, "should have marked icon as updated")
require.Equal(t, bot.DeleteAt, returnedBot.DeleteAt, "should have marked bot as deleted")
bot.CreateAt = returnedBot.CreateAt
bot.UpdateAt = returnedBot.UpdateAt
// Verify the actual (now deleted) bot in the database
actualBot, err := ss.Bot().Get(bot.UserId, true)
require.NoError(t, err)
require.Equal(t, bot, actualBot)
})
t.Run("deleted bot should update, restoring", func(t *testing.T) {
existingBot, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "existing_bot",
OwnerId: model.NewId(),
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(existingBot.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(existingBot.UserId)) }()
existingBot.DeleteAt = 100000
existingBot, err := ss.Bot().Update(existingBot)
require.NoError(t, err)
bot := existingBot.Clone()
bot.DeleteAt = 0
returnedBot, err := ss.Bot().Update(bot)
require.NoError(t, err)
// Verify the returned bot matches the updated bot, modulo expected timestamp changes
require.EqualValues(t, 0, returnedBot.DeleteAt)
bot.UpdateAt = returnedBot.UpdateAt
// Verify the actual bot in the database
actualBot, err := ss.Bot().Get(bot.UserId, false)
require.NoError(t, err)
require.Equal(t, bot, actualBot)
})
}
func testBotStorePermanentDelete(t *testing.T, ss store.Store) {
b1, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "b1",
OwnerId: model.NewId(),
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(b1.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(b1.UserId)) }()
b2, _ := makeBotWithUser(t, ss, &model.Bot{
Username: "b2",
OwnerId: model.NewId(),
})
defer func() { require.NoError(t, ss.Bot().PermanentDelete(b2.UserId)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(b2.UserId)) }()
t.Run("permanently delete a non-existent bot", func(t *testing.T) {
err := ss.Bot().PermanentDelete("unknown")
require.NoError(t, err)
})
t.Run("permanently delete bot", func(t *testing.T) {
err := ss.Bot().PermanentDelete(b1.UserId)
require.NoError(t, err)
_, err = ss.Bot().Get(b1.UserId, false)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"math"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestChannelMemberHistoryStore(t *testing.T, ss store.Store) {
t.Run("TestLogJoinEvent", func(t *testing.T) { testLogJoinEvent(t, ss) })
t.Run("TestLogLeaveEvent", func(t *testing.T) { testLogLeaveEvent(t, ss) })
t.Run("TestGetUsersInChannelAtChannelMemberHistory", func(t *testing.T) { testGetUsersInChannelAtChannelMemberHistory(t, ss) })
t.Run("TestGetUsersInChannelAtChannelMembers", func(t *testing.T) { testGetUsersInChannelAtChannelMembers(t, ss) })
t.Run("TestPermanentDeleteBatch", func(t *testing.T) { testPermanentDeleteBatch(t, ss) })
t.Run("TestPermanentDeleteBatchForRetentionPolicies", func(t *testing.T) { testPermanentDeleteBatchForRetentionPolicies(t, ss) })
t.Run("TestGetChannelsLeftSince", func(t *testing.T) { testGetChannelsLeftSince(t, ss) })
}
func testLogJoinEvent(t *testing.T, ss store.Store) {
// create a test channel
ch := model.Channel{
TeamId: model.NewId(),
DisplayName: "Display " + model.NewId(),
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
channel, err := ss.Channel().Save(&ch, -1)
require.NoError(t, err)
// and a test user
user := model.User{
Email: MakeEmail(),
Nickname: model.NewId(),
Username: model.NewId(),
}
userPtr, err := ss.User().Save(&user)
require.NoError(t, err)
user = *userPtr
// log a join event
err = ss.ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis())
assert.NoError(t, err)
}
func testLogLeaveEvent(t *testing.T, ss store.Store) {
// create a test channel
ch := model.Channel{
TeamId: model.NewId(),
DisplayName: "Display " + model.NewId(),
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
channel, err := ss.Channel().Save(&ch, -1)
require.NoError(t, err)
// and a test user
user := model.User{
Email: MakeEmail(),
Nickname: model.NewId(),
Username: model.NewId(),
}
userPtr, err := ss.User().Save(&user)
require.NoError(t, err)
user = *userPtr
// log a join event, followed by a leave event
err = ss.ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis())
assert.NoError(t, err)
err = ss.ChannelMemberHistory().LogLeaveEvent(user.Id, channel.Id, model.GetMillis())
assert.NoError(t, err)
}
func testGetUsersInChannelAtChannelMemberHistory(t *testing.T, ss store.Store) {
// create a test channel
ch := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Display " + model.NewId(),
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
channel, err := ss.Channel().Save(ch, -1)
require.NoError(t, err)
// and a test user
user := model.User{
Email: MakeEmail(),
Nickname: model.NewId(),
Username: model.NewId(),
}
userPtr, err := ss.User().Save(&user)
require.NoError(t, err)
user = *userPtr
// the user was previously in the channel a long time ago, before the export period starts
// the existence of this record makes it look like the MessageExport feature has been active for awhile, and prevents
// us from looking in the ChannelMembers table for data that isn't found in the ChannelMemberHistory table
leaveTime := model.GetMillis() - 20000
joinTime := leaveTime - 10000
err = ss.ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, joinTime)
require.NoError(t, err)
err = ss.ChannelMemberHistory().LogLeaveEvent(user.Id, channel.Id, leaveTime)
require.NoError(t, err)
// log a join event
leaveTime = model.GetMillis()
joinTime = leaveTime - 10000
err = ss.ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, joinTime)
require.NoError(t, err)
// case 1: user joins and leaves the channel before the export period begins
channelMembers, err := ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-500, joinTime-100, channel.Id)
require.NoError(t, err)
assert.Empty(t, channelMembers)
// case 2: user joins the channel after the export period begins, but has not yet left the channel when the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-100, joinTime+500, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime, channelMembers[0].JoinTime)
assert.Nil(t, channelMembers[0].LeaveTime)
// case 3: user joins the channel before the export period begins, but has not yet left the channel when the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+100, joinTime+500, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime, channelMembers[0].JoinTime)
assert.Nil(t, channelMembers[0].LeaveTime)
// add a leave time for the user
err = ss.ChannelMemberHistory().LogLeaveEvent(user.Id, channel.Id, leaveTime)
require.NoError(t, err)
// case 4: user joins the channel before the export period begins, but has not yet left the channel when the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+100, leaveTime-100, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime, channelMembers[0].JoinTime)
assert.Equal(t, leaveTime, *channelMembers[0].LeaveTime)
// case 5: user joins the channel after the export period begins, and leaves the channel before the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-100, leaveTime+100, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime, channelMembers[0].JoinTime)
assert.Equal(t, leaveTime, *channelMembers[0].LeaveTime)
// case 6: user has joined and left the channel long before the export period begins
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(leaveTime+100, leaveTime+200, channel.Id)
require.NoError(t, err)
assert.Empty(t, channelMembers)
}
func testGetUsersInChannelAtChannelMembers(t *testing.T, ss store.Store) {
// create a test channel
channel := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Display " + model.NewId(),
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
channel, err := ss.Channel().Save(channel, -1)
require.NoError(t, err)
// and a test user
user := model.User{
Email: MakeEmail(),
Nickname: model.NewId(),
Username: model.NewId(),
}
userPtr, err := ss.User().Save(&user)
require.NoError(t, err)
user = *userPtr
// clear any existing ChannelMemberHistory data that might interfere with our test
var tableDataTruncated = false
for !tableDataTruncated {
var count int64
count, _, err = ss.ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies(
0, model.GetMillis(), 1000, model.RetentionPolicyCursor{})
require.NoError(t, err, "Failed to truncate ChannelMemberHistory contents")
tableDataTruncated = count == int64(0)
}
// in this test, we're pretending that Message Export was not activated during the export period, so there's no data
// available in the ChannelMemberHistory table. Instead, we'll fall back to the ChannelMembers table for a rough approximation
joinTime := int64(1000)
leaveTime := joinTime + 5000
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
// in every single case, the user will be included in the export, because ChannelMembers says they were in the channel at some point in
// the past, even though the time that they were actually in the channel doesn't necessarily overlap with the export period
// case 1: user joins and leaves the channel before the export period begins
channelMembers, err := ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-500, joinTime-100, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime-500, channelMembers[0].JoinTime)
assert.Equal(t, joinTime-100, *channelMembers[0].LeaveTime)
// case 2: user joins the channel after the export period begins, but has not yet left the channel when the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-100, joinTime+500, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime-100, channelMembers[0].JoinTime)
assert.Equal(t, joinTime+500, *channelMembers[0].LeaveTime)
// case 3: user joins the channel before the export period begins, but has not yet left the channel when the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+100, joinTime+500, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime+100, channelMembers[0].JoinTime)
assert.Equal(t, joinTime+500, *channelMembers[0].LeaveTime)
// case 4: user joins the channel before the export period begins, but has not yet left the channel when the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+100, leaveTime-100, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime+100, channelMembers[0].JoinTime)
assert.Equal(t, leaveTime-100, *channelMembers[0].LeaveTime)
// case 5: user joins the channel after the export period begins, and leaves the channel before the export period ends
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime-100, leaveTime+100, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime-100, channelMembers[0].JoinTime)
assert.Equal(t, leaveTime+100, *channelMembers[0].LeaveTime)
// case 6: user has joined and left the channel long before the export period begins
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(leaveTime+100, leaveTime+200, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, leaveTime+100, channelMembers[0].JoinTime)
assert.Equal(t, leaveTime+200, *channelMembers[0].LeaveTime)
}
func testPermanentDeleteBatch(t *testing.T, ss store.Store) {
// create a test channel
channel := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Display " + model.NewId(),
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
channel, err := ss.Channel().Save(channel, -1)
require.NoError(t, err)
// and two test users
user := model.User{
Email: MakeEmail(),
Nickname: model.NewId(),
Username: model.NewId(),
}
userPtr, err := ss.User().Save(&user)
require.NoError(t, err)
user = *userPtr
user2 := model.User{
Email: MakeEmail(),
Nickname: model.NewId(),
Username: model.NewId(),
}
user2Ptr, err := ss.User().Save(&user2)
require.NoError(t, err)
user2 = *user2Ptr
// user1 joins and leaves the channel
leaveTime := model.GetMillis()
joinTime := leaveTime - 10000
err = ss.ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, joinTime)
require.NoError(t, err)
err = ss.ChannelMemberHistory().LogLeaveEvent(user.Id, channel.Id, leaveTime)
require.NoError(t, err)
// user2 joins the channel but never leaves
err = ss.ChannelMemberHistory().LogJoinEvent(user2.Id, channel.Id, joinTime)
require.NoError(t, err)
// in between the join time and the leave time, both users were members of the channel
channelMembers, err := ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+10, leaveTime-10, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 2)
// the permanent delete should delete at least one record
rowsDeleted, _, err := ss.ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies(
0, leaveTime+1, math.MaxInt64, model.RetentionPolicyCursor{})
require.NoError(t, err)
assert.NotEqual(t, int64(0), rowsDeleted)
// after the delete, there should be one less member in the channel
channelMembers, err = ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime+10, leaveTime-10, channel.Id)
require.NoError(t, err)
assert.Len(t, channelMembers, 1)
assert.Equal(t, user2.Id, channelMembers[0].UserId)
}
func testPermanentDeleteBatchForRetentionPolicies(t *testing.T, ss store.Store) {
const limit = 1000
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
userID := model.NewId()
joinTime := int64(1000)
leaveTime := int64(1500)
err = ss.ChannelMemberHistory().LogJoinEvent(userID, channel.Id, joinTime)
require.NoError(t, err)
err = ss.ChannelMemberHistory().LogLeaveEvent(userID, channel.Id, leaveTime)
require.NoError(t, err)
channelPolicy, err := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewInt64(30),
},
ChannelIDs: []string{channel.Id},
})
require.NoError(t, err)
nowMillis := leaveTime + *channelPolicy.PostDurationDays*model.DayInMilliseconds + 1
_, _, err = ss.ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies(
nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
result, err := ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime, leaveTime, channel.Id)
require.NoError(t, err)
require.Empty(t, result, "history should have been deleted by channel policy")
}
func testGetChannelsLeftSince(t *testing.T, ss store.Store) {
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
userID := model.NewId()
joinTime := int64(1000)
err = ss.ChannelMemberHistory().LogJoinEvent(userID, channel.Id, joinTime)
require.NoError(t, err)
// has not left
ids, err := ss.ChannelMemberHistory().GetChannelsLeftSince(userID, joinTime)
require.NoError(t, err)
assert.Empty(t, ids)
// left
err = ss.ChannelMemberHistory().LogLeaveEvent(userID, channel.Id, joinTime+100)
require.NoError(t, err)
ids, err = ss.ChannelMemberHistory().GetChannelsLeftSince(userID, joinTime+100)
require.NoError(t, err)
assert.Equal(t, []string{channel.Id}, ids)
ids, err = ss.ChannelMemberHistory().GetChannelsLeftSince(userID, joinTime+200)
require.NoError(t, err)
assert.Empty(t, ids)
// joined and left again.
err = ss.ChannelMemberHistory().LogJoinEvent(userID, channel.Id, joinTime+200)
require.NoError(t, err)
err = ss.ChannelMemberHistory().LogLeaveEvent(userID, channel.Id, joinTime+300)
require.NoError(t, err)
// should be same for both time stamps
ids, err = ss.ChannelMemberHistory().GetChannelsLeftSince(userID, joinTime+100)
require.NoError(t, err)
assert.Equal(t, []string{channel.Id}, ids)
ids, err = ss.ChannelMemberHistory().GetChannelsLeftSince(userID, joinTime+300)
require.NoError(t, err)
assert.Equal(t, []string{channel.Id}, ids)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"database/sql"
"encoding/json"
"errors"
"sort"
"strconv"
"strings"
"testing"
"time"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/services/timezones"
)
type SqlStore interface {
GetMasterX() SqlXExecutor
DriverName() string
}
type SqlXExecutor interface {
Get(dest any, query string, args ...any) error
NamedExec(query string, arg any) (sql.Result, error)
Exec(query string, args ...any) (sql.Result, error)
ExecRaw(query string, args ...any) (sql.Result, error)
NamedQuery(query string, arg any) (*sqlx.Rows, error)
QueryRowX(query string, args ...any) *sqlx.Row
QueryX(query string, args ...any) (*sqlx.Rows, error)
Select(dest any, query string, args ...any) error
}
func cleanupChannels(t *testing.T, ss store.Store) {
list, err := ss.Channel().GetAllChannels(0, 100000, store.ChannelSearchOpts{IncludeDeleted: true})
require.NoError(t, err, "error cleaning all channels", err)
for _, channel := range list {
err = ss.Channel().PermanentDelete(channel.Id)
assert.NoError(t, err)
}
}
func channelToJSON(t *testing.T, channel *model.Channel) string {
t.Helper()
js, err := json.Marshal(channel)
require.NoError(t, err)
return string(js)
}
func channelMemberToJSON(t *testing.T, cm *model.ChannelMember) string {
t.Helper()
js, err := json.Marshal(cm)
require.NoError(t, err)
return string(js)
}
func TestChannelStore(t *testing.T, ss store.Store, s SqlStore) {
createDefaultRoles(ss)
t.Run("Save", func(t *testing.T) { testChannelStoreSave(t, ss) })
t.Run("SaveDirectChannel", func(t *testing.T) { testChannelStoreSaveDirectChannel(t, ss, s) })
t.Run("CreateDirectChannel", func(t *testing.T) { testChannelStoreCreateDirectChannel(t, ss) })
t.Run("Update", func(t *testing.T) { testChannelStoreUpdate(t, ss) })
t.Run("GetChannelUnread", func(t *testing.T) { testGetChannelUnread(t, ss) })
t.Run("Get", func(t *testing.T) { testChannelStoreGet(t, ss, s) })
t.Run("GetMany", func(t *testing.T) { testChannelStoreGetMany(t, ss, s) })
t.Run("GetChannelsByIds", func(t *testing.T) { testChannelStoreGetChannelsByIds(t, ss) })
t.Run("GetChannelsWithTeamDataByIds", func(t *testing.T) { testGetChannelsWithTeamDataByIds(t, ss) })
t.Run("GetForPost", func(t *testing.T) { testChannelStoreGetForPost(t, ss) })
t.Run("Restore", func(t *testing.T) { testChannelStoreRestore(t, ss) })
t.Run("Delete", func(t *testing.T) { testChannelStoreDelete(t, ss) })
t.Run("GetByName", func(t *testing.T) { testChannelStoreGetByName(t, ss) })
t.Run("GetByNames", func(t *testing.T) { testChannelStoreGetByNames(t, ss) })
t.Run("GetDeletedByName", func(t *testing.T) { testChannelStoreGetDeletedByName(t, ss) })
t.Run("GetDeleted", func(t *testing.T) { testChannelStoreGetDeleted(t, ss) })
t.Run("ChannelMemberStore", func(t *testing.T) { testChannelMemberStore(t, ss) })
t.Run("SaveMember", func(t *testing.T) { testChannelSaveMember(t, ss) })
t.Run("SaveMultipleMembers", func(t *testing.T) { testChannelSaveMultipleMembers(t, ss) })
t.Run("UpdateMember", func(t *testing.T) { testChannelUpdateMember(t, ss) })
t.Run("UpdateMemberNotifyProps", func(t *testing.T) { testChannelUpdateMemberNotifyProps(t, ss) })
t.Run("UpdateMultipleMembers", func(t *testing.T) { testChannelUpdateMultipleMembers(t, ss) })
t.Run("RemoveMember", func(t *testing.T) { testChannelRemoveMember(t, ss) })
t.Run("RemoveMembers", func(t *testing.T) { testChannelRemoveMembers(t, ss) })
t.Run("ChannelDeleteMemberStore", func(t *testing.T) { testChannelDeleteMemberStore(t, ss) })
t.Run("GetChannels", func(t *testing.T) { testChannelStoreGetChannels(t, ss) })
t.Run("GetChannelsWithCursor", func(t *testing.T) { testChannelStoreGetChannelsWithCursor(t, ss) })
t.Run("GetChannelsByUser", func(t *testing.T) { testChannelStoreGetChannelsByUser(t, ss) })
t.Run("GetAllChannels", func(t *testing.T) { testChannelStoreGetAllChannels(t, ss, s) })
t.Run("GetMoreChannels", func(t *testing.T) { testChannelStoreGetMoreChannels(t, ss) })
t.Run("GetPrivateChannelsForTeam", func(t *testing.T) { testChannelStoreGetPrivateChannelsForTeam(t, ss) })
t.Run("GetPublicChannelsForTeam", func(t *testing.T) { testChannelStoreGetPublicChannelsForTeam(t, ss) })
t.Run("GetPublicChannelsByIdsForTeam", func(t *testing.T) { testChannelStoreGetPublicChannelsByIdsForTeam(t, ss) })
t.Run("GetChannelCounts", func(t *testing.T) { testChannelStoreGetChannelCounts(t, ss) })
t.Run("GetMembersForUser", func(t *testing.T) { testChannelStoreGetMembersForUser(t, ss) })
t.Run("GetMembersForUserWithCursor", func(t *testing.T) { testChannelStoreGetMembersForUserWithCursor(t, ss) })
t.Run("GetMembersForUserWithPagination", func(t *testing.T) { testChannelStoreGetMembersForUserWithPagination(t, ss) })
t.Run("CountPostsAfter", func(t *testing.T) { testCountPostsAfter(t, ss) })
t.Run("CountUrgentPostsAfter", func(t *testing.T) { testCountUrgentPostsAfter(t, ss) })
t.Run("UpdateLastViewedAt", func(t *testing.T) { testChannelStoreUpdateLastViewedAt(t, ss) })
t.Run("IncrementMentionCount", func(t *testing.T) { testChannelStoreIncrementMentionCount(t, ss) })
t.Run("UpdateChannelMember", func(t *testing.T) { testUpdateChannelMember(t, ss) })
t.Run("GetMember", func(t *testing.T) { testGetMember(t, ss) })
t.Run("GetMemberForPost", func(t *testing.T) { testChannelStoreGetMemberForPost(t, ss) })
t.Run("GetMemberCount", func(t *testing.T) { testGetMemberCount(t, ss) })
t.Run("GetMemberCountsByGroup", func(t *testing.T) { testGetMemberCountsByGroup(t, ss) })
t.Run("GetGuestCount", func(t *testing.T) { testGetGuestCount(t, ss) })
t.Run("SearchMore", func(t *testing.T) { testChannelStoreSearchMore(t, ss) })
t.Run("SearchInTeam", func(t *testing.T) { testChannelStoreSearchInTeam(t, ss) })
t.Run("Autocomplete", func(t *testing.T) { testAutocomplete(t, ss) })
t.Run("SearchArchivedInTeam", func(t *testing.T) { testChannelStoreSearchArchivedInTeam(t, ss, s) })
t.Run("SearchForUserInTeam", func(t *testing.T) { testChannelStoreSearchForUserInTeam(t, ss) })
t.Run("SearchAllChannels", func(t *testing.T) { testChannelStoreSearchAllChannels(t, ss) })
t.Run("GetMembersByIds", func(t *testing.T) { testChannelStoreGetMembersByIds(t, ss) })
t.Run("GetMembersByChannelIds", func(t *testing.T) { testChannelStoreGetMembersByChannelIds(t, ss) })
t.Run("GetMembersInfoByChannelIds", func(t *testing.T) { testChannelStoreGetMembersInfoByChannelIds(t, ss) })
t.Run("SearchGroupChannels", func(t *testing.T) { testChannelStoreSearchGroupChannels(t, ss) })
t.Run("AnalyticsDeletedTypeCount", func(t *testing.T) { testChannelStoreAnalyticsDeletedTypeCount(t, ss) })
t.Run("GetPinnedPosts", func(t *testing.T) { testChannelStoreGetPinnedPosts(t, ss) })
t.Run("GetPinnedPostCount", func(t *testing.T) { testChannelStoreGetPinnedPostCount(t, ss) })
t.Run("MaxChannelsPerTeam", func(t *testing.T) { testChannelStoreMaxChannelsPerTeam(t, ss) })
t.Run("GetChannelsByScheme", func(t *testing.T) { testChannelStoreGetChannelsByScheme(t, ss) })
t.Run("MigrateChannelMembers", func(t *testing.T) { testChannelStoreMigrateChannelMembers(t, ss) })
t.Run("ResetAllChannelSchemes", func(t *testing.T) { testResetAllChannelSchemes(t, ss) })
t.Run("ClearAllCustomRoleAssignments", func(t *testing.T) { testChannelStoreClearAllCustomRoleAssignments(t, ss) })
t.Run("MaterializedPublicChannels", func(t *testing.T) { testMaterializedPublicChannels(t, ss, s) })
t.Run("GetAllChannelsForExportAfter", func(t *testing.T) { testChannelStoreGetAllChannelsForExportAfter(t, ss) })
t.Run("GetChannelMembersForExport", func(t *testing.T) { testChannelStoreGetChannelMembersForExport(t, ss) })
t.Run("RemoveAllDeactivatedMembers", func(t *testing.T) { testChannelStoreRemoveAllDeactivatedMembers(t, ss, s) })
t.Run("ExportAllDirectChannels", func(t *testing.T) { testChannelStoreExportAllDirectChannels(t, ss, s) })
t.Run("ExportAllDirectChannelsExcludePrivateAndPublic", func(t *testing.T) { testChannelStoreExportAllDirectChannelsExcludePrivateAndPublic(t, ss, s) })
t.Run("ExportAllDirectChannelsDeletedChannel", func(t *testing.T) { testChannelStoreExportAllDirectChannelsDeletedChannel(t, ss, s) })
t.Run("GetChannelsBatchForIndexing", func(t *testing.T) { testChannelStoreGetChannelsBatchForIndexing(t, ss) })
t.Run("GroupSyncedChannelCount", func(t *testing.T) { testGroupSyncedChannelCount(t, ss) })
t.Run("CreateInitialSidebarCategories", func(t *testing.T) { testCreateInitialSidebarCategories(t, ss) })
t.Run("CreateSidebarCategory", func(t *testing.T) { testCreateSidebarCategory(t, ss) })
t.Run("GetSidebarCategory", func(t *testing.T) { testGetSidebarCategory(t, ss, s) })
t.Run("GetSidebarCategories", func(t *testing.T) { testGetSidebarCategories(t, ss) })
t.Run("UpdateSidebarCategories", func(t *testing.T) { testUpdateSidebarCategories(t, ss) })
t.Run("DeleteSidebarCategory", func(t *testing.T) { testDeleteSidebarCategory(t, ss, s) })
t.Run("UpdateSidebarChannelsByPreferences", func(t *testing.T) { testUpdateSidebarChannelsByPreferences(t, ss) })
t.Run("SetShared", func(t *testing.T) { testSetShared(t, ss) })
t.Run("GetTeamForChannel", func(t *testing.T) { testGetTeamForChannel(t, ss) })
t.Run("PostCountsByDuration", func(t *testing.T) { testChannelPostCountsByDuration(t, ss) })
t.Run("GetTopInactiveChannels", func(t *testing.T) { testGetTopInactiveChannels(t, ss) })
}
func testChannelStoreSave(t *testing.T, ss store.Store) {
teamId := model.NewId()
o1 := model.Channel{}
o1.TeamId = teamId
o1.DisplayName = "Name"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr, "couldn't save item", nErr)
_, nErr = ss.Channel().Save(&o1, -1)
require.Error(t, nErr, "shouldn't be able to update from save")
o1.Id = ""
_, nErr = ss.Channel().Save(&o1, -1)
require.Error(t, nErr, "should be unique name")
o1.Id = ""
o1.Name = NewTestId()
o1.Type = model.ChannelTypeDirect
_, nErr = ss.Channel().Save(&o1, -1)
require.Error(t, nErr, "should not be able to save direct channel")
o1 = model.Channel{}
o1.TeamId = teamId
o1.DisplayName = "Name"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o1, -1)
require.NoError(t, nErr, "should have saved channel")
o2 := o1
o2.Id = ""
_, nErr = ss.Channel().Save(&o2, -1)
require.Error(t, nErr, "should have failed to save a duplicate channel")
var cErr *store.ErrConflict
require.True(t, errors.As(nErr, &cErr))
err := ss.Channel().Delete(o1.Id, 100)
require.NoError(t, err, "should have deleted channel")
o2.Id = ""
_, nErr = ss.Channel().Save(&o2, -1)
require.Error(t, nErr, "should have failed to save a duplicate of an archived channel")
require.True(t, errors.As(nErr, &cErr))
}
func testChannelStoreSaveDirectChannel(t *testing.T, ss store.Store, s SqlStore) {
teamId := model.NewId()
o1 := model.Channel{}
o1.TeamId = teamId
o1.DisplayName = "Name"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeDirect
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, nErr = ss.Channel().SaveDirectChannel(&o1, &m1, &m2)
require.NoError(t, nErr, "couldn't save direct channel", nErr)
members, nErr := ss.Channel().GetMembers(o1.Id, 0, 100)
require.NoError(t, nErr)
require.Len(t, members, 2, "should have saved 2 members")
_, nErr = ss.Channel().SaveDirectChannel(&o1, &m1, &m2)
require.Error(t, nErr, "shouldn't be a able to update from save")
// Attempt to save a direct channel that already exists
o1a := model.Channel{
TeamId: o1.TeamId,
DisplayName: o1.DisplayName,
Name: o1.Name,
Type: o1.Type,
}
returnedChannel, nErr := ss.Channel().SaveDirectChannel(&o1a, &m1, &m2)
require.Error(t, nErr, "should've failed to save a duplicate direct channel")
var cErr *store.ErrConflict
require.Truef(t, errors.As(nErr, &cErr), "should've returned ChannelExistsError")
require.Equal(t, o1.Id, returnedChannel.Id, "should've failed to save a duplicate direct channel")
// Attempt to save a non-direct channel
o1.Id = ""
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().SaveDirectChannel(&o1, &m1, &m2)
require.Error(t, nErr, "Should not be able to save non-direct channel")
// Save yourself Direct Message
o1.Id = ""
o1.DisplayName = "Myself"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeDirect
_, nErr = ss.Channel().SaveDirectChannel(&o1, &m1, &m1)
require.NoError(t, nErr, "couldn't save direct channel", nErr)
members, nErr = ss.Channel().GetMembers(o1.Id, 0, 100)
require.NoError(t, nErr)
require.Len(t, members, 1, "should have saved just 1 member")
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testChannelStoreCreateDirectChannel(t *testing.T, ss store.Store) {
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
c1, nErr := ss.Channel().CreateDirectChannel(u1, u2)
require.NoError(t, nErr, "couldn't create direct channel", nErr)
defer func() {
ss.Channel().PermanentDeleteMembersByChannel(c1.Id)
ss.Channel().PermanentDelete(c1.Id)
}()
members, nErr := ss.Channel().GetMembers(c1.Id, 0, 100)
require.NoError(t, nErr)
require.Len(t, members, 2, "should have saved 2 members")
}
func testChannelStoreUpdate(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Name"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{}
o2.TeamId = o1.TeamId
o2.DisplayName = "Name"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
time.Sleep(100 * time.Millisecond)
_, err := ss.Channel().Update(&o1)
require.NoError(t, err, err)
o1.DeleteAt = 100
_, err = ss.Channel().Update(&o1)
require.Error(t, err, "update should have failed because channel is archived")
o1.DeleteAt = 0
o1.Id = "missing"
_, err = ss.Channel().Update(&o1)
require.Error(t, err, "Update should have failed because of missing key")
o2.Name = o1.Name
_, err = ss.Channel().Update(&o2)
require.Error(t, err, "update should have failed because of existing name")
}
func testGetChannelUnread(t *testing.T, ss store.Store) {
teamId1 := model.NewId()
teamId2 := model.NewId()
uid := model.NewId()
m1 := &model.TeamMember{TeamId: teamId1, UserId: uid}
m2 := &model.TeamMember{TeamId: teamId2, UserId: uid}
_, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(m2, -1)
require.NoError(t, nErr)
notifyPropsModel := model.GetDefaultChannelNotifyProps()
// Setup Channel 1
c1 := &model.Channel{TeamId: m1.TeamId, Name: model.NewId(), DisplayName: "Downtown", Type: model.ChannelTypeOpen, TotalMsgCount: 100, TotalMsgCountRoot: 99}
_, nErr = ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
cm1 := &model.ChannelMember{ChannelId: c1.Id, UserId: m1.UserId, NotifyProps: notifyPropsModel, MsgCount: 90, MsgCountRoot: 80}
_, err := ss.Channel().SaveMember(cm1)
require.NoError(t, err)
// Setup Channel 2
c2 := &model.Channel{TeamId: m2.TeamId, Name: model.NewId(), DisplayName: "Cultural", Type: model.ChannelTypeOpen, TotalMsgCount: 100, TotalMsgCountRoot: 100}
_, nErr = ss.Channel().Save(c2, -1)
require.NoError(t, nErr)
cm2 := &model.ChannelMember{ChannelId: c2.Id, UserId: m2.UserId, NotifyProps: notifyPropsModel, MsgCount: 90, MsgCountRoot: 90, MentionCount: 5, MentionCountRoot: 1}
_, err = ss.Channel().SaveMember(cm2)
require.NoError(t, err)
// Check for Channel 1
ch, nErr := ss.Channel().GetChannelUnread(c1.Id, uid)
require.NoError(t, nErr, nErr)
require.Equal(t, c1.Id, ch.ChannelId, "Wrong channel id")
require.Equal(t, teamId1, ch.TeamId, "Wrong team id for channel 1")
require.NotNil(t, ch.NotifyProps, "wrong props for channel 1")
require.EqualValues(t, 0, ch.MentionCount, "wrong MentionCount for channel 1")
require.EqualValues(t, 10, ch.MsgCount, "wrong MsgCount for channel 1")
require.EqualValues(t, 19, ch.MsgCountRoot, "wrong MsgCountRoot for channel 1")
// Check for Channel 2
ch2, nErr := ss.Channel().GetChannelUnread(c2.Id, uid)
require.NoError(t, nErr, nErr)
require.Equal(t, c2.Id, ch2.ChannelId, "Wrong channel id")
require.Equal(t, teamId2, ch2.TeamId, "Wrong team id")
require.EqualValues(t, 5, ch2.MentionCount, "wrong MentionCount for channel 2")
require.EqualValues(t, 1, ch2.MentionCountRoot, "wrong MentionCountRoot for channel 2")
require.EqualValues(t, 10, ch2.MsgCount, "wrong MsgCount for channel 2")
}
func testChannelStoreGet(t *testing.T, ss store.Store, s SqlStore) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Name"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
c1, err := ss.Channel().Get(o1.Id, false)
require.NoError(t, err, err)
require.Equal(t, channelToJSON(t, &o1), channelToJSON(t, c1), "invalid returned channel")
_, err = ss.Channel().Get("", false)
require.Error(t, err, "missing id should have failed")
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err = ss.User().Save(u1)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(&u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
o2 := model.Channel{}
o2.TeamId = model.NewId()
o2.DisplayName = "Direct Name"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeDirect
m1 := model.ChannelMember{}
m1.ChannelId = o2.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = o2.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, nErr = ss.Channel().SaveDirectChannel(&o2, &m1, &m2)
require.NoError(t, nErr)
c2, err := ss.Channel().Get(o2.Id, false)
require.NoError(t, err, err)
require.Equal(t, channelToJSON(t, &o2), channelToJSON(t, c2), "invalid returned channel")
c4, err := ss.Channel().Get(o2.Id, true)
require.NoError(t, err, err)
require.Equal(t, channelToJSON(t, &o2), channelToJSON(t, c4), "invalid returned channel")
channels, chanErr := ss.Channel().GetAll(o1.TeamId)
require.NoError(t, chanErr, chanErr)
require.Greater(t, len(channels), 0, "too little")
channelsTeam, err := ss.Channel().GetTeamChannels(o1.TeamId)
require.NoError(t, err, err)
require.Greater(t, len(channelsTeam), 0, "too little")
_, err = ss.Channel().GetTeamChannels("notfound")
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testChannelStoreGetMany(t *testing.T, ss store.Store, s SqlStore) {
o1, nErr := ss.Channel().Save(&model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
o2, nErr := ss.Channel().Save(&model.Channel{
TeamId: model.NewId(),
DisplayName: "Name2",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
res, err := ss.Channel().GetMany([]string{o1.Id, o2.Id}, true)
require.NoError(t, err)
assert.Len(t, res, 2)
res, err = ss.Channel().GetMany([]string{o1.Id, "notexists"}, true)
require.NoError(t, err)
assert.Len(t, res, 1)
_, err = ss.Channel().GetMany([]string{"notexists"}, true)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testChannelStoreGetChannelsByIds(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Name"
o1.Name = "aa" + model.NewId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(&u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
o2 := model.Channel{}
o2.TeamId = model.NewId()
o2.DisplayName = "Direct Name"
o2.Name = "bb" + model.NewId()
o2.Type = model.ChannelTypeDirect
o3 := model.Channel{}
o3.TeamId = model.NewId()
o3.DisplayName = "Deleted channel"
o3.Name = "cc" + model.NewId()
o3.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
nErr = ss.Channel().Delete(o3.Id, 123)
require.NoError(t, nErr)
o3.DeleteAt = 123
o3.UpdateAt = 123
m1 := model.ChannelMember{}
m1.ChannelId = o2.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = o2.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, nErr = ss.Channel().SaveDirectChannel(&o2, &m1, &m2)
require.NoError(t, nErr)
t.Run("Get 2 existing channels", func(t *testing.T) {
r1, err := ss.Channel().GetChannelsByIds([]string{o1.Id, o2.Id}, false)
require.NoError(t, err, err)
require.Len(t, r1, 2, "invalid returned channels, expected 2 and got "+strconv.Itoa(len(r1)))
require.Equal(t, channelToJSON(t, &o1), channelToJSON(t, r1[0]))
require.Equal(t, channelToJSON(t, &o2), channelToJSON(t, r1[1]))
})
t.Run("Get 1 existing and 1 not existing channel", func(t *testing.T) {
nonexistentId := "abcd1234"
r2, err := ss.Channel().GetChannelsByIds([]string{o1.Id, nonexistentId}, false)
require.NoError(t, err, err)
require.Len(t, r2, 1, "invalid returned channels, expected 1 and got "+strconv.Itoa(len(r2)))
require.Equal(t, channelToJSON(t, &o1), channelToJSON(t, r2[0]), "invalid returned channel")
})
t.Run("Get 2 existing and 1 deleted channel", func(t *testing.T) {
r1, err := ss.Channel().GetChannelsByIds([]string{o1.Id, o2.Id, o3.Id}, true)
require.NoError(t, err, err)
require.Len(t, r1, 3, "invalid returned channels, expected 3 and got "+strconv.Itoa(len(r1)))
require.Equal(t, channelToJSON(t, &o1), channelToJSON(t, r1[0]))
require.Equal(t, channelToJSON(t, &o2), channelToJSON(t, r1[1]))
require.Equal(t, channelToJSON(t, &o3), channelToJSON(t, r1[2]))
})
}
func testGetChannelsWithTeamDataByIds(t *testing.T, ss store.Store) {
t1 := &model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
t1, err := ss.Team().Save(t1)
require.NoError(t, err, "couldn't save item")
c1 := model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Name"
c1.Name = "aa" + model.NewId()
c1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&c1, -1)
require.NoError(t, nErr)
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err = ss.User().Save(u1)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: t1.Id, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(&u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: t1.Id, UserId: u2.Id}, -1)
require.NoError(t, nErr)
c2 := model.Channel{}
c2.TeamId = t1.Id
c2.DisplayName = "Direct Name"
c2.Name = "bb" + model.NewId()
c2.Type = model.ChannelTypeDirect
c3 := model.Channel{}
c3.TeamId = t1.Id
c3.DisplayName = "Deleted channel"
c3.Name = "cc" + model.NewId()
c3.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&c3, -1)
require.NoError(t, nErr)
nErr = ss.Channel().Delete(c3.Id, 123)
require.NoError(t, nErr)
c3.DeleteAt = 123
c3.UpdateAt = 123
m1 := model.ChannelMember{}
m1.ChannelId = c2.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = c2.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, nErr = ss.Channel().SaveDirectChannel(&c2, &m1, &m2)
require.NoError(t, nErr)
res, err := ss.Channel().GetChannelsWithTeamDataByIds([]string{c1.Id, c2.Id}, false)
require.NoError(t, err)
require.Len(t, res, 2)
assert.Equal(t, res[0].Id, c1.Id)
assert.Equal(t, res[0].TeamName, t1.Name)
assert.Equal(t, res[1].Id, c2.Id)
assert.Equal(t, res[1].TeamName, "")
}
func testChannelStoreGetForPost(t *testing.T, ss store.Store) {
ch := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
o1, nErr := ss.Channel().Save(ch, -1)
require.NoError(t, nErr)
p1, err := ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: o1.Id,
Message: "test",
})
require.NoError(t, err)
channel, chanErr := ss.Channel().GetForPost(p1.Id)
require.NoError(t, chanErr, chanErr)
require.Equal(t, o1.Id, channel.Id, "incorrect channel returned")
}
func testChannelStoreRestore(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
err := ss.Channel().Delete(o1.Id, model.GetMillis())
require.NoError(t, err, err)
c, _ := ss.Channel().Get(o1.Id, false)
require.NotEqual(t, 0, c.DeleteAt, "should have been deleted")
err = ss.Channel().Restore(o1.Id, model.GetMillis())
require.NoError(t, err, err)
c, _ = ss.Channel().Get(o1.Id, false)
require.EqualValues(t, 0, c.DeleteAt, "should have been restored")
}
func testChannelStoreDelete(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{}
o2.TeamId = o1.TeamId
o2.DisplayName = "Channel2"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
o3 := model.Channel{}
o3.TeamId = o1.TeamId
o3.DisplayName = "Channel3"
o3.Name = NewTestId()
o3.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
o4 := model.Channel{}
o4.TeamId = o1.TeamId
o4.DisplayName = "Channel4"
o4.Name = NewTestId()
o4.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o4, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err := ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{}
m2.ChannelId = o2.Id
m2.UserId = m1.UserId
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
nErr = ss.Channel().Delete(o1.Id, model.GetMillis())
require.NoError(t, nErr, nErr)
c, _ := ss.Channel().Get(o1.Id, false)
require.NotEqual(t, 0, c.DeleteAt, "should have been deleted")
nErr = ss.Channel().Delete(o3.Id, model.GetMillis())
require.NoError(t, nErr, nErr)
list, nErr := ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
})
require.NoError(t, nErr)
require.Len(t, list, 1, "invalid number of channels")
list, nErr = ss.Channel().GetMoreChannels(o1.TeamId, m1.UserId, 0, 100)
require.NoError(t, nErr)
require.Len(t, list, 1, "invalid number of channels")
cresult := ss.Channel().PermanentDelete(o2.Id)
require.NoError(t, cresult)
list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
})
if assert.Error(t, nErr) {
var nfErr *store.ErrNotFound
require.True(t, errors.As(nErr, &nfErr))
} else {
require.Equal(t, model.ChannelList{}, list)
}
nErr = ss.Channel().PermanentDeleteByTeam(o1.TeamId)
require.NoError(t, nErr, nErr)
}
func testChannelStoreGetByName(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Name"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
result, err := ss.Channel().GetByName(o1.TeamId, o1.Name, true)
require.NoError(t, err)
require.Equal(t, channelToJSON(t, &o1), channelToJSON(t, result), "invalid returned channel")
channelID := result.Id
_, err = ss.Channel().GetByName(o1.TeamId, "", true)
require.Error(t, err, "Missing id should have failed")
result, err = ss.Channel().GetByName(o1.TeamId, o1.Name, false)
require.NoError(t, err)
require.Equal(t, channelToJSON(t, &o1), channelToJSON(t, result), "invalid returned channel")
_, err = ss.Channel().GetByName(o1.TeamId, "", false)
require.Error(t, err, "Missing id should have failed")
nErr = ss.Channel().Delete(channelID, model.GetMillis())
require.NoError(t, nErr, "channel should have been deleted")
_, err = ss.Channel().GetByName(o1.TeamId, o1.Name, false)
require.Error(t, err, "Deleted channel should not be returned by GetByName()")
}
func testChannelStoreGetByNames(t *testing.T, ss store.Store) {
o1 := model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{
TeamId: o1.TeamId,
DisplayName: "Name",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
for index, tc := range []struct {
TeamId string
Names []string
ExpectedIds []string
}{
{o1.TeamId, []string{o1.Name}, []string{o1.Id}},
{o1.TeamId, []string{o1.Name, o2.Name}, []string{o1.Id, o2.Id}},
{o1.TeamId, nil, nil},
{o1.TeamId, []string{"foo"}, nil},
{o1.TeamId, []string{o1.Name, "foo", o2.Name, o2.Name}, []string{o1.Id, o2.Id}},
{"", []string{o1.Name, "foo", o2.Name, o2.Name}, []string{o1.Id, o2.Id}},
{"asd", []string{o1.Name, "foo", o2.Name, o2.Name}, nil},
} {
var channels []*model.Channel
channels, err := ss.Channel().GetByNames(tc.TeamId, tc.Names, true)
require.NoError(t, err)
var ids []string
for _, channel := range channels {
ids = append(ids, channel.Id)
}
sort.Strings(ids)
sort.Strings(tc.ExpectedIds)
assert.Equal(t, tc.ExpectedIds, ids, "tc %v", index)
}
err := ss.Channel().Delete(o1.Id, model.GetMillis())
require.NoError(t, err, "channel should have been deleted")
err = ss.Channel().Delete(o2.Id, model.GetMillis())
require.NoError(t, err, "channel should have been deleted")
channels, nErr := ss.Channel().GetByNames(o1.TeamId, []string{o1.Name}, false)
require.NoError(t, nErr)
assert.Empty(t, channels)
}
func testChannelStoreGetDeletedByName(t *testing.T, ss store.Store) {
o1 := &model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Name"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(o1, -1)
require.NoError(t, nErr)
now := model.GetMillis()
err := ss.Channel().Delete(o1.Id, now)
require.NoError(t, err, "channel should have been deleted")
o1.DeleteAt = now
o1.UpdateAt = now
r1, nErr := ss.Channel().GetDeletedByName(o1.TeamId, o1.Name)
require.NoError(t, nErr)
require.Equal(t, o1, r1)
_, nErr = ss.Channel().GetDeletedByName(o1.TeamId, "")
require.Error(t, nErr, "missing id should have failed")
}
func testChannelStoreGetDeleted(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
userId := model.NewId()
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
err := ss.Channel().Delete(o1.Id, model.GetMillis())
require.NoError(t, err, "channel should have been deleted")
list, nErr := ss.Channel().GetDeleted(o1.TeamId, 0, 100, userId)
require.NoError(t, nErr, nErr)
require.Len(t, list, 1, "wrong list")
require.Equal(t, o1.Name, list[0].Name, "missing channel")
o2 := model.Channel{}
o2.TeamId = o1.TeamId
o2.DisplayName = "Channel2"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
list, nErr = ss.Channel().GetDeleted(o1.TeamId, 0, 100, userId)
require.NoError(t, nErr, nErr)
require.Len(t, list, 1, "wrong list")
o3 := model.Channel{}
o3.TeamId = o1.TeamId
o3.DisplayName = "Channel3"
o3.Name = NewTestId()
o3.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
err = ss.Channel().Delete(o3.Id, model.GetMillis())
require.NoError(t, err, "channel should have been deleted")
list, nErr = ss.Channel().GetDeleted(o1.TeamId, 0, 100, userId)
require.NoError(t, nErr, nErr)
require.Len(t, list, 2, "wrong list length")
list, nErr = ss.Channel().GetDeleted(o1.TeamId, 0, 1, userId)
require.NoError(t, nErr, nErr)
require.Len(t, list, 1, "wrong list length")
list, nErr = ss.Channel().GetDeleted(o1.TeamId, 1, 1, userId)
require.NoError(t, nErr, nErr)
require.Len(t, list, 1, "wrong list length")
}
func testChannelMemberStore(t *testing.T, ss store.Store) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "NameName"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, nErr := ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
c1t1, _ := ss.Channel().Get(c1.Id, false)
assert.EqualValues(t, 0, c1t1.ExtraUpdateAt, "ExtraUpdateAt should be 0")
u1 := model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(&u1)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(&u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
o1 := model.ChannelMember{}
o1.ChannelId = c1.Id
o1.UserId = u1.Id
o1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, nErr = ss.Channel().SaveMember(&o1)
require.NoError(t, nErr)
o2 := model.ChannelMember{}
o2.ChannelId = c1.Id
o2.UserId = u2.Id
o2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, nErr = ss.Channel().SaveMember(&o2)
require.NoError(t, nErr)
c1t2, _ := ss.Channel().Get(c1.Id, false)
assert.EqualValues(t, 0, c1t2.ExtraUpdateAt, "ExtraUpdateAt should be 0")
count, nErr := ss.Channel().GetMemberCount(o1.ChannelId, true)
require.NoError(t, nErr)
require.EqualValues(t, 2, count, "should have saved 2 members")
count, nErr = ss.Channel().GetMemberCount(o1.ChannelId, true)
require.NoError(t, nErr)
require.EqualValues(t, 2, count, "should have saved 2 members")
require.EqualValues(
t,
2,
ss.Channel().GetMemberCountFromCache(o1.ChannelId),
"should have saved 2 members")
require.EqualValues(
t,
0,
ss.Channel().GetMemberCountFromCache("junk"),
"should have saved 0 members")
count, nErr = ss.Channel().GetMemberCount(o1.ChannelId, false)
require.NoError(t, nErr)
require.EqualValues(t, 2, count, "should have saved 2 members")
nErr = ss.Channel().RemoveMember(o2.ChannelId, o2.UserId)
require.NoError(t, nErr)
count, nErr = ss.Channel().GetMemberCount(o1.ChannelId, false)
require.NoError(t, nErr)
require.EqualValues(t, 1, count, "should have removed 1 member")
c1t3, _ := ss.Channel().Get(c1.Id, false)
assert.EqualValues(t, 0, c1t3.ExtraUpdateAt, "ExtraUpdateAt should be 0")
member, _ := ss.Channel().GetMember(context.Background(), o1.ChannelId, o1.UserId)
require.Equal(t, o1.ChannelId, member.ChannelId, "should have go member")
_, nErr = ss.Channel().SaveMember(&o1)
require.Error(t, nErr, "should have been a duplicate")
c1t4, _ := ss.Channel().Get(c1.Id, false)
assert.EqualValues(t, 0, c1t4.ExtraUpdateAt, "ExtraUpdateAt should be 0")
}
func testChannelSaveMember(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
defaultNotifyProps := model.GetDefaultChannelNotifyProps()
t.Run("not valid channel member", func(t *testing.T) {
member := &model.ChannelMember{ChannelId: "wrong", UserId: u1.Id, NotifyProps: defaultNotifyProps}
_, nErr := ss.Channel().SaveMember(member)
require.Error(t, nErr)
var appErr *model.AppError
require.True(t, errors.As(nErr, &appErr))
require.Equal(t, "model.channel_member.is_valid.channel_id.app_error", appErr.Id)
})
t.Run("duplicated entries should fail", func(t *testing.T) {
channelID1 := model.NewId()
m1 := &model.ChannelMember{ChannelId: channelID1, UserId: u1.Id, NotifyProps: defaultNotifyProps}
_, nErr := ss.Channel().SaveMember(m1)
require.NoError(t, nErr)
m2 := &model.ChannelMember{ChannelId: channelID1, UserId: u1.Id, NotifyProps: defaultNotifyProps}
_, nErr = ss.Channel().SaveMember(m2)
require.Error(t, nErr)
require.IsType(t, &store.ErrConflict{}, nErr)
})
t.Run("insert member correctly (in channel without channel scheme and team without scheme)", func(t *testing.T) {
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
channel := &model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: "channel_user",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: "channel_user",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: "channel_guest",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: "channel_guest",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: "channel_user channel_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: "channel_user channel_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test channel_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test channel_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_user channel_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test channel_user channel_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u1.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
NotifyProps: defaultNotifyProps,
}
member, nErr = ss.Channel().SaveMember(member)
require.NoError(t, nErr)
defer ss.Channel().RemoveMember(channel.Id, u1.Id)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert member correctly (in channel without scheme and team with scheme)", func(t *testing.T) {
ts := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
ts, nErr := ss.Scheme().Save(ts)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &ts.Id,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
channel := &model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: ts.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: ts.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: ts.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: ts.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test " + ts.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u1.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
NotifyProps: defaultNotifyProps,
}
member, nErr = ss.Channel().SaveMember(member)
require.NoError(t, nErr)
defer ss.Channel().RemoveMember(channel.Id, u1.Id)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert member correctly (in channel with channel scheme)", func(t *testing.T) {
cs := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
cs, nErr := ss.Scheme().Save(cs)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
SchemeId: &cs.Id,
}, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: cs.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: cs.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: cs.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: cs.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test " + cs.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u1.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
NotifyProps: defaultNotifyProps,
}
member, nErr = ss.Channel().SaveMember(member)
require.NoError(t, nErr)
defer ss.Channel().RemoveMember(channel.Id, u1.Id)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
}
func testChannelSaveMultipleMembers(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
defaultNotifyProps := model.GetDefaultChannelNotifyProps()
t.Run("any not valid channel member", func(t *testing.T) {
m1 := &model.ChannelMember{ChannelId: "wrong", UserId: u1.Id, NotifyProps: defaultNotifyProps}
m2 := &model.ChannelMember{ChannelId: model.NewId(), UserId: u2.Id, NotifyProps: defaultNotifyProps}
_, nErr := ss.Channel().SaveMultipleMembers([]*model.ChannelMember{m1, m2})
require.Error(t, nErr)
var appErr *model.AppError
require.True(t, errors.As(nErr, &appErr))
require.Equal(t, "model.channel_member.is_valid.channel_id.app_error", appErr.Id)
})
t.Run("duplicated entries should fail", func(t *testing.T) {
channelID1 := model.NewId()
m1 := &model.ChannelMember{ChannelId: channelID1, UserId: u1.Id, NotifyProps: defaultNotifyProps}
m2 := &model.ChannelMember{ChannelId: channelID1, UserId: u1.Id, NotifyProps: defaultNotifyProps}
_, nErr := ss.Channel().SaveMultipleMembers([]*model.ChannelMember{m1, m2})
require.Error(t, nErr)
require.IsType(t, &store.ErrConflict{}, nErr)
})
t.Run("insert members correctly (in channel without channel scheme and team without scheme)", func(t *testing.T) {
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
channel := &model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: "channel_user",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: "channel_user",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: "channel_guest",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: "channel_guest",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: "channel_user channel_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: "channel_user channel_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test channel_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test channel_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_user channel_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test channel_user channel_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u1.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
NotifyProps: defaultNotifyProps,
}
otherMember := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u2.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
NotifyProps: defaultNotifyProps,
}
var members []*model.ChannelMember
members, nErr = ss.Channel().SaveMultipleMembers([]*model.ChannelMember{member, otherMember})
require.NoError(t, nErr)
require.Len(t, members, 2)
member = members[0]
defer ss.Channel().RemoveMember(channel.Id, u1.Id)
defer ss.Channel().RemoveMember(channel.Id, u2.Id)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert members correctly (in channel without scheme and team with scheme)", func(t *testing.T) {
ts := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
ts, nErr := ss.Scheme().Save(ts)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &ts.Id,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
channel := &model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: ts.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: ts.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: ts.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: ts.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test " + ts.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u1.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
NotifyProps: defaultNotifyProps,
}
otherMember := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u2.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
NotifyProps: defaultNotifyProps,
}
var members []*model.ChannelMember
members, nErr = ss.Channel().SaveMultipleMembers([]*model.ChannelMember{member, otherMember})
require.NoError(t, nErr)
require.Len(t, members, 2)
member = members[0]
defer ss.Channel().RemoveMember(channel.Id, u1.Id)
defer ss.Channel().RemoveMember(channel.Id, u2.Id)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert members correctly (in channel with channel scheme)", func(t *testing.T) {
cs := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
cs, nErr := ss.Scheme().Save(cs)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
SchemeId: &cs.Id,
}, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: cs.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: cs.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: cs.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: cs.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test " + cs.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u1.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
NotifyProps: defaultNotifyProps,
}
otherMember := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u2.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
NotifyProps: defaultNotifyProps,
}
members, err := ss.Channel().SaveMultipleMembers([]*model.ChannelMember{member, otherMember})
require.NoError(t, err)
require.Len(t, members, 2)
member = members[0]
defer ss.Channel().RemoveMember(channel.Id, u1.Id)
defer ss.Channel().RemoveMember(channel.Id, u2.Id)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
}
func testChannelUpdateMember(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
defaultNotifyProps := model.GetDefaultChannelNotifyProps()
t.Run("not valid channel member", func(t *testing.T) {
member := &model.ChannelMember{ChannelId: "wrong", UserId: u1.Id, NotifyProps: defaultNotifyProps}
_, nErr := ss.Channel().UpdateMember(member)
require.Error(t, nErr)
var appErr *model.AppError
require.True(t, errors.As(nErr, &appErr))
require.Equal(t, "model.channel_member.is_valid.channel_id.app_error", appErr.Id)
})
t.Run("insert member correctly (in channel without channel scheme and team without scheme)", func(t *testing.T) {
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
channel := &model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u1.Id,
NotifyProps: defaultNotifyProps,
}
member, nErr = ss.Channel().SaveMember(member)
require.NoError(t, nErr)
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: "channel_user",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: "channel_user",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: "channel_guest",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: "channel_guest",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: "channel_user channel_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: "channel_user channel_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test channel_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test channel_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_user channel_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test channel_user channel_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member.SchemeGuest = tc.SchemeGuest
member.SchemeUser = tc.SchemeUser
member.SchemeAdmin = tc.SchemeAdmin
member.ExplicitRoles = tc.ExplicitRoles
member, nErr = ss.Channel().UpdateMember(member)
require.NoError(t, nErr)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert member correctly (in channel without scheme and team with scheme)", func(t *testing.T) {
ts := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
ts, nErr := ss.Scheme().Save(ts)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &ts.Id,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
channel := &model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u1.Id,
NotifyProps: defaultNotifyProps,
}
member, nErr = ss.Channel().SaveMember(member)
require.NoError(t, nErr)
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: ts.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: ts.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: ts.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: ts.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test " + ts.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member.SchemeGuest = tc.SchemeGuest
member.SchemeUser = tc.SchemeUser
member.SchemeAdmin = tc.SchemeAdmin
member.ExplicitRoles = tc.ExplicitRoles
member, nErr = ss.Channel().UpdateMember(member)
require.NoError(t, nErr)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert member correctly (in channel with channel scheme)", func(t *testing.T) {
cs := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
cs, nErr := ss.Scheme().Save(cs)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
SchemeId: &cs.Id,
}, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u1.Id,
NotifyProps: defaultNotifyProps,
}
member, nErr = ss.Channel().SaveMember(member)
require.NoError(t, nErr)
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: cs.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: cs.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: cs.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: cs.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test " + cs.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member.SchemeGuest = tc.SchemeGuest
member.SchemeUser = tc.SchemeUser
member.SchemeAdmin = tc.SchemeAdmin
member.ExplicitRoles = tc.ExplicitRoles
member, nErr = ss.Channel().UpdateMember(member)
require.NoError(t, nErr)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
}
func testChannelUpdateMultipleMembers(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
defaultNotifyProps := model.GetDefaultChannelNotifyProps()
t.Run("any not valid channel member", func(t *testing.T) {
m1 := &model.ChannelMember{ChannelId: "wrong", UserId: u1.Id, NotifyProps: defaultNotifyProps}
m2 := &model.ChannelMember{ChannelId: model.NewId(), UserId: u2.Id, NotifyProps: defaultNotifyProps}
_, nErr := ss.Channel().SaveMultipleMembers([]*model.ChannelMember{m1, m2})
require.Error(t, nErr)
var appErr *model.AppError
require.True(t, errors.As(nErr, &appErr))
require.Equal(t, "model.channel_member.is_valid.channel_id.app_error", appErr.Id)
})
t.Run("duplicated entries should fail", func(t *testing.T) {
channelID1 := model.NewId()
m1 := &model.ChannelMember{ChannelId: channelID1, UserId: u1.Id, NotifyProps: defaultNotifyProps}
m2 := &model.ChannelMember{ChannelId: channelID1, UserId: u1.Id, NotifyProps: defaultNotifyProps}
_, nErr := ss.Channel().SaveMultipleMembers([]*model.ChannelMember{m1, m2})
require.Error(t, nErr)
require.IsType(t, &store.ErrConflict{}, nErr)
})
t.Run("insert members correctly (in channel without channel scheme and team without scheme)", func(t *testing.T) {
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
channel := &model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
member := &model.ChannelMember{ChannelId: channel.Id, UserId: u1.Id, NotifyProps: defaultNotifyProps}
otherMember := &model.ChannelMember{ChannelId: channel.Id, UserId: u2.Id, NotifyProps: defaultNotifyProps}
var members []*model.ChannelMember
members, nErr = ss.Channel().SaveMultipleMembers([]*model.ChannelMember{member, otherMember})
require.NoError(t, nErr)
defer ss.Channel().RemoveMember(channel.Id, u1.Id)
defer ss.Channel().RemoveMember(channel.Id, u2.Id)
require.Len(t, members, 2)
member = members[0]
otherMember = members[1]
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: "channel_user",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: "channel_user",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: "channel_guest",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: "channel_guest",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: "channel_user channel_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: "channel_user channel_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test channel_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test channel_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test channel_user channel_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test channel_user channel_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member.SchemeGuest = tc.SchemeGuest
member.SchemeUser = tc.SchemeUser
member.SchemeAdmin = tc.SchemeAdmin
member.ExplicitRoles = tc.ExplicitRoles
var members []*model.ChannelMember
members, nErr = ss.Channel().UpdateMultipleMembers([]*model.ChannelMember{member, otherMember})
require.NoError(t, nErr)
require.Len(t, members, 2)
member = members[0]
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert members correctly (in channel without scheme and team with scheme)", func(t *testing.T) {
ts := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
ts, nErr := ss.Scheme().Save(ts)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &ts.Id,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
channel := &model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
member := &model.ChannelMember{ChannelId: channel.Id, UserId: u1.Id, NotifyProps: defaultNotifyProps}
otherMember := &model.ChannelMember{ChannelId: channel.Id, UserId: u2.Id, NotifyProps: defaultNotifyProps}
var members []*model.ChannelMember
members, nErr = ss.Channel().SaveMultipleMembers([]*model.ChannelMember{member, otherMember})
require.NoError(t, nErr)
defer ss.Channel().RemoveMember(channel.Id, u1.Id)
defer ss.Channel().RemoveMember(channel.Id, u2.Id)
require.Len(t, members, 2)
member = members[0]
otherMember = members[1]
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: ts.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: ts.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: ts.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: ts.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test " + ts.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test " + ts.DefaultChannelUserRole + " " + ts.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member.SchemeGuest = tc.SchemeGuest
member.SchemeUser = tc.SchemeUser
member.SchemeAdmin = tc.SchemeAdmin
member.ExplicitRoles = tc.ExplicitRoles
var members []*model.ChannelMember
members, nErr = ss.Channel().UpdateMultipleMembers([]*model.ChannelMember{member, otherMember})
require.NoError(t, nErr)
require.Len(t, members, 2)
member = members[0]
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert members correctly (in channel with channel scheme)", func(t *testing.T) {
cs := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
cs, nErr := ss.Scheme().Save(cs)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
DisplayName: "DisplayName",
Name: "z-z-z" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
SchemeId: &cs.Id,
}, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
member := &model.ChannelMember{ChannelId: channel.Id, UserId: u1.Id, NotifyProps: defaultNotifyProps}
otherMember := &model.ChannelMember{ChannelId: channel.Id, UserId: u2.Id, NotifyProps: defaultNotifyProps}
members, err := ss.Channel().SaveMultipleMembers([]*model.ChannelMember{member, otherMember})
require.NoError(t, err)
defer ss.Channel().RemoveMember(channel.Id, u1.Id)
defer ss.Channel().RemoveMember(channel.Id, u2.Id)
require.Len(t, members, 2)
member = members[0]
otherMember = members[1]
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "channel user implicit",
SchemeUser: true,
ExpectedRoles: cs.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit",
ExplicitRoles: "channel_user",
ExpectedRoles: cs.DefaultChannelUserRole,
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit",
SchemeGuest: true,
ExpectedRoles: cs.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit",
ExplicitRoles: "channel_guest",
ExpectedRoles: cs.DefaultChannelGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit",
ExplicitRoles: "channel_user channel_admin",
ExpectedRoles: cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel user explicit and explicit custom role",
ExplicitRoles: "channel_user test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "channel guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel guest explicit and explicit custom role",
ExplicitRoles: "channel_guest test",
ExpectedRoles: "test " + cs.DefaultChannelGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "channel admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel admin explicit and explicit custom role",
ExplicitRoles: "channel_user channel_admin test",
ExpectedRoles: "test " + cs.DefaultChannelUserRole + " " + cs.DefaultChannelAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "channel member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member.SchemeGuest = tc.SchemeGuest
member.SchemeUser = tc.SchemeUser
member.SchemeAdmin = tc.SchemeAdmin
member.ExplicitRoles = tc.ExplicitRoles
members, err := ss.Channel().UpdateMultipleMembers([]*model.ChannelMember{member, otherMember})
require.NoError(t, err)
require.Len(t, members, 2)
member = members[0]
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
}
func testChannelUpdateMemberNotifyProps(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
defaultNotifyProps := model.GetDefaultChannelNotifyProps()
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
channel := &model.Channel{
DisplayName: "DisplayName",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
defer func() { ss.Channel().PermanentDelete(channel.Id) }()
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: u1.Id,
NotifyProps: defaultNotifyProps,
}
member, nErr = ss.Channel().SaveMember(member)
require.NoError(t, nErr)
props := member.NotifyProps
props["hello"] = "world"
props[model.DesktopNotifyProp] = model.ChannelNotifyAll
member, nErr = ss.Channel().UpdateMemberNotifyProps(member.ChannelId, member.UserId, props)
require.NoError(t, nErr)
// Verify props.
assert.Equal(t, props, member.NotifyProps)
}
func testChannelRemoveMember(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u3, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u4, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
channelID := model.NewId()
defaultNotifyProps := model.GetDefaultChannelNotifyProps()
m1 := &model.ChannelMember{ChannelId: channelID, UserId: u1.Id, NotifyProps: defaultNotifyProps}
m2 := &model.ChannelMember{ChannelId: channelID, UserId: u2.Id, NotifyProps: defaultNotifyProps}
m3 := &model.ChannelMember{ChannelId: channelID, UserId: u3.Id, NotifyProps: defaultNotifyProps}
m4 := &model.ChannelMember{ChannelId: channelID, UserId: u4.Id, NotifyProps: defaultNotifyProps}
_, nErr := ss.Channel().SaveMultipleMembers([]*model.ChannelMember{m1, m2, m3, m4})
require.NoError(t, nErr)
t.Run("remove member from not existing channel", func(t *testing.T) {
nErr = ss.Channel().RemoveMember("not-existing-channel", u1.Id)
require.NoError(t, nErr)
var membersCount int64
membersCount, nErr = ss.Channel().GetMemberCount(channelID, false)
require.NoError(t, nErr)
require.Equal(t, int64(4), membersCount)
})
t.Run("remove not existing member from an existing channel", func(t *testing.T) {
nErr = ss.Channel().RemoveMember(channelID, model.NewId())
require.NoError(t, nErr)
var membersCount int64
membersCount, nErr = ss.Channel().GetMemberCount(channelID, false)
require.NoError(t, nErr)
require.Equal(t, int64(4), membersCount)
})
t.Run("remove existing member from an existing channel", func(t *testing.T) {
nErr = ss.Channel().RemoveMember(channelID, u1.Id)
require.NoError(t, nErr)
defer ss.Channel().SaveMember(m1)
var membersCount int64
membersCount, nErr = ss.Channel().GetMemberCount(channelID, false)
require.NoError(t, nErr)
require.Equal(t, int64(3), membersCount)
})
}
func testChannelRemoveMembers(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u3, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u4, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
channelID := model.NewId()
defaultNotifyProps := model.GetDefaultChannelNotifyProps()
m1 := &model.ChannelMember{ChannelId: channelID, UserId: u1.Id, NotifyProps: defaultNotifyProps}
m2 := &model.ChannelMember{ChannelId: channelID, UserId: u2.Id, NotifyProps: defaultNotifyProps}
m3 := &model.ChannelMember{ChannelId: channelID, UserId: u3.Id, NotifyProps: defaultNotifyProps}
m4 := &model.ChannelMember{ChannelId: channelID, UserId: u4.Id, NotifyProps: defaultNotifyProps}
_, nErr := ss.Channel().SaveMultipleMembers([]*model.ChannelMember{m1, m2, m3, m4})
require.NoError(t, nErr)
t.Run("remove members from not existing channel", func(t *testing.T) {
nErr = ss.Channel().RemoveMembers("not-existing-channel", []string{u1.Id, u2.Id, u3.Id, u4.Id})
require.NoError(t, nErr)
var membersCount int64
membersCount, nErr = ss.Channel().GetMemberCount(channelID, false)
require.NoError(t, nErr)
require.Equal(t, int64(4), membersCount)
})
t.Run("remove not existing members from an existing channel", func(t *testing.T) {
nErr = ss.Channel().RemoveMembers(channelID, []string{model.NewId(), model.NewId()})
require.NoError(t, nErr)
var membersCount int64
membersCount, nErr = ss.Channel().GetMemberCount(channelID, false)
require.NoError(t, nErr)
require.Equal(t, int64(4), membersCount)
})
t.Run("remove not existing and not existing members from an existing channel", func(t *testing.T) {
nErr = ss.Channel().RemoveMembers(channelID, []string{u1.Id, u2.Id, model.NewId(), model.NewId()})
require.NoError(t, nErr)
defer ss.Channel().SaveMultipleMembers([]*model.ChannelMember{m1, m2})
var membersCount int64
membersCount, nErr = ss.Channel().GetMemberCount(channelID, false)
require.NoError(t, nErr)
require.Equal(t, int64(2), membersCount)
})
t.Run("remove existing members from an existing channel", func(t *testing.T) {
nErr = ss.Channel().RemoveMembers(channelID, []string{u1.Id, u2.Id, u3.Id})
require.NoError(t, nErr)
defer ss.Channel().SaveMultipleMembers([]*model.ChannelMember{m1, m2, m3})
membersCount, err := ss.Channel().GetMemberCount(channelID, false)
require.NoError(t, err)
require.Equal(t, int64(1), membersCount)
})
}
func testChannelDeleteMemberStore(t *testing.T, ss store.Store) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "NameName"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, nErr := ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
c1t1, _ := ss.Channel().Get(c1.Id, false)
assert.EqualValues(t, 0, c1t1.ExtraUpdateAt, "ExtraUpdateAt should be 0")
u1 := model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(&u1)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(&u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
o1 := model.ChannelMember{}
o1.ChannelId = c1.Id
o1.UserId = u1.Id
o1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, nErr = ss.Channel().SaveMember(&o1)
require.NoError(t, nErr)
o2 := model.ChannelMember{}
o2.ChannelId = c1.Id
o2.UserId = u2.Id
o2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, nErr = ss.Channel().SaveMember(&o2)
require.NoError(t, nErr)
c1t2, _ := ss.Channel().Get(c1.Id, false)
assert.EqualValues(t, 0, c1t2.ExtraUpdateAt, "ExtraUpdateAt should be 0")
count, nErr := ss.Channel().GetMemberCount(o1.ChannelId, false)
require.NoError(t, nErr)
require.EqualValues(t, 2, count, "should have saved 2 members")
nErr = ss.Channel().PermanentDeleteMembersByUser(o2.UserId)
require.NoError(t, nErr)
count, nErr = ss.Channel().GetMemberCount(o1.ChannelId, false)
require.NoError(t, nErr)
require.EqualValues(t, 1, count, "should have removed 1 member")
nErr = ss.Channel().PermanentDeleteMembersByChannel(o1.ChannelId)
require.NoError(t, nErr)
count, nErr = ss.Channel().GetMemberCount(o1.ChannelId, false)
require.NoError(t, nErr)
require.EqualValues(t, 0, count, "should have removed all members")
}
func testChannelStoreGetChannels(t *testing.T, ss store.Store) {
team := model.NewId()
o1 := &model.Channel{}
o1.TeamId = team
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
var nErr error
o1, nErr = ss.Channel().Save(o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{}
o2.TeamId = team
o2.DisplayName = "Channel2"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
o3 := model.Channel{}
o3.TeamId = team
o3.DisplayName = "Channel3"
o3.Name = NewTestId()
o3.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err := ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = model.NewId()
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
m3 := model.ChannelMember{}
m3.ChannelId = o2.Id
m3.UserId = m1.UserId
m3.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m3)
require.NoError(t, err)
m4 := model.ChannelMember{}
m4.ChannelId = o3.Id
m4.UserId = m1.UserId
m4.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m4)
require.NoError(t, err)
list, nErr := ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
})
require.NoError(t, nErr)
require.Len(t, list, 3)
require.Equal(t, o1.Id, list[0].Id, "missing channel")
require.Equal(t, o2.Id, list[1].Id, "missing channel")
require.Equal(t, o3.Id, list[2].Id, "missing channel")
ids, err := ss.Channel().GetAllChannelMembersForUser(m1.UserId, false, false)
require.NoError(t, err)
_, ok := ids[o1.Id]
require.True(t, ok, "missing channel")
ids2, err := ss.Channel().GetAllChannelMembersForUser(m1.UserId, true, false)
require.NoError(t, err)
_, ok = ids2[o1.Id]
require.True(t, ok, "missing channel")
ids3, err := ss.Channel().GetAllChannelMembersForUser(m1.UserId, true, false)
require.NoError(t, err)
_, ok = ids3[o1.Id]
require.True(t, ok, "missing channel")
ids4, err := ss.Channel().GetAllChannelMembersForUser(m1.UserId, true, true)
require.NoError(t, err)
_, ok = ids4[o1.Id]
require.True(t, ok, "missing channel")
// Sleeping to guarantee that the
// UpdateAt is different.
// The proper way would be to set UpdateAt during channel creation itself,
// but the *Channel.PreSave method ignores any existing CreateAt value.
// TODO: check if using an existing CreateAt breaks anything.
time.Sleep(time.Millisecond)
now := model.GetMillis()
_, nErr = ss.Channel().Update(o1)
require.NoError(t, nErr)
list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastUpdateAt: int(now),
})
require.NoError(t, nErr)
// should return 1
require.Len(t, list, 1)
nErr = ss.Channel().Delete(o2.Id, 10)
require.NoError(t, nErr)
nErr = ss.Channel().Delete(o3.Id, 20)
require.NoError(t, nErr)
// should return 1
list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
})
require.NoError(t, nErr)
require.Len(t, list, 1)
require.Equal(t, o1.Id, list[0].Id, "missing channel")
// Should return all
list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 0,
})
require.NoError(t, nErr)
require.Len(t, list, 3)
require.Equal(t, o1.Id, list[0].Id, "missing channel")
require.Equal(t, o2.Id, list[1].Id, "missing channel")
require.Equal(t, o3.Id, list[2].Id, "missing channel")
// Should still return all
list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 10,
})
require.NoError(t, nErr)
require.Len(t, list, 3)
require.Equal(t, o1.Id, list[0].Id, "missing channel")
require.Equal(t, o2.Id, list[1].Id, "missing channel")
require.Equal(t, o3.Id, list[2].Id, "missing channel")
// Should return 2
list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 20,
})
require.NoError(t, nErr)
require.Len(t, list, 2)
require.Equal(t, o1.Id, list[0].Id, "missing channel")
require.Equal(t, o3.Id, list[1].Id, "missing channel")
require.True(
t,
ss.Channel().IsUserInChannelUseCache(m1.UserId, o1.Id),
"missing channel")
require.True(
t,
ss.Channel().IsUserInChannelUseCache(m1.UserId, o2.Id),
"missing channel")
require.False(
t,
ss.Channel().IsUserInChannelUseCache(m1.UserId, "blahblah"),
"missing channel")
require.False(
t,
ss.Channel().IsUserInChannelUseCache("blahblah", "blahblah"),
"missing channel")
ss.Channel().InvalidateAllChannelMembersForUser(m1.UserId)
}
func testChannelStoreGetChannelsWithCursor(t *testing.T, ss store.Store) {
teamID := model.NewId()
o1 := &model.Channel{}
o1.TeamId = teamID
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
var nErr error
o1, nErr = ss.Channel().Save(o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{}
o2.TeamId = teamID
o2.DisplayName = "Channel2"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
o3 := model.Channel{}
o3.TeamId = teamID
o3.DisplayName = "Channel3"
o3.Name = NewTestId()
o3.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err := ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = model.NewId()
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
m3 := model.ChannelMember{}
m3.ChannelId = o2.Id
m3.UserId = m1.UserId
m3.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m3)
require.NoError(t, err)
m4 := model.ChannelMember{}
m4.ChannelId = o3.Id
m4.UserId = m1.UserId
m4.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m4)
require.NoError(t, err)
list, nErr := ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
PerPage: model.NewInt(2),
}, "")
require.NoError(t, nErr)
require.Len(t, list, 2)
require.Equal(t, teamID, list[0].TeamId, "incorrect teamID")
require.Equal(t, teamID, list[1].TeamId, "incorrect teamID")
list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
PerPage: model.NewInt(2),
}, list[1].Id)
require.NoError(t, nErr)
require.Len(t, list, 1)
require.Equal(t, teamID, list[0].TeamId, "incorrect teamID")
// all channels should be returned
list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
}, "")
require.NoError(t, nErr)
require.Len(t, list, 3)
// should return empty list
list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
}, list[2].Id)
require.NoError(t, nErr)
require.Len(t, list, 0)
// Sleeping to guarantee that the
// UpdateAt is different.
// The proper way would be to set UpdateAt during channel creation itself,
// but the *Channel.PreSave method ignores any existing CreateAt value.
// TODO: check if using an existing CreateAt breaks anything.
time.Sleep(time.Millisecond)
now := model.GetMillis()
_, nErr = ss.Channel().Update(o1)
require.NoError(t, nErr)
list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastUpdateAt: int(now),
}, "")
require.NoError(t, nErr)
// should return 1
require.Len(t, list, 1)
nErr = ss.Channel().Delete(o2.Id, 10)
require.NoError(t, nErr)
nErr = ss.Channel().Delete(o3.Id, 20)
require.NoError(t, nErr)
// should return 1
list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
}, "")
require.NoError(t, nErr)
require.Len(t, list, 1)
// Should return all
list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 0,
PerPage: model.NewInt(2),
}, "")
require.NoError(t, nErr)
require.Len(t, list, 2)
list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 0,
PerPage: model.NewInt(2),
}, list[1].Id)
require.NoError(t, nErr)
require.Len(t, list, 1)
// Should still return all
list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 10,
}, "")
require.NoError(t, nErr)
require.Len(t, list, 3)
// Should return 2
list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 20,
}, "")
require.NoError(t, nErr)
require.Len(t, list, 2)
}
func testChannelStoreGetChannelsByUser(t *testing.T, ss store.Store) {
team := model.NewId()
team2 := model.NewId()
o1 := model.Channel{}
o1.TeamId = team
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{}
o2.TeamId = team
o2.DisplayName = "Channel2"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
o3 := model.Channel{}
o3.TeamId = team2
o3.DisplayName = "Channel3"
o3.Name = NewTestId()
o3.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err := ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = model.NewId()
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
m3 := model.ChannelMember{}
m3.ChannelId = o2.Id
m3.UserId = m1.UserId
m3.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m3)
require.NoError(t, err)
m4 := model.ChannelMember{}
m4.ChannelId = o3.Id
m4.UserId = m1.UserId
m4.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m4)
require.NoError(t, err)
list, nErr := ss.Channel().GetChannelsByUser(m1.UserId, false, 0, -1, "")
require.NoError(t, nErr)
require.Len(t, list, 3)
require.ElementsMatch(t, []string{o1.Id, o2.Id, o3.Id}, []string{list[0].Id, list[1].Id, list[2].Id}, "channels did not match")
nErr = ss.Channel().Delete(o2.Id, 10)
require.NoError(t, nErr)
nErr = ss.Channel().Delete(o3.Id, 20)
require.NoError(t, nErr)
// should return 1
list, nErr = ss.Channel().GetChannelsByUser(m1.UserId, false, 0, -1, "")
require.NoError(t, nErr)
require.Len(t, list, 1)
require.Equal(t, o1.Id, list[0].Id, "missing channel")
// Should return all
list, nErr = ss.Channel().GetChannelsByUser(m1.UserId, true, 0, -1, "")
require.NoError(t, nErr)
require.Len(t, list, 3)
require.ElementsMatch(t, []string{o1.Id, o2.Id, o3.Id}, []string{list[0].Id, list[1].Id, list[2].Id}, "channels did not match")
// Should still return all
list, nErr = ss.Channel().GetChannelsByUser(m1.UserId, true, 10, -1, "")
require.NoError(t, nErr)
require.Len(t, list, 3)
require.ElementsMatch(t, []string{o1.Id, o2.Id, o3.Id}, []string{list[0].Id, list[1].Id, list[2].Id}, "channels did not match")
// Should return 2
list, nErr = ss.Channel().GetChannelsByUser(m1.UserId, true, 20, -1, "")
require.NoError(t, nErr)
require.Len(t, list, 2)
require.ElementsMatch(t, []string{o1.Id, o3.Id}, []string{list[0].Id, list[1].Id}, "channels did not match")
}
func testChannelStoreGetAllChannels(t *testing.T, ss store.Store, s SqlStore) {
cleanupChannels(t, ss)
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
t2 := model.Team{}
t2.DisplayName = "Name2"
t2.Name = NewTestId()
t2.Email = MakeEmail()
t2.Type = model.TeamOpen
_, err = ss.Team().Save(&t2)
require.NoError(t, err)
c1 := model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel1" + model.NewId()
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&c1, -1)
require.NoError(t, nErr)
group := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
_, err = ss.Group().Create(group)
require.NoError(t, err)
_, err = ss.Group().CreateGroupSyncable(model.NewGroupChannel(group.Id, c1.Id, true))
require.NoError(t, err)
c2 := model.Channel{}
c2.TeamId = t1.Id
c2.DisplayName = "Channel2" + model.NewId()
c2.Name = NewTestId()
c2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&c2, -1)
require.NoError(t, nErr)
c2.DeleteAt = model.GetMillis()
c2.UpdateAt = c2.DeleteAt
nErr = ss.Channel().Delete(c2.Id, c2.DeleteAt)
require.NoError(t, nErr, "channel should have been deleted")
c3 := model.Channel{}
c3.TeamId = t2.Id
c3.DisplayName = "Channel3" + model.NewId()
c3.Name = NewTestId()
c3.Type = model.ChannelTypePrivate
_, nErr = ss.Channel().Save(&c3, -1)
require.NoError(t, nErr)
u1 := model.User{Id: model.NewId()}
u2 := model.User{Id: model.NewId()}
_, nErr = ss.Channel().CreateDirectChannel(&u1, &u2)
require.NoError(t, nErr)
userIds := []string{model.NewId(), model.NewId(), model.NewId()}
c5 := model.Channel{}
c5.Name = model.GetGroupNameFromUserIds(userIds)
c5.DisplayName = "GroupChannel" + model.NewId()
c5.Name = NewTestId()
c5.Type = model.ChannelTypeGroup
_, nErr = ss.Channel().Save(&c5, -1)
require.NoError(t, nErr)
list, nErr := ss.Channel().GetAllChannels(0, 10, store.ChannelSearchOpts{})
require.NoError(t, nErr)
assert.Len(t, list, 2)
assert.Equal(t, c1.Id, list[0].Id)
assert.Equal(t, "Name", list[0].TeamDisplayName)
assert.Equal(t, c3.Id, list[1].Id)
assert.Equal(t, "Name2", list[1].TeamDisplayName)
count1, nErr := ss.Channel().GetAllChannelsCount(store.ChannelSearchOpts{})
require.NoError(t, nErr)
list, nErr = ss.Channel().GetAllChannels(0, 10, store.ChannelSearchOpts{IncludeDeleted: true})
require.NoError(t, nErr)
assert.Len(t, list, 3)
assert.Equal(t, c1.Id, list[0].Id)
assert.Equal(t, "Name", list[0].TeamDisplayName)
assert.Equal(t, c2.Id, list[1].Id)
assert.Equal(t, c3.Id, list[2].Id)
count2, nErr := ss.Channel().GetAllChannelsCount(store.ChannelSearchOpts{IncludeDeleted: true})
require.NoError(t, nErr)
require.True(t, func() bool {
return count2 > count1
}())
list, nErr = ss.Channel().GetAllChannels(0, 1, store.ChannelSearchOpts{IncludeDeleted: true})
require.NoError(t, nErr)
assert.Len(t, list, 1)
assert.Equal(t, c1.Id, list[0].Id)
assert.Equal(t, "Name", list[0].TeamDisplayName)
// Not associated to group
list, nErr = ss.Channel().GetAllChannels(0, 10, store.ChannelSearchOpts{NotAssociatedToGroup: group.Id})
require.NoError(t, nErr)
assert.Len(t, list, 1)
// Exclude channel names
list, nErr = ss.Channel().GetAllChannels(0, 10, store.ChannelSearchOpts{ExcludeChannelNames: []string{c1.Name}})
require.NoError(t, nErr)
assert.Len(t, list, 1)
// Exclude policy constrained
policy, nErr := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "Policy 1",
PostDurationDays: model.NewInt64(30),
},
ChannelIDs: []string{c1.Id},
})
require.NoError(t, nErr)
list, nErr = ss.Channel().GetAllChannels(0, 10, store.ChannelSearchOpts{ExcludePolicyConstrained: true})
require.NoError(t, nErr)
assert.Len(t, list, 1)
assert.Equal(t, c3.Id, list[0].Id)
// Without the policy ID
list, nErr = ss.Channel().GetAllChannels(0, 1, store.ChannelSearchOpts{})
require.NoError(t, nErr)
assert.Len(t, list, 1)
assert.Equal(t, c1.Id, list[0].Id)
assert.Nil(t, list[0].PolicyID)
// With the policy ID
list, nErr = ss.Channel().GetAllChannels(0, 1, store.ChannelSearchOpts{IncludePolicyID: true})
require.NoError(t, nErr)
assert.Len(t, list, 1)
assert.Equal(t, c1.Id, list[0].Id)
assert.Equal(t, *list[0].PolicyID, policy.ID)
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testChannelStoreGetMoreChannels(t *testing.T, ss store.Store) {
teamId := model.NewId()
otherTeamId := model.NewId()
userId := model.NewId()
otherUserId1 := model.NewId()
otherUserId2 := model.NewId()
// o1 is a channel on the team to which the user (and the other user 1) belongs
o1 := model.Channel{
TeamId: teamId,
DisplayName: "Channel1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
_, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: o1.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: o1.Id,
UserId: otherUserId1,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
// o2 is a channel on the other team to which the user belongs
o2 := model.Channel{
TeamId: otherTeamId,
DisplayName: "Channel2",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: o2.Id,
UserId: otherUserId2,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
// o3 is a channel on the team to which the user does not belong, and thus should show up
// in "more channels"
o3 := model.Channel{
TeamId: teamId,
DisplayName: "ChannelA",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
// o4 is a private channel on the team to which the user does not belong
o4 := model.Channel{
TeamId: teamId,
DisplayName: "ChannelB",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o4, -1)
require.NoError(t, nErr)
// o5 is another private channel on the team to which the user does belong
o5 := model.Channel{
TeamId: teamId,
DisplayName: "ChannelC",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o5, -1)
require.NoError(t, nErr)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: o5.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
t.Run("only o3 listed in more channels", func(t *testing.T) {
list, channelErr := ss.Channel().GetMoreChannels(teamId, userId, 0, 100)
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{&o3}, list)
})
// o6 is another channel on the team to which the user does not belong, and would thus
// start showing up in "more channels".
o6 := model.Channel{
TeamId: teamId,
DisplayName: "ChannelD",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o6, -1)
require.NoError(t, nErr)
// o7 is another channel on the team to which the user does not belong, but is deleted,
// and thus would not start showing up in "more channels"
o7 := model.Channel{
TeamId: teamId,
DisplayName: "ChannelD",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o7, -1)
require.NoError(t, nErr)
nErr = ss.Channel().Delete(o7.Id, model.GetMillis())
require.NoError(t, nErr, "channel should have been deleted")
t.Run("both o3 and o6 listed in more channels", func(t *testing.T) {
list, err := ss.Channel().GetMoreChannels(teamId, userId, 0, 100)
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o3, &o6}, list)
})
t.Run("only o3 listed in more channels with offset 0, limit 1", func(t *testing.T) {
list, err := ss.Channel().GetMoreChannels(teamId, userId, 0, 1)
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o3}, list)
})
t.Run("only o6 listed in more channels with offset 1, limit 1", func(t *testing.T) {
list, err := ss.Channel().GetMoreChannels(teamId, userId, 1, 1)
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o6}, list)
})
t.Run("verify analytics for open channels", func(t *testing.T) {
count, err := ss.Channel().AnalyticsTypeCount(teamId, model.ChannelTypeOpen)
require.NoError(t, err)
require.EqualValues(t, 4, count)
})
t.Run("verify analytics for private channels", func(t *testing.T) {
count, err := ss.Channel().AnalyticsTypeCount(teamId, model.ChannelTypePrivate)
require.NoError(t, err)
require.EqualValues(t, 2, count)
})
t.Run("verify analytics for all channels", func(t *testing.T) {
count, err := ss.Channel().AnalyticsTypeCount(teamId, "")
require.NoError(t, err)
require.EqualValues(t, 6, count)
})
}
func testChannelStoreGetPrivateChannelsForTeam(t *testing.T, ss store.Store) {
teamId := model.NewId()
// p1 is a private channel on the team
p1 := model.Channel{
TeamId: teamId,
DisplayName: "PrivateChannel1Team1",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr := ss.Channel().Save(&p1, -1)
require.NoError(t, nErr)
// p2 is a private channel on another team
p2 := model.Channel{
TeamId: model.NewId(),
DisplayName: "PrivateChannel1Team2",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&p2, -1)
require.NoError(t, nErr)
// o1 is a public channel on the team
o1 := model.Channel{
TeamId: teamId,
DisplayName: "OpenChannel1Team1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
t.Run("only p1 initially listed in private channels", func(t *testing.T) {
list, channelErr := ss.Channel().GetPrivateChannelsForTeam(teamId, 0, 100)
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{&p1}, list)
})
// p3 is another private channel on the team
p3 := model.Channel{
TeamId: teamId,
DisplayName: "PrivateChannel2Team1",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&p3, -1)
require.NoError(t, nErr)
// p4 is another private, but deleted channel on the team
p4 := model.Channel{
TeamId: teamId,
DisplayName: "PrivateChannel3Team1",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&p4, -1)
require.NoError(t, nErr)
err := ss.Channel().Delete(p4.Id, model.GetMillis())
require.NoError(t, err, "channel should have been deleted")
t.Run("both p1 and p3 listed in private channels", func(t *testing.T) {
list, err := ss.Channel().GetPrivateChannelsForTeam(teamId, 0, 100)
require.NoError(t, err)
require.Equal(t, model.ChannelList{&p1, &p3}, list)
})
t.Run("only p1 listed in private channels with offset 0, limit 1", func(t *testing.T) {
list, err := ss.Channel().GetPrivateChannelsForTeam(teamId, 0, 1)
require.NoError(t, err)
require.Equal(t, model.ChannelList{&p1}, list)
})
t.Run("only p3 listed in private channels with offset 1, limit 1", func(t *testing.T) {
list, err := ss.Channel().GetPrivateChannelsForTeam(teamId, 1, 1)
require.NoError(t, err)
require.Equal(t, model.ChannelList{&p3}, list)
})
t.Run("verify analytics for private channels", func(t *testing.T) {
count, err := ss.Channel().AnalyticsTypeCount(teamId, model.ChannelTypePrivate)
require.NoError(t, err)
require.EqualValues(t, 3, count)
})
t.Run("verify analytics for open open channels", func(t *testing.T) {
count, err := ss.Channel().AnalyticsTypeCount(teamId, model.ChannelTypeOpen)
require.NoError(t, err)
require.EqualValues(t, 1, count)
})
}
func testChannelStoreGetPublicChannelsForTeam(t *testing.T, ss store.Store) {
teamId := model.NewId()
// o1 is a public channel on the team
o1 := model.Channel{
TeamId: teamId,
DisplayName: "OpenChannel1Team1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
// o2 is a public channel on another team
o2 := model.Channel{
TeamId: model.NewId(),
DisplayName: "OpenChannel1Team2",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
// o3 is a private channel on the team
o3 := model.Channel{
TeamId: teamId,
DisplayName: "PrivateChannel1Team1",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
t.Run("only o1 initially listed in public channels", func(t *testing.T) {
list, channelErr := ss.Channel().GetPublicChannelsForTeam(teamId, 0, 100)
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{&o1}, list)
})
// o4 is another public channel on the team
o4 := model.Channel{
TeamId: teamId,
DisplayName: "OpenChannel2Team1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o4, -1)
require.NoError(t, nErr)
// o5 is another public, but deleted channel on the team
o5 := model.Channel{
TeamId: teamId,
DisplayName: "OpenChannel3Team1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o5, -1)
require.NoError(t, nErr)
err := ss.Channel().Delete(o5.Id, model.GetMillis())
require.NoError(t, err, "channel should have been deleted")
t.Run("both o1 and o4 listed in public channels", func(t *testing.T) {
list, err := ss.Channel().GetPublicChannelsForTeam(teamId, 0, 100)
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o1, &o4}, list)
})
t.Run("only o1 listed in public channels with offset 0, limit 1", func(t *testing.T) {
list, err := ss.Channel().GetPublicChannelsForTeam(teamId, 0, 1)
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o1}, list)
})
t.Run("only o4 listed in public channels with offset 1, limit 1", func(t *testing.T) {
list, err := ss.Channel().GetPublicChannelsForTeam(teamId, 1, 1)
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o4}, list)
})
t.Run("verify analytics for open channels", func(t *testing.T) {
count, err := ss.Channel().AnalyticsTypeCount(teamId, model.ChannelTypeOpen)
require.NoError(t, err)
require.EqualValues(t, 3, count)
})
t.Run("verify analytics for private channels", func(t *testing.T) {
count, err := ss.Channel().AnalyticsTypeCount(teamId, model.ChannelTypePrivate)
require.NoError(t, err)
require.EqualValues(t, 1, count)
})
}
func testChannelStoreGetPublicChannelsByIdsForTeam(t *testing.T, ss store.Store) {
teamId := model.NewId()
// oc1 is a public channel on the team
oc1 := model.Channel{
TeamId: teamId,
DisplayName: "OpenChannel1Team1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&oc1, -1)
require.NoError(t, nErr)
// oc2 is a public channel on another team
oc2 := model.Channel{
TeamId: model.NewId(),
DisplayName: "OpenChannel2TeamOther",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&oc2, -1)
require.NoError(t, nErr)
// pc3 is a private channel on the team
pc3 := model.Channel{
TeamId: teamId,
DisplayName: "PrivateChannel3Team1",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&pc3, -1)
require.NoError(t, nErr)
t.Run("oc1 by itself should be found as a public channel in the team", func(t *testing.T) {
list, channelErr := ss.Channel().GetPublicChannelsByIdsForTeam(teamId, []string{oc1.Id})
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{&oc1}, list)
})
t.Run("only oc1, among others, should be found as a public channel in the team", func(t *testing.T) {
list, channelErr := ss.Channel().GetPublicChannelsByIdsForTeam(teamId, []string{oc1.Id, oc2.Id, model.NewId(), pc3.Id})
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{&oc1}, list)
})
// oc4 is another public channel on the team
oc4 := model.Channel{
TeamId: teamId,
DisplayName: "OpenChannel4Team1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&oc4, -1)
require.NoError(t, nErr)
// oc4 is another public, but deleted channel on the team
oc5 := model.Channel{
TeamId: teamId,
DisplayName: "OpenChannel4Team1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&oc5, -1)
require.NoError(t, nErr)
err := ss.Channel().Delete(oc5.Id, model.GetMillis())
require.NoError(t, err, "channel should have been deleted")
t.Run("only oc1 and oc4, among others, should be found as a public channel in the team", func(t *testing.T) {
list, err := ss.Channel().GetPublicChannelsByIdsForTeam(teamId, []string{oc1.Id, oc2.Id, model.NewId(), pc3.Id, oc4.Id})
require.NoError(t, err)
require.Equal(t, model.ChannelList{&oc1, &oc4}, list)
})
t.Run("random channel id should not be found as a public channel in the team", func(t *testing.T) {
_, err := ss.Channel().GetPublicChannelsByIdsForTeam(teamId, []string{model.NewId()})
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
})
}
func testChannelStoreGetChannelCounts(t *testing.T, ss store.Store) {
o2 := model.Channel{}
o2.TeamId = model.NewId()
o2.DisplayName = "Channel2"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err := ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = model.NewId()
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
m3 := model.ChannelMember{}
m3.ChannelId = o2.Id
m3.UserId = model.NewId()
m3.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m3)
require.NoError(t, err)
counts, _ := ss.Channel().GetChannelCounts(o1.TeamId, m1.UserId)
require.Len(t, counts.Counts, 1, "wrong number of counts")
require.Len(t, counts.UpdateTimes, 1, "wrong number of update times")
}
func testChannelStoreGetMembersForUser(t *testing.T, ss store.Store) {
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
o1 := model.Channel{}
o1.TeamId = t1.Id
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{}
o2.TeamId = o1.TeamId
o2.DisplayName = "Channel2"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{}
m2.ChannelId = o2.Id
m2.UserId = m1.UserId
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
t.Run("with channels", func(t *testing.T) {
var members model.ChannelMembers
members, err = ss.Channel().GetMembersForUser(o1.TeamId, m1.UserId)
require.NoError(t, err)
assert.Len(t, members, 2)
})
t.Run("with channels and direct messages", func(t *testing.T) {
user := model.User{Id: m1.UserId}
u1 := model.User{Id: model.NewId()}
u2 := model.User{Id: model.NewId()}
u3 := model.User{Id: model.NewId()}
u4 := model.User{Id: model.NewId()}
_, nErr = ss.Channel().CreateDirectChannel(&u1, &user)
require.NoError(t, nErr)
_, nErr = ss.Channel().CreateDirectChannel(&u2, &user)
require.NoError(t, nErr)
// other user direct message
_, nErr = ss.Channel().CreateDirectChannel(&u3, &u4)
require.NoError(t, nErr)
var members model.ChannelMembers
members, err = ss.Channel().GetMembersForUser(o1.TeamId, m1.UserId)
require.NoError(t, err)
assert.Len(t, members, 4)
})
t.Run("with channels, direct channels and group messages", func(t *testing.T) {
userIds := []string{model.NewId(), model.NewId(), model.NewId(), m1.UserId}
group := &model.Channel{
Name: model.GetGroupNameFromUserIds(userIds),
DisplayName: "test",
Type: model.ChannelTypeGroup,
}
var channel *model.Channel
channel, nErr = ss.Channel().Save(group, 10000)
require.NoError(t, nErr)
for _, userId := range userIds {
cm := &model.ChannelMember{
UserId: userId,
ChannelId: channel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeUser: true,
}
_, err = ss.Channel().SaveMember(cm)
require.NoError(t, err)
}
var members model.ChannelMembers
members, err = ss.Channel().GetMembersForUser(o1.TeamId, m1.UserId)
require.NoError(t, err)
assert.Len(t, members, 5)
})
}
func testChannelStoreGetMembersForUserWithCursor(t *testing.T, ss store.Store) {
t1 := model.Team{}
t1.DisplayName = "Team1"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
t2 := model.Team{}
t2.DisplayName = "Team2"
t2.Name = NewTestId()
t2.Email = MakeEmail()
t2.Type = model.TeamOpen
_, err = ss.Team().Save(&t2)
require.NoError(t, err)
o1 := model.Channel{}
o1.TeamId = t1.Id
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{}
o2.TeamId = o1.TeamId
o2.DisplayName = "Channel2"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
o3 := model.Channel{}
o3.TeamId = t2.Id
o3.DisplayName = "Channel3"
o3.Name = NewTestId()
o3.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{}
m2.ChannelId = o2.Id
m2.UserId = m1.UserId
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
m3 := model.ChannelMember{}
m3.ChannelId = o3.Id
m3.UserId = m1.UserId
m3.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m3)
require.NoError(t, err)
t.Run("with channels", func(t *testing.T) {
var members model.ChannelMembers
opts := &store.ChannelMemberGraphQLSearchOpts{
Limit: 1,
}
members, err = ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", opts)
require.NoError(t, err)
assert.Len(t, members, 1)
opts.Limit = 3
members, err = ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", opts)
require.NoError(t, err)
assert.Len(t, members, 3)
opts.AfterChannel = members[0].ChannelId
opts.AfterUser = m1.UserId
opts.Limit = 1
members, err = ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", opts)
require.NoError(t, err)
assert.Len(t, members, 1)
})
t.Run("with channels and direct messages", func(t *testing.T) {
user := model.User{Id: m1.UserId}
u1 := model.User{Id: model.NewId()}
u2 := model.User{Id: model.NewId()}
u3 := model.User{Id: model.NewId()}
u4 := model.User{Id: model.NewId()}
_, nErr = ss.Channel().CreateDirectChannel(&u1, &user)
require.NoError(t, nErr)
_, nErr = ss.Channel().CreateDirectChannel(&u2, &user)
require.NoError(t, nErr)
// other user direct message
_, nErr = ss.Channel().CreateDirectChannel(&u3, &u4)
require.NoError(t, nErr)
opts := &store.ChannelMemberGraphQLSearchOpts{
Limit: 10,
}
members, err2 := ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", opts)
require.NoError(t, err2)
assert.Len(t, members, 5)
opts.Limit = 2
members, err2 = ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", opts)
require.NoError(t, err2)
assert.Len(t, members, 2)
opts.AfterChannel = members[1].ChannelId
opts.AfterUser = m1.UserId
opts.Limit = 2
members, err2 = ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", opts)
require.NoError(t, err2)
assert.Len(t, members, 2)
})
t.Run("for a specific team", func(t *testing.T) {
opts := &store.ChannelMemberGraphQLSearchOpts{
Limit: 10,
}
members, err2 := ss.Channel().GetMembersForUserWithCursor(m1.UserId, t2.Id, opts)
require.NoError(t, err2)
assert.Len(t, members, 3)
})
t.Run("excluding a team", func(t *testing.T) {
opts := &store.ChannelMemberGraphQLSearchOpts{
Limit: 10,
ExcludeTeam: true,
}
members, err2 := ss.Channel().GetMembersForUserWithCursor(m1.UserId, t2.Id, opts)
require.NoError(t, err2)
assert.Len(t, members, 2)
})
t.Run("with channels, direct channels and group messages", func(t *testing.T) {
userIds := []string{model.NewId(), model.NewId(), model.NewId(), m1.UserId}
group := &model.Channel{
Name: model.GetGroupNameFromUserIds(userIds),
DisplayName: "test",
Type: model.ChannelTypeGroup,
}
var channel *model.Channel
channel, nErr = ss.Channel().Save(group, 10000)
require.NoError(t, nErr)
for _, userId := range userIds {
cm := &model.ChannelMember{
UserId: userId,
ChannelId: channel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeUser: true,
}
_, err = ss.Channel().SaveMember(cm)
require.NoError(t, err)
}
opts := &store.ChannelMemberGraphQLSearchOpts{
Limit: 10,
}
members, err := ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", opts)
require.NoError(t, err)
assert.Len(t, members, 6)
opts.Limit = 2
members, err = ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", opts)
require.NoError(t, err)
assert.Len(t, members, 2)
opts.AfterChannel = members[1].ChannelId
opts.AfterUser = m1.UserId
opts.Limit = 10
members, err = ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", opts)
require.NoError(t, err)
assert.Len(t, members, 4)
})
}
func testChannelStoreGetMembersForUserWithPagination(t *testing.T, ss store.Store) {
t1 := model.Team{
DisplayName: "team1",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
o1 := model.Channel{
TeamId: t1.Id,
DisplayName: "Channel1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, err = ss.Channel().Save(&o1, -1)
require.NoError(t, err)
t2 := model.Team{
DisplayName: "team2",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
_, err = ss.Team().Save(&t2)
require.NoError(t, err)
o2 := model.Channel{
TeamId: t2.Id,
DisplayName: "Channel2",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, err = ss.Channel().Save(&o2, -1)
require.NoError(t, err)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{}
m2.ChannelId = o2.Id
m2.UserId = m1.UserId
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
members, err := ss.Channel().GetMembersForUserWithPagination(m1.UserId, 0, 2)
require.NoError(t, err)
assert.Len(t, members, 2)
teamNames := make([]string, 0, 2)
for _, member := range members {
teamNames = append(teamNames, member.TeamDisplayName)
}
assert.ElementsMatch(t, teamNames, []string{t1.DisplayName, t2.DisplayName})
members, err = ss.Channel().GetMembersForUserWithPagination(m1.UserId, 1, 1)
require.NoError(t, err)
assert.Len(t, members, 1)
}
func testCountPostsAfter(t *testing.T, ss store.Store) {
t.Run("should count all posts with or without the given user ID", func(t *testing.T) {
userId1 := model.NewId()
userId2 := model.NewId()
channelId := model.NewId()
p1, err := ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1000,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1001,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userId2,
ChannelId: channelId,
CreateAt: 1002,
})
require.NoError(t, err)
count, _, err := ss.Channel().CountPostsAfter(channelId, p1.CreateAt-1, "")
require.NoError(t, err)
assert.Equal(t, 3, count)
count, _, err = ss.Channel().CountPostsAfter(channelId, p1.CreateAt, "")
require.NoError(t, err)
assert.Equal(t, 2, count)
count, _, err = ss.Channel().CountPostsAfter(channelId, p1.CreateAt-1, userId1)
require.NoError(t, err)
assert.Equal(t, 2, count)
count, _, err = ss.Channel().CountPostsAfter(channelId, p1.CreateAt, userId1)
require.NoError(t, err)
assert.Equal(t, 1, count)
})
t.Run("should not count deleted posts", func(t *testing.T) {
userId1 := model.NewId()
channelId := model.NewId()
p1, err := ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1000,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1001,
DeleteAt: 1001,
})
require.NoError(t, err)
count, _, err := ss.Channel().CountPostsAfter(channelId, p1.CreateAt-1, "")
require.NoError(t, err)
assert.Equal(t, 1, count)
count, _, err = ss.Channel().CountPostsAfter(channelId, p1.CreateAt, "")
require.NoError(t, err)
assert.Equal(t, 0, count)
})
t.Run("should count system/bot messages, but not join/leave messages", func(t *testing.T) {
userId1 := model.NewId()
channelId := model.NewId()
p1, err := ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1000,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1001,
Type: model.PostTypeJoinChannel,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1002,
Type: model.PostTypeRemoveFromChannel,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1003,
Type: model.PostTypeLeaveTeam,
})
require.NoError(t, err)
p5, err := ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1004,
Type: model.PostTypeHeaderChange,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1005,
Type: "custom_nps_survey",
})
require.NoError(t, err)
count, _, err := ss.Channel().CountPostsAfter(channelId, p1.CreateAt-1, "")
require.NoError(t, err)
assert.Equal(t, 3, count)
count, _, err = ss.Channel().CountPostsAfter(channelId, p1.CreateAt, "")
require.NoError(t, err)
assert.Equal(t, 2, count)
count, _, err = ss.Channel().CountPostsAfter(channelId, p5.CreateAt-1, "")
require.NoError(t, err)
assert.Equal(t, 2, count)
count, _, err = ss.Channel().CountPostsAfter(channelId, p5.CreateAt, "")
require.NoError(t, err)
assert.Equal(t, 1, count)
})
}
func testCountUrgentPostsAfter(t *testing.T, ss store.Store) {
t.Run("should count all posts with or without the given user ID", func(t *testing.T) {
userId1 := model.NewId()
userId2 := model.NewId()
channelId := model.NewId()
p1, err := ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1000,
Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(false),
},
},
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userId1,
ChannelId: channelId,
CreateAt: 1001,
Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(false),
},
},
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userId2,
ChannelId: channelId,
CreateAt: 1002,
})
require.NoError(t, err)
count, err := ss.Channel().CountUrgentPostsAfter(channelId, p1.CreateAt-1, "")
require.NoError(t, err)
assert.Equal(t, 1, count)
count, err = ss.Channel().CountUrgentPostsAfter(channelId, p1.CreateAt, "")
require.NoError(t, err)
assert.Equal(t, 0, count)
count, err = ss.Channel().CountUrgentPostsAfter(channelId, p1.CreateAt-1, userId1)
require.NoError(t, err)
assert.Equal(t, 1, count)
count, err = ss.Channel().CountUrgentPostsAfter(channelId, p1.CreateAt, userId1)
require.NoError(t, err)
assert.Equal(t, 0, count)
})
}
func testChannelStoreUpdateLastViewedAt(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
o1.TotalMsgCount = 25
o1.LastPostAt = 12345
o1.LastRootPostAt = 12345
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err := ss.Channel().SaveMember(&m1)
require.NoError(t, err)
o2 := model.Channel{}
o2.TeamId = model.NewId()
o2.DisplayName = "Channel1"
o2.Name = NewTestId() + "c"
o2.Type = model.ChannelTypeOpen
o2.TotalMsgCount = 26
o2.LastPostAt = 123456
o2.LastRootPostAt = 123456
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
m2 := model.ChannelMember{}
m2.ChannelId = o2.Id
m2.UserId = m1.UserId
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
var times map[string]int64
times, err = ss.Channel().UpdateLastViewedAt([]string{m1.ChannelId}, m1.UserId)
require.NoError(t, err, "failed to update ", err)
require.Equal(t, o1.LastPostAt, times[o1.Id], "last viewed at time incorrect")
times, err = ss.Channel().UpdateLastViewedAt([]string{m1.ChannelId, m2.ChannelId}, m1.UserId)
require.NoError(t, err, "failed to update ", err)
require.Equal(t, o2.LastPostAt, times[o2.Id], "last viewed at time incorrect")
rm1, err := ss.Channel().GetMember(context.Background(), m1.ChannelId, m1.UserId)
assert.NoError(t, err)
assert.Equal(t, o1.LastPostAt, rm1.LastViewedAt)
assert.Equal(t, o1.LastPostAt, rm1.LastUpdateAt)
assert.Equal(t, o1.TotalMsgCount, rm1.MsgCount)
rm2, err := ss.Channel().GetMember(context.Background(), m2.ChannelId, m2.UserId)
assert.NoError(t, err)
assert.Equal(t, o2.LastPostAt, rm2.LastViewedAt)
assert.Equal(t, o2.LastPostAt, rm2.LastUpdateAt)
assert.Equal(t, o2.TotalMsgCount, rm2.MsgCount)
_, err = ss.Channel().UpdateLastViewedAt([]string{m1.ChannelId}, "missing id")
require.NoError(t, err, "failed to update")
}
func testChannelStoreIncrementMentionCount(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
o1.TotalMsgCount = 25
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = model.NewId()
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err := ss.Channel().SaveMember(&m1)
require.NoError(t, err)
err = ss.Channel().IncrementMentionCount(m1.ChannelId, []string{m1.UserId}, false, false)
require.NoError(t, err, "failed to update")
err = ss.Channel().IncrementMentionCount(m1.ChannelId, []string{"missing id"}, false, false)
require.NoError(t, err, "failed to update")
err = ss.Channel().IncrementMentionCount("missing id", []string{m1.UserId}, false, false)
require.NoError(t, err, "failed to update")
err = ss.Channel().IncrementMentionCount("missing id", []string{"missing id"}, false, false)
require.NoError(t, err, "failed to update")
}
func testUpdateChannelMember(t *testing.T, ss store.Store) {
userId := model.NewId()
c1 := &model.Channel{
TeamId: model.NewId(),
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
m1 := &model.ChannelMember{
ChannelId: c1.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(m1)
require.NoError(t, err)
m1.NotifyProps["test"] = "sometext"
_, err = ss.Channel().UpdateMember(m1)
require.NoError(t, err, err)
m1.UserId = ""
_, err = ss.Channel().UpdateMember(m1)
require.Error(t, err, "bad user id - should fail")
}
func testGetMember(t *testing.T, ss store.Store) {
userId := model.NewId()
c1 := &model.Channel{
TeamId: model.NewId(),
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
c2 := &model.Channel{
TeamId: c1.TeamId,
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(c2, -1)
require.NoError(t, nErr)
m1 := &model.ChannelMember{
ChannelId: c1.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(m1)
require.NoError(t, err)
m2 := &model.ChannelMember{
ChannelId: c2.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(m2)
require.NoError(t, err)
_, err = ss.Channel().GetMember(context.Background(), model.NewId(), userId)
require.Error(t, err, "should've failed to get member for non-existent channel")
_, err = ss.Channel().GetMember(context.Background(), c1.Id, model.NewId())
require.Error(t, err, "should've failed to get member for non-existent user")
member, err := ss.Channel().GetMember(context.Background(), c1.Id, userId)
require.NoError(t, err, "shouldn't have errored when getting member", err)
require.Equal(t, c1.Id, member.ChannelId, "should've gotten member of channel 1")
require.Equal(t, userId, member.UserId, "should've have gotten member for user")
member, err = ss.Channel().GetMember(context.Background(), c2.Id, userId)
require.NoError(t, err, "shouldn't have errored when getting member", err)
require.Equal(t, c2.Id, member.ChannelId, "should've gotten member of channel 2")
require.Equal(t, userId, member.UserId, "should've gotten member for user")
props, err := ss.Channel().GetAllChannelMembersNotifyPropsForChannel(c2.Id, false)
require.NoError(t, err, err)
require.NotEqual(t, 0, len(props), "should not be empty")
props, err = ss.Channel().GetAllChannelMembersNotifyPropsForChannel(c2.Id, true)
require.NoError(t, err, err)
require.NotEqual(t, 0, len(props), "should not be empty")
ss.Channel().InvalidateCacheForChannelMembersNotifyProps(c2.Id)
}
func testChannelStoreGetMemberForPost(t *testing.T, ss store.Store) {
ch := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
o1, nErr := ss.Channel().Save(ch, -1)
require.NoError(t, nErr)
m1, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: o1.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
p1, nErr := ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: o1.Id,
Message: "test",
})
require.NoError(t, nErr)
r1, err := ss.Channel().GetMemberForPost(p1.Id, m1.UserId)
require.NoError(t, err, err)
require.Equal(t, channelMemberToJSON(t, m1), channelMemberToJSON(t, r1), "invalid returned channel member")
_, err = ss.Channel().GetMemberForPost(p1.Id, model.NewId())
require.Error(t, err, "shouldn't have returned a member")
}
func testGetMemberCount(t *testing.T, ss store.Store) {
teamId := model.NewId()
c1 := model.Channel{
TeamId: teamId,
DisplayName: "Channel1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&c1, -1)
require.NoError(t, nErr)
c2 := model.Channel{
TeamId: teamId,
DisplayName: "Channel2",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&c2, -1)
require.NoError(t, nErr)
u1 := &model.User{
Email: MakeEmail(),
DeleteAt: 0,
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{
ChannelId: c1.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, nErr = ss.Channel().SaveMember(&m1)
require.NoError(t, nErr)
count, channelErr := ss.Channel().GetMemberCount(c1.Id, false)
require.NoError(t, channelErr, "failed to get member count", channelErr)
require.EqualValuesf(t, 1, count, "got incorrect member count %v", count)
u2 := model.User{
Email: MakeEmail(),
DeleteAt: 0,
}
_, err = ss.User().Save(&u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
m2 := model.ChannelMember{
ChannelId: c1.Id,
UserId: u2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, nErr = ss.Channel().SaveMember(&m2)
require.NoError(t, nErr)
count, channelErr = ss.Channel().GetMemberCount(c1.Id, false)
require.NoErrorf(t, channelErr, "failed to get member count: %v", channelErr)
require.EqualValuesf(t, 2, count, "got incorrect member count %v", count)
// make sure members of other channels aren't counted
u3 := model.User{
Email: MakeEmail(),
DeleteAt: 0,
}
_, err = ss.User().Save(&u3)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
m3 := model.ChannelMember{
ChannelId: c2.Id,
UserId: u3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, nErr = ss.Channel().SaveMember(&m3)
require.NoError(t, nErr)
count, channelErr = ss.Channel().GetMemberCount(c1.Id, false)
require.NoErrorf(t, channelErr, "failed to get member count: %v", channelErr)
require.EqualValuesf(t, 2, count, "got incorrect member count %v", count)
// make sure inactive users aren't counted
u4 := &model.User{
Email: MakeEmail(),
DeleteAt: 10000,
}
_, err = ss.User().Save(u4)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u4.Id}, -1)
require.NoError(t, nErr)
m4 := model.ChannelMember{
ChannelId: c1.Id,
UserId: u4.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, nErr = ss.Channel().SaveMember(&m4)
require.NoError(t, nErr)
count, nErr = ss.Channel().GetMemberCount(c1.Id, false)
require.NoError(t, nErr, "failed to get member count", nErr)
require.EqualValuesf(t, 2, count, "got incorrect member count %v", count)
}
func testGetMemberCountsByGroup(t *testing.T, ss store.Store) {
var memberCounts []*model.ChannelMemberCountByGroup
teamId := model.NewId()
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
_, err := ss.Group().Create(g1)
require.NoError(t, err)
c1 := model.Channel{
TeamId: teamId,
DisplayName: "Channel1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&c1, -1)
require.NoError(t, nErr)
u1 := &model.User{
Timezone: timezones.DefaultUserTimezone(),
Email: MakeEmail(),
DeleteAt: 0,
}
_, nErr = ss.User().Save(u1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{
ChannelId: c1.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, nErr = ss.Channel().SaveMember(&m1)
require.NoError(t, nErr)
t.Run("empty slice for channel with no groups", func(t *testing.T) {
memberCounts, nErr = ss.Channel().GetMemberCountsByGroup(context.Background(), c1.Id, false)
expectedMemberCounts := []*model.ChannelMemberCountByGroup{}
require.NoError(t, nErr)
require.Equal(t, expectedMemberCounts, memberCounts)
})
_, err = ss.Group().UpsertMember(g1.Id, u1.Id)
require.NoError(t, err)
t.Run("returns memberCountsByGroup without timezones", func(t *testing.T) {
memberCounts, nErr = ss.Channel().GetMemberCountsByGroup(context.Background(), c1.Id, false)
expectedMemberCounts := []*model.ChannelMemberCountByGroup{
{
GroupId: g1.Id,
ChannelMemberCount: 1,
ChannelMemberTimezonesCount: 0,
},
}
require.NoError(t, nErr)
require.Equal(t, expectedMemberCounts, memberCounts)
})
t.Run("returns memberCountsByGroup with timezones when no timezones set", func(t *testing.T) {
memberCounts, nErr = ss.Channel().GetMemberCountsByGroup(context.Background(), c1.Id, true)
expectedMemberCounts := []*model.ChannelMemberCountByGroup{
{
GroupId: g1.Id,
ChannelMemberCount: 1,
ChannelMemberTimezonesCount: 0,
},
}
require.NoError(t, nErr)
require.Equal(t, expectedMemberCounts, memberCounts)
})
g2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
_, err = ss.Group().Create(g2)
require.NoError(t, err)
// create 5 different users with 2 different timezones for group 2
for i := 1; i <= 5; i++ {
timeZone := timezones.DefaultUserTimezone()
if i == 1 {
timeZone["manualTimezone"] = "EDT"
timeZone["useAutomaticTimezone"] = "false"
}
u := &model.User{
Timezone: timeZone,
Email: MakeEmail(),
DeleteAt: 0,
}
_, nErr = ss.User().Save(u)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u.Id}, -1)
require.NoError(t, nErr)
m := model.ChannelMember{
ChannelId: c1.Id,
UserId: u.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, nErr = ss.Channel().SaveMember(&m)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(g2.Id, u.Id)
require.NoError(t, err)
}
g3 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
_, err = ss.Group().Create(g3)
require.NoError(t, err)
// create 10 different users with 3 different timezones for group 3
for i := 1; i <= 10; i++ {
timeZone := timezones.DefaultUserTimezone()
if i == 1 || i == 2 {
timeZone["manualTimezone"] = "EDT"
timeZone["useAutomaticTimezone"] = "false"
} else if i == 3 || i == 4 {
timeZone["manualTimezone"] = "PST"
timeZone["useAutomaticTimezone"] = "false"
} else if i == 5 {
timeZone["autoTimezone"] = "CET"
timeZone["useAutomaticTimezone"] = "true"
} else if i == 6 {
timeZone["automaticTimezone"] = "CET"
timeZone["useAutomaticTimezone"] = "true"
} else {
// Give every user with auto timezone set to true a random manual timezone to ensure that manual timezone is not looked at if auto is set
timeZone["useAutomaticTimezone"] = "true"
timeZone["manualTimezone"] = "PST" + utils.RandomName(utils.Range{Begin: 5, End: 5}, utils.ALPHANUMERIC)
}
u := &model.User{
Timezone: timeZone,
Email: MakeEmail(),
DeleteAt: 0,
}
_, nErr = ss.User().Save(u)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u.Id}, -1)
require.NoError(t, nErr)
m := model.ChannelMember{
ChannelId: c1.Id,
UserId: u.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, nErr = ss.Channel().SaveMember(&m)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(g3.Id, u.Id)
require.NoError(t, err)
}
t.Run("returns memberCountsByGroup for multiple groups with lots of users without timezones", func(t *testing.T) {
memberCounts, nErr = ss.Channel().GetMemberCountsByGroup(context.Background(), c1.Id, false)
expectedMemberCounts := []*model.ChannelMemberCountByGroup{
{
GroupId: g1.Id,
ChannelMemberCount: 1,
ChannelMemberTimezonesCount: 0,
},
{
GroupId: g2.Id,
ChannelMemberCount: 5,
ChannelMemberTimezonesCount: 0,
},
{
GroupId: g3.Id,
ChannelMemberCount: 10,
ChannelMemberTimezonesCount: 0,
},
}
require.NoError(t, nErr)
require.ElementsMatch(t, expectedMemberCounts, memberCounts)
})
t.Run("returns memberCountsByGroup for multiple groups with lots of users with timezones", func(t *testing.T) {
memberCounts, nErr = ss.Channel().GetMemberCountsByGroup(context.Background(), c1.Id, true)
expectedMemberCounts := []*model.ChannelMemberCountByGroup{
{
GroupId: g1.Id,
ChannelMemberCount: 1,
ChannelMemberTimezonesCount: 0,
},
{
GroupId: g2.Id,
ChannelMemberCount: 5,
ChannelMemberTimezonesCount: 1,
},
{
GroupId: g3.Id,
ChannelMemberCount: 10,
ChannelMemberTimezonesCount: 3,
},
}
require.NoError(t, nErr)
require.ElementsMatch(t, expectedMemberCounts, memberCounts)
})
}
func testGetGuestCount(t *testing.T, ss store.Store) {
teamId := model.NewId()
c1 := model.Channel{
TeamId: teamId,
DisplayName: "Channel1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&c1, -1)
require.NoError(t, nErr)
c2 := model.Channel{
TeamId: teamId,
DisplayName: "Channel2",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&c2, -1)
require.NoError(t, nErr)
t.Run("Regular member doesn't count", func(t *testing.T) {
u1 := &model.User{
Email: MakeEmail(),
DeleteAt: 0,
Roles: model.SystemUserRoleId,
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{
ChannelId: c1.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: false,
}
_, nErr = ss.Channel().SaveMember(&m1)
require.NoError(t, nErr)
count, channelErr := ss.Channel().GetGuestCount(c1.Id, false)
require.NoError(t, channelErr)
require.Equal(t, int64(0), count)
})
t.Run("Guest member does count", func(t *testing.T) {
u2 := model.User{
Email: MakeEmail(),
DeleteAt: 0,
Roles: model.SystemGuestRoleId,
}
_, err := ss.User().Save(&u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
m2 := model.ChannelMember{
ChannelId: c1.Id,
UserId: u2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: true,
}
_, nErr = ss.Channel().SaveMember(&m2)
require.NoError(t, nErr)
count, channelErr := ss.Channel().GetGuestCount(c1.Id, false)
require.NoError(t, channelErr)
require.Equal(t, int64(1), count)
})
t.Run("make sure members of other channels aren't counted", func(t *testing.T) {
u3 := model.User{
Email: MakeEmail(),
DeleteAt: 0,
Roles: model.SystemGuestRoleId,
}
_, err := ss.User().Save(&u3)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
m3 := model.ChannelMember{
ChannelId: c2.Id,
UserId: u3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: true,
}
_, nErr = ss.Channel().SaveMember(&m3)
require.NoError(t, nErr)
count, channelErr := ss.Channel().GetGuestCount(c1.Id, false)
require.NoError(t, channelErr)
require.Equal(t, int64(1), count)
})
t.Run("make sure inactive users aren't counted", func(t *testing.T) {
u4 := &model.User{
Email: MakeEmail(),
DeleteAt: 10000,
Roles: model.SystemGuestRoleId,
}
_, err := ss.User().Save(u4)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u4.Id}, -1)
require.NoError(t, nErr)
m4 := model.ChannelMember{
ChannelId: c1.Id,
UserId: u4.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: true,
}
_, nErr = ss.Channel().SaveMember(&m4)
require.NoError(t, nErr)
count, channelErr := ss.Channel().GetGuestCount(c1.Id, false)
require.NoError(t, channelErr)
require.Equal(t, int64(1), count)
})
}
func testChannelStoreSearchMore(t *testing.T, ss store.Store) {
teamId := model.NewId()
otherTeamId := model.NewId()
o1 := model.Channel{
TeamId: teamId,
DisplayName: "ChannelA",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{
ChannelId: o1.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{
ChannelId: o1.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
o2 := model.Channel{
TeamId: otherTeamId,
DisplayName: "Channel2",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
m3 := model.ChannelMember{
ChannelId: o2.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m3)
require.NoError(t, err)
o3 := model.Channel{
TeamId: teamId,
DisplayName: "ChannelA",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
o4 := model.Channel{
TeamId: teamId,
DisplayName: "ChannelB",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o4, -1)
require.NoError(t, nErr)
o5 := model.Channel{
TeamId: teamId,
DisplayName: "ChannelC",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o5, -1)
require.NoError(t, nErr)
o6 := model.Channel{
TeamId: teamId,
DisplayName: "Off-Topic",
Name: "off-topic",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o6, -1)
require.NoError(t, nErr)
o7 := model.Channel{
TeamId: teamId,
DisplayName: "Off-Set",
Name: "off-set",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o7, -1)
require.NoError(t, nErr)
o8 := model.Channel{
TeamId: teamId,
DisplayName: "Off-Limit",
Name: "off-limit",
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o8, -1)
require.NoError(t, nErr)
o9 := model.Channel{
TeamId: teamId,
DisplayName: "Channel With Purpose",
Purpose: "This can now be searchable!",
Name: "with-purpose",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o9, -1)
require.NoError(t, nErr)
o10 := model.Channel{
TeamId: teamId,
DisplayName: "ChannelA",
Name: "channel-a-deleted",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o10, -1)
require.NoError(t, nErr)
o10.DeleteAt = model.GetMillis()
o10.UpdateAt = o10.DeleteAt
nErr = ss.Channel().Delete(o10.Id, o10.DeleteAt)
require.NoError(t, nErr, "channel should have been deleted")
t.Run("three public channels matching 'ChannelA', but already a member of one and one deleted", func(t *testing.T) {
channels, err := ss.Channel().SearchMore(m1.UserId, teamId, "ChannelA")
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o3}, channels)
})
t.Run("one public channels, but already a member", func(t *testing.T) {
channels, err := ss.Channel().SearchMore(m1.UserId, teamId, o4.Name)
require.NoError(t, err)
require.Equal(t, model.ChannelList{}, channels)
})
t.Run("three matching channels, but only two public", func(t *testing.T) {
channels, err := ss.Channel().SearchMore(m1.UserId, teamId, "off-")
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o7, &o6}, channels)
})
t.Run("one channel matching 'off-topic'", func(t *testing.T) {
channels, err := ss.Channel().SearchMore(m1.UserId, teamId, "off-topic")
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o6}, channels)
})
t.Run("search purpose", func(t *testing.T) {
channels, err := ss.Channel().SearchMore(m1.UserId, teamId, "now searchable")
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o9}, channels)
})
}
type ByChannelDisplayName model.ChannelList
func (s ByChannelDisplayName) Len() int { return len(s) }
func (s ByChannelDisplayName) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s ByChannelDisplayName) Less(i, j int) bool {
if s[i].DisplayName != s[j].DisplayName {
return s[i].DisplayName < s[j].DisplayName
}
return s[i].Id < s[j].Id
}
func testChannelStoreSearchArchivedInTeam(t *testing.T, ss store.Store, s SqlStore) {
teamId := model.NewId()
userId := model.NewId()
o1 := model.Channel{}
o1.TeamId = teamId
o1.DisplayName = "Channel1"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o1.DeleteAt = model.GetMillis()
o1.UpdateAt = o1.DeleteAt
nErr = ss.Channel().Delete(o1.Id, o1.DeleteAt)
require.NoError(t, nErr)
t.Run("empty result", func(t *testing.T) {
list, err := ss.Channel().SearchArchivedInTeam(teamId, "term", userId)
require.NoError(t, err)
require.NotNil(t, list)
require.Empty(t, list)
})
t.Run("error", func(t *testing.T) {
// trigger a SQL error
s.GetMasterX().Exec("ALTER TABLE Channels RENAME TO Channels_renamed")
defer s.GetMasterX().Exec("ALTER TABLE Channels_renamed RENAME TO Channels")
list, err := ss.Channel().SearchArchivedInTeam(teamId, "term", userId)
require.Error(t, err)
require.Nil(t, list)
})
t.Run("find term", func(t *testing.T) {
list, err := ss.Channel().SearchArchivedInTeam(teamId, "Channel", userId)
require.NoError(t, err)
require.NotNil(t, list)
require.Equal(t, len(list), 1)
require.Equal(t, "Channel1", list[0].DisplayName)
})
}
func testChannelStoreSearchInTeam(t *testing.T, ss store.Store) {
teamID := model.NewId()
otherTeamID := model.NewId()
o1 := model.Channel{
TeamId: teamID,
DisplayName: "ChannelA",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{
TeamId: otherTeamID,
DisplayName: "ChannelA",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{
ChannelId: o1.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{
ChannelId: o1.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
m3 := model.ChannelMember{
ChannelId: o2.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m3)
require.NoError(t, err)
o3 := model.Channel{
TeamId: teamID,
DisplayName: "ChannelA (alternate)",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
o4 := model.Channel{
TeamId: teamID,
DisplayName: "Channel B",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o4, -1)
require.NoError(t, nErr)
m4 := &model.ChannelMember{
ChannelId: o4.Id,
UserId: m3.UserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(m4)
require.NoError(t, err)
o5 := model.Channel{
TeamId: teamID,
DisplayName: "Channel C",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o5, -1)
require.NoError(t, nErr)
o6 := model.Channel{
TeamId: teamID,
DisplayName: "Off-Topic",
Name: "off-topic",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o6, -1)
require.NoError(t, nErr)
o7 := model.Channel{
TeamId: teamID,
DisplayName: "Off-Set",
Name: "off-set",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o7, -1)
require.NoError(t, nErr)
o8 := model.Channel{
TeamId: teamID,
DisplayName: "Off-Limit",
Name: "off-limit",
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o8, -1)
require.NoError(t, nErr)
m5 := &model.ChannelMember{
ChannelId: o8.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(m5)
require.NoError(t, err)
o9 := model.Channel{
TeamId: teamID,
DisplayName: "Town Square",
Name: "town-square",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o9, -1)
require.NoError(t, nErr)
o10 := model.Channel{
TeamId: teamID,
DisplayName: "The",
Name: "thename",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o10, -1)
require.NoError(t, nErr)
o11 := model.Channel{
TeamId: teamID,
DisplayName: "Native Mobile Apps",
Name: "native-mobile-apps",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o11, -1)
require.NoError(t, nErr)
o12 := model.Channel{
TeamId: teamID,
DisplayName: "ChannelZ",
Purpose: "This can now be searchable!",
Name: "with-purpose",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o12, -1)
require.NoError(t, nErr)
o13 := model.Channel{
TeamId: teamID,
DisplayName: "ChannelA (deleted)",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o13, -1)
require.NoError(t, nErr)
o13.DeleteAt = model.GetMillis()
o13.UpdateAt = o13.DeleteAt
nErr = ss.Channel().Delete(o13.Id, o13.DeleteAt)
require.NoError(t, nErr, "channel should have been deleted")
testCases := []struct {
Description string
TeamID string
UserID string
Term string
IncludeDeleted bool
ExpectedResults model.ChannelList
}{
{"ChannelA", teamID, m1.UserId, "ChannelA", false, model.ChannelList{&o1, &o3}},
{"ChannelA, include deleted", teamID, m1.UserId, "ChannelA", true, model.ChannelList{&o1, &o3, &o13}},
{"ChannelA, other team", otherTeamID, m3.UserId, "ChannelA", false, model.ChannelList{&o2}},
{"empty string", teamID, m1.UserId, "", false, model.ChannelList{&o1, &o3, &o12, &o11, &o7, &o6, &o10, &o9}},
{"no matches", teamID, m1.UserId, "blargh", false, model.ChannelList{}},
{"prefix", teamID, m1.UserId, "off-", false, model.ChannelList{&o7, &o6}},
{"full match with dash", teamID, m1.UserId, "off-topic", false, model.ChannelList{&o6}},
{"town square", teamID, m1.UserId, "town square", false, model.ChannelList{&o9}},
{"the in name", teamID, m1.UserId, "thename", false, model.ChannelList{&o10}},
{"Mobile", teamID, m1.UserId, "Mobile", false, model.ChannelList{&o11}},
{"search purpose", teamID, m1.UserId, "now searchable", false, model.ChannelList{&o12}},
{"pipe ignored", teamID, m1.UserId, "town square |", false, model.ChannelList{&o9}},
}
for _, testCase := range testCases {
t.Run("SearchInTeam/"+testCase.Description, func(t *testing.T) {
channels, err := ss.Channel().SearchInTeam(testCase.TeamID, testCase.Term, testCase.IncludeDeleted)
require.NoError(t, err)
require.Equal(t, testCase.ExpectedResults, channels)
})
}
testCases = append(testCases, []struct {
Description string
TeamID string
UserID string
Term string
IncludeDeleted bool
ExpectedResults model.ChannelList
}{
{"Channel A", teamID, m4.UserId, "Channel ", false, model.ChannelList{&o4, &o1, &o3, &o12}},
{"off limit (private)", teamID, m5.UserId, "off limit", false, model.ChannelList{&o8}},
}...,
)
for _, testCase := range testCases {
t.Run("AutoCompleteInTeam/"+testCase.Description, func(t *testing.T) {
channels, err := ss.Channel().AutocompleteInTeam(testCase.TeamID, testCase.UserID, testCase.Term, testCase.IncludeDeleted, false)
require.NoError(t, err)
sort.Sort(ByChannelDisplayName(channels))
require.Equal(t, testCase.ExpectedResults, channels)
})
}
}
func testAutocomplete(t *testing.T, ss store.Store) {
t1 := &model.Team{
DisplayName: "t1",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
t1, err := ss.Team().Save(t1)
require.NoError(t, err)
teamID := t1.Id
t2 := &model.Team{
DisplayName: "t2",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
t2, err = ss.Team().Save(t2)
require.NoError(t, err)
otherTeamID := t2.Id
o1 := model.Channel{
TeamId: teamID,
DisplayName: "ChannelA1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, err = ss.Channel().Save(&o1, -1)
require.NoError(t, err)
o2 := model.Channel{
TeamId: otherTeamID,
DisplayName: "ChannelA2",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, err = ss.Channel().Save(&o2, -1)
require.NoError(t, err)
o6 := model.Channel{
TeamId: teamID,
DisplayName: "ChannelA3",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, err = ss.Channel().Save(&o6, -1)
require.NoError(t, err)
m1 := model.ChannelMember{
ChannelId: o1.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{
ChannelId: o2.Id,
UserId: m1.UserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
tm1 := &model.TeamMember{TeamId: teamID, UserId: m1.UserId}
_, err = ss.Team().SaveMember(tm1, -1)
require.NoError(t, err)
tm2 := &model.TeamMember{TeamId: otherTeamID, UserId: m1.UserId}
_, err = ss.Team().SaveMember(tm2, -1)
require.NoError(t, err)
m3 := model.ChannelMember{
ChannelId: o2.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m3)
require.NoError(t, err)
tm3 := &model.TeamMember{TeamId: otherTeamID, UserId: m3.UserId}
_, err = ss.Team().SaveMember(tm3, -1)
require.NoError(t, err)
tm4 := &model.TeamMember{TeamId: teamID, UserId: m3.UserId}
_, err = ss.Team().SaveMember(tm4, -1)
require.NoError(t, err)
o3 := model.Channel{
TeamId: teamID,
DisplayName: "ChannelA private",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, err = ss.Channel().Save(&o3, -1)
require.NoError(t, err)
o4 := model.Channel{
TeamId: otherTeamID,
DisplayName: "ChannelB",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, err = ss.Channel().Save(&o4, -1)
require.NoError(t, err)
m4 := &model.ChannelMember{
ChannelId: o3.Id,
UserId: m3.UserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(m4)
require.NoError(t, err)
m5 := &model.ChannelMember{
ChannelId: o4.Id,
UserId: m1.UserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(m5)
require.NoError(t, err)
t3 := &model.Team{
DisplayName: "t3",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
t3, err = ss.Team().Save(t3)
require.NoError(t, err)
leftTeamId := t3.Id
o5 := model.Channel{
TeamId: leftTeamId,
DisplayName: "ChannelA3",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, err = ss.Channel().Save(&o5, -1)
require.NoError(t, err)
m6 := model.ChannelMember{
ChannelId: o5.Id,
UserId: m1.UserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m6)
require.NoError(t, err)
tm5 := &model.TeamMember{TeamId: leftTeamId, UserId: m1.UserId}
_, err = ss.Team().SaveMember(tm5, -1)
require.NoError(t, err)
err = ss.Channel().RemoveMember(o5.Id, m1.UserId)
require.NoError(t, err)
tm5.Roles = ""
tm5.DeleteAt = model.GetMillis()
_, err = ss.Team().UpdateMember(tm5)
require.NoError(t, err)
testCases := []struct {
Description string
UserID string
Term string
IncludeDeleted bool
IsGuest bool
ExpectedChannelIds []string
ExpectedTeamNames []string
}{
{"user 1, Channel A", m1.UserId, "ChannelA", false, false, []string{o1.Id, o2.Id, o6.Id}, []string{t1.Name, t2.Name, t1.Name}},
{"user 1, Channel B", m1.UserId, "ChannelB", false, false, []string{o4.Id}, []string{t2.Name}},
{"user 2, Channel A", m3.UserId, "ChannelA", false, false, []string{o3.Id, o1.Id, o2.Id, o6.Id}, []string{t2.Name, t1.Name, t1.Name, t1.Name}},
{"user 2 guest, Channel A", m3.UserId, "ChannelA", false, true, []string{o2.Id, o3.Id}, []string{t2.Name, t1.Name}},
{"user 2, Channel B", m3.UserId, "ChannelB", false, false, nil, nil},
{"user 1, empty string", m1.UserId, "", false, false, []string{o1.Id, o2.Id, o4.Id, o6.Id}, []string{t1.Name, t2.Name, t2.Name, t1.Name}},
{"user 2, empty string", m3.UserId, "", false, false, []string{o1.Id, o2.Id, o3.Id, o6.Id}, []string{t1.Name, t2.Name, t1.Name, t1.Name}},
}
for _, testCase := range testCases {
t.Run("Autocomplete/"+testCase.Description, func(t *testing.T) {
channels, err := ss.Channel().Autocomplete(testCase.UserID, testCase.Term, testCase.IncludeDeleted, testCase.IsGuest)
require.NoError(t, err)
var gotChannelIds []string
var gotTeamNames []string
for _, ch := range channels {
gotChannelIds = append(gotChannelIds, ch.Id)
gotTeamNames = append(gotTeamNames, ch.TeamName)
}
require.ElementsMatch(t, testCase.ExpectedChannelIds, gotChannelIds, "channels IDs are not as expected")
require.ElementsMatch(t, testCase.ExpectedTeamNames, gotTeamNames, "team names are not as expected")
})
}
}
func testChannelStoreSearchForUserInTeam(t *testing.T, ss store.Store) {
userId := model.NewId()
teamId := model.NewId()
otherTeamId := model.NewId()
// create 4 channels for the same team and one for other team
o1 := model.Channel{
TeamId: teamId,
DisplayName: "test-dev-1",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{
TeamId: teamId,
DisplayName: "test-dev-2",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
o3 := model.Channel{
TeamId: teamId,
DisplayName: "dev-3",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
o4 := model.Channel{
TeamId: teamId,
DisplayName: "dev-4",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o4, -1)
require.NoError(t, nErr)
o5 := model.Channel{
TeamId: otherTeamId,
DisplayName: "other-team-dev-5",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o5, -1)
require.NoError(t, nErr)
// add the user to the first 3 channels and the other team channel
for _, c := range []model.Channel{o1, o2, o3, o5} {
_, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
}
searchAndCheck := func(t *testing.T, term string, includeDeleted bool, expectedDisplayNames []string) {
res, searchErr := ss.Channel().SearchForUserInTeam(userId, teamId, term, includeDeleted)
require.NoError(t, searchErr)
require.Len(t, res, len(expectedDisplayNames))
resultDisplayNames := []string{}
for _, c := range res {
resultDisplayNames = append(resultDisplayNames, c.DisplayName)
}
require.ElementsMatch(t, expectedDisplayNames, resultDisplayNames)
}
t.Run("Search for test, get channels 1 and 2", func(t *testing.T) {
searchAndCheck(t, "test", false, []string{o1.DisplayName, o2.DisplayName})
})
t.Run("Search for dev, get channels 1, 2 and 3", func(t *testing.T) {
searchAndCheck(t, "dev", false, []string{o1.DisplayName, o2.DisplayName, o3.DisplayName})
})
t.Run("After adding user to channel 4, search for dev, get channels 1, 2, 3 and 4", func(t *testing.T) {
_, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: o4.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
searchAndCheck(t, "dev", false, []string{o1.DisplayName, o2.DisplayName, o3.DisplayName, o4.DisplayName})
})
t.Run("Mark channel 1 as deleted, search for dev, get channels 2, 3 and 4", func(t *testing.T) {
o1.DeleteAt = model.GetMillis()
o1.UpdateAt = o1.DeleteAt
err := ss.Channel().Delete(o1.Id, o1.DeleteAt)
require.NoError(t, err)
searchAndCheck(t, "dev", false, []string{o2.DisplayName, o3.DisplayName, o4.DisplayName})
})
t.Run("With includeDeleted, search for dev, get channels 1, 2, 3 and 4", func(t *testing.T) {
searchAndCheck(t, "dev", true, []string{o1.DisplayName, o2.DisplayName, o3.DisplayName, o4.DisplayName})
})
}
func testChannelStoreSearchAllChannels(t *testing.T, ss store.Store) {
cleanupChannels(t, ss)
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
t2 := model.Team{}
t2.DisplayName = "Name2"
t2.Name = NewTestId()
t2.Email = MakeEmail()
t2.Type = model.TeamOpen
_, err = ss.Team().Save(&t2)
require.NoError(t, err)
o1 := model.Channel{
TeamId: t1.Id,
DisplayName: "A1 ChannelA",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{
TeamId: t2.Id,
DisplayName: "A2 ChannelA",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{
ChannelId: o1.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{
ChannelId: o1.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
m3 := model.ChannelMember{
ChannelId: o2.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m3)
require.NoError(t, err)
o3 := model.Channel{
TeamId: t1.Id,
DisplayName: "A3 ChannelA (alternate)",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
o4 := model.Channel{
TeamId: t1.Id,
DisplayName: "A4 ChannelB",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o4, -1)
require.NoError(t, nErr)
o5 := model.Channel{
TeamId: t1.Id,
DisplayName: "A5 ChannelC",
Name: NewTestId(),
Type: model.ChannelTypePrivate,
GroupConstrained: model.NewBool(true),
}
_, nErr = ss.Channel().Save(&o5, -1)
require.NoError(t, nErr)
o6 := model.Channel{
TeamId: t1.Id,
DisplayName: "A6 Off-Topic",
Name: "off-topic",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o6, -1)
require.NoError(t, nErr)
o7 := model.Channel{
TeamId: t1.Id,
DisplayName: "A7 Off-Set",
Name: "off-set",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o7, -1)
require.NoError(t, nErr)
group := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
_, err = ss.Group().Create(group)
require.NoError(t, err)
_, err = ss.Group().CreateGroupSyncable(model.NewGroupChannel(group.Id, o7.Id, true))
require.NoError(t, err)
o8 := model.Channel{
TeamId: t1.Id,
DisplayName: "A8 Off-Limit",
Name: "off-limit",
Type: model.ChannelTypePrivate,
}
_, nErr = ss.Channel().Save(&o8, -1)
require.NoError(t, nErr)
o9 := model.Channel{
TeamId: t1.Id,
DisplayName: "A9 Town Square",
Name: "town-square",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o9, -1)
require.NoError(t, nErr)
o10 := model.Channel{
TeamId: t1.Id,
DisplayName: "B10 Which",
Name: "which",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o10, -1)
require.NoError(t, nErr)
o11 := model.Channel{
TeamId: t1.Id,
DisplayName: "B11 Native Mobile Apps",
Name: "native-mobile-apps",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o11, -1)
require.NoError(t, nErr)
o12 := model.Channel{
TeamId: t1.Id,
DisplayName: "B12 ChannelZ",
Purpose: "This can now be searchable!",
Name: "with-purpose",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o12, -1)
require.NoError(t, nErr)
o13 := model.Channel{
TeamId: t1.Id,
DisplayName: "B13 ChannelA (deleted)",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o13, -1)
require.NoError(t, nErr)
o13.DeleteAt = model.GetMillis()
o13.UpdateAt = o13.DeleteAt
nErr = ss.Channel().Delete(o13.Id, o13.DeleteAt)
require.NoError(t, nErr, "channel should have been deleted")
o14 := model.Channel{
TeamId: t2.Id,
DisplayName: "B14 FOOBARDISPLAYNAME",
Name: "whatever",
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o14, -1)
require.NoError(t, nErr)
_, nErr = ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "Policy 1",
PostDurationDays: model.NewInt64(30),
},
ChannelIDs: []string{o14.Id},
})
require.NoError(t, nErr)
testCases := []struct {
Description string
Term string
Opts store.ChannelSearchOpts
ExpectedResults model.ChannelList
TotalCount int
}{
{"Search FooBar by display name", "bardisplay", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o14}, 1},
{"Search FooBar by display name2", "foobar", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o14}, 1},
{"Search FooBar by display name3", "displayname", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o14}, 1},
{"Search FooBar by name", "what", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o14}, 1},
{"Search FooBar by name2", "ever", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o14}, 1},
{"ChannelA", "ChannelA", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o1, &o2, &o3}, 0},
{"ChannelA, include deleted", "ChannelA", store.ChannelSearchOpts{IncludeDeleted: true}, model.ChannelList{&o1, &o2, &o3, &o13}, 0},
{"empty string", "", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o1, &o2, &o3, &o4, &o5, &o6, &o7, &o8, &o9, &o10, &o11, &o12, &o14}, 0},
{"no matches", "blargh", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{}, 0},
{"prefix", "off-", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o6, &o7, &o8}, 0},
{"full match with dash", "off-topic", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o6}, 0},
{"town square", "town square", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o9}, 0},
{"which in name", "which", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o10}, 0},
{"Mobile", "Mobile", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o11}, 0},
{"search purpose", "now searchable", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o12}, 0},
{"pipe ignored", "town square |", store.ChannelSearchOpts{IncludeDeleted: false}, model.ChannelList{&o9}, 0},
{"exclude defaults search 'off'", "off-", store.ChannelSearchOpts{IncludeDeleted: false, ExcludeChannelNames: []string{"off-topic"}}, model.ChannelList{&o7, &o8}, 0},
{"exclude defaults search 'town'", "town", store.ChannelSearchOpts{IncludeDeleted: false, ExcludeChannelNames: []string{"town-square"}}, model.ChannelList{}, 0},
{"exclude by group association", "off-", store.ChannelSearchOpts{IncludeDeleted: false, NotAssociatedToGroup: group.Id}, model.ChannelList{&o6, &o8}, 0},
{"paginate includes count", "off-", store.ChannelSearchOpts{IncludeDeleted: false, PerPage: model.NewInt(100)}, model.ChannelList{&o6, &o7, &o8}, 3},
{"paginate, page 2 correct entries and count", "off-", store.ChannelSearchOpts{IncludeDeleted: false, PerPage: model.NewInt(2), Page: model.NewInt(1)}, model.ChannelList{&o8}, 3},
{"Filter private", "", store.ChannelSearchOpts{IncludeDeleted: false, Private: true}, model.ChannelList{&o4, &o5, &o8}, 3},
{"Filter public", "", store.ChannelSearchOpts{IncludeDeleted: false, Public: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o1, &o2, &o3, &o6, &o7}, 10},
{"Filter public and private", "", store.ChannelSearchOpts{IncludeDeleted: false, Public: true, Private: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o1, &o2, &o3, &o4, &o5}, 13},
{"Filter public and private and include deleted", "", store.ChannelSearchOpts{IncludeDeleted: true, Public: true, Private: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o1, &o2, &o3, &o4, &o5}, 14},
{"Filter group constrained", "", store.ChannelSearchOpts{IncludeDeleted: false, GroupConstrained: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o5}, 1},
{"Filter exclude group constrained and include deleted", "", store.ChannelSearchOpts{IncludeDeleted: true, ExcludeGroupConstrained: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o1, &o2, &o3, &o4, &o6}, 13},
{"Filter private and exclude group constrained", "", store.ChannelSearchOpts{IncludeDeleted: false, ExcludeGroupConstrained: true, Private: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o4, &o8}, 2},
{"Exclude policy constrained", "", store.ChannelSearchOpts{ExcludePolicyConstrained: true}, model.ChannelList{&o1, &o2, &o3, &o4, &o5, &o6, &o7, &o8, &o9, &o10, &o11, &o12}, 0},
{"Filter team 2", "", store.ChannelSearchOpts{IncludeDeleted: false, TeamIds: []string{t2.Id}, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o2, &o14}, 2},
{"Filter team 2, private", "", store.ChannelSearchOpts{IncludeDeleted: false, TeamIds: []string{t2.Id}, Private: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{}, 0},
{"Filter team 1 and team 2, private", "", store.ChannelSearchOpts{IncludeDeleted: false, TeamIds: []string{t1.Id, t2.Id}, Private: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o4, &o5, &o8}, 3},
{"Filter team 1 and team 2, public and private", "", store.ChannelSearchOpts{IncludeDeleted: false, TeamIds: []string{t1.Id, t2.Id}, Public: true, Private: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o1, &o2, &o3, &o4, &o5}, 13},
{"Filter team 1 and team 2, public and private and group constrained", "", store.ChannelSearchOpts{IncludeDeleted: false, TeamIds: []string{t1.Id, t2.Id}, Public: true, Private: true, GroupConstrained: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o5}, 1},
{"Filter team 1 and team 2, public and private and exclude group constrained", "", store.ChannelSearchOpts{IncludeDeleted: false, TeamIds: []string{t1.Id, t2.Id}, Public: true, Private: true, ExcludeGroupConstrained: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o1, &o2, &o3, &o4, &o6}, 12},
{"Filter deleted returns only deleted channels", "", store.ChannelSearchOpts{Deleted: true, Page: model.NewInt(0), PerPage: model.NewInt(5)}, model.ChannelList{&o13}, 1},
{"Search ChannelA by id", o1.Id, store.ChannelSearchOpts{IncludeDeleted: false, Page: model.NewInt(0), PerPage: model.NewInt(5), IncludeSearchById: true}, model.ChannelList{&o1}, 1},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
channels, count, err := ss.Channel().SearchAllChannels(testCase.Term, testCase.Opts)
require.NoError(t, err)
require.Equal(t, len(testCase.ExpectedResults), len(channels))
for i, expected := range testCase.ExpectedResults {
require.Equal(t, expected.Id, channels[i].Id)
}
if testCase.Opts.Page != nil || testCase.Opts.PerPage != nil {
require.Equal(t, int64(testCase.TotalCount), count)
}
})
}
}
func testChannelStoreGetMembersByIds(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "ChannelA"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
m1 := &model.ChannelMember{ChannelId: o1.Id, UserId: model.NewId(), NotifyProps: model.GetDefaultChannelNotifyProps()}
_, err := ss.Channel().SaveMember(m1)
require.NoError(t, err)
var members model.ChannelMembers
members, nErr = ss.Channel().GetMembersByIds(m1.ChannelId, []string{m1.UserId})
require.NoError(t, nErr, nErr)
rm1 := members[0]
require.Equal(t, m1.ChannelId, rm1.ChannelId, "bad team id")
require.Equal(t, m1.UserId, rm1.UserId, "bad user id")
m2 := &model.ChannelMember{ChannelId: o1.Id, UserId: model.NewId(), NotifyProps: model.GetDefaultChannelNotifyProps()}
_, err = ss.Channel().SaveMember(m2)
require.NoError(t, err)
members, nErr = ss.Channel().GetMembersByIds(m1.ChannelId, []string{m1.UserId, m2.UserId, model.NewId()})
require.NoError(t, nErr, nErr)
require.Len(t, members, 2, "return wrong number of results")
members, nErr = ss.Channel().GetMembersByIds(m1.ChannelId, []string{})
require.NoError(t, nErr)
require.Len(t, members, 0)
}
func testChannelStoreGetMembersByChannelIds(t *testing.T, ss store.Store) {
userId := model.NewId()
// Create a couple channels and add the user to them
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: model.NewId(),
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: model.NewId(),
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel1.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel2.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
t.Run("should return the user's members for the given channels", func(t *testing.T) {
result, nErr := ss.Channel().GetMembersByChannelIds([]string{channel1.Id, channel2.Id}, userId)
require.NoError(t, nErr)
assert.Len(t, result, 2)
assert.Equal(t, userId, result[0].UserId)
assert.True(t, result[0].ChannelId == channel1.Id || result[1].ChannelId == channel1.Id)
assert.Equal(t, userId, result[1].UserId)
assert.True(t, result[0].ChannelId == channel2.Id || result[1].ChannelId == channel2.Id)
})
t.Run("should not error or return anything for invalid channel IDs", func(t *testing.T) {
result, nErr := ss.Channel().GetMembersByChannelIds([]string{model.NewId(), model.NewId()}, userId)
require.NoError(t, nErr)
assert.Len(t, result, 0)
})
t.Run("should not error or return anything for invalid user IDs", func(t *testing.T) {
result, nErr := ss.Channel().GetMembersByChannelIds([]string{channel1.Id, channel2.Id}, model.NewId())
require.NoError(t, nErr)
assert.Len(t, result, 0)
})
}
func testChannelStoreGetMembersInfoByChannelIds(t *testing.T, ss store.Store) {
u, err := ss.User().Save(&model.User{
Username: "user.test",
Email: MakeEmail(),
Nickname: model.NewId(),
})
require.NoError(t, err)
// Create a couple channels and add the user to them
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: model.NewId(),
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: model.NewId(),
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel1.Id,
UserId: u.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel2.Id,
UserId: u.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
t.Run("should return the user's members for the given channels", func(t *testing.T) {
result, nErr := ss.Channel().GetMembersInfoByChannelIds([]string{channel1.Id, channel2.Id})
require.NoError(t, nErr)
assert.Len(t, result, 2)
for _, item := range result {
assert.Len(t, item, 1)
assert.Equal(t, u.Id, item[0].Id)
}
})
t.Run("should not error or return anything for invalid channel IDs", func(t *testing.T) {
_, err := ss.Channel().GetMembersInfoByChannelIds([]string{model.NewId(), model.NewId()})
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
})
}
func testChannelStoreSearchGroupChannels(t *testing.T, ss store.Store) {
// Users
u1 := &model.User{}
u1.Username = "user.one"
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
u2 := &model.User{}
u2.Username = "user.two"
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
u3 := &model.User{}
u3.Username = "user.three"
u3.Email = MakeEmail()
u3.Nickname = model.NewId()
_, err = ss.User().Save(u3)
require.NoError(t, err)
u4 := &model.User{}
u4.Username = "user.four"
u4.Email = MakeEmail()
u4.Nickname = model.NewId()
_, err = ss.User().Save(u4)
require.NoError(t, err)
// Group channels
userIds := []string{u1.Id, u2.Id, u3.Id}
gc1 := model.Channel{}
gc1.Name = model.GetGroupNameFromUserIds(userIds)
gc1.DisplayName = "GroupChannel" + model.NewId()
gc1.Type = model.ChannelTypeGroup
_, nErr := ss.Channel().Save(&gc1, -1)
require.NoError(t, nErr)
for _, userId := range userIds {
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: gc1.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
}
userIds = []string{u1.Id, u4.Id}
gc2 := model.Channel{}
gc2.Name = model.GetGroupNameFromUserIds(userIds)
gc2.DisplayName = "GroupChannel" + model.NewId()
gc2.Type = model.ChannelTypeGroup
_, nErr = ss.Channel().Save(&gc2, -1)
require.NoError(t, nErr)
for _, userId := range userIds {
_, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: gc2.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
}
userIds = []string{u1.Id, u2.Id, u3.Id, u4.Id}
gc3 := model.Channel{}
gc3.Name = model.GetGroupNameFromUserIds(userIds)
gc3.DisplayName = "GroupChannel" + model.NewId()
gc3.Type = model.ChannelTypeGroup
_, nErr = ss.Channel().Save(&gc3, -1)
require.NoError(t, nErr)
for _, userId := range userIds {
_, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: gc3.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
}
defer func() {
for _, gc := range []model.Channel{gc1, gc2, gc3} {
ss.Channel().PermanentDeleteMembersByChannel(gc3.Id)
ss.Channel().PermanentDelete(gc.Id)
}
}()
testCases := []struct {
Name string
UserId string
Term string
ExpectedResult []string
}{
{
Name: "Get all group channels for user1",
UserId: u1.Id,
Term: "",
ExpectedResult: []string{gc1.Id, gc2.Id, gc3.Id},
},
{
Name: "Get group channels for user1 and term 'three'",
UserId: u1.Id,
Term: "three",
ExpectedResult: []string{gc1.Id, gc3.Id},
},
{
Name: "Get group channels for user1 and term 'four two'",
UserId: u1.Id,
Term: "four two",
ExpectedResult: []string{gc3.Id},
},
{
Name: "Get all group channels for user2",
UserId: u2.Id,
Term: "",
ExpectedResult: []string{gc1.Id, gc3.Id},
},
{
Name: "Get group channels for user2 and term 'four'",
UserId: u2.Id,
Term: "four",
ExpectedResult: []string{gc3.Id},
},
{
Name: "Get all group channels for user4",
UserId: u4.Id,
Term: "",
ExpectedResult: []string{gc2.Id, gc3.Id},
},
{
Name: "Get group channels for user4 and term 'one five'",
UserId: u4.Id,
Term: "one five",
ExpectedResult: []string{},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
result, err := ss.Channel().SearchGroupChannels(tc.UserId, tc.Term)
require.NoError(t, err)
resultIds := []string{}
for _, gc := range result {
resultIds = append(resultIds, gc.Id)
}
require.ElementsMatch(t, tc.ExpectedResult, resultIds)
})
}
}
func testChannelStoreAnalyticsDeletedTypeCount(t *testing.T, ss store.Store) {
o1 := model.Channel{}
o1.TeamId = model.NewId()
o1.DisplayName = "ChannelA"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
o2 := model.Channel{}
o2.TeamId = model.NewId()
o2.DisplayName = "Channel2"
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
p3 := model.Channel{}
p3.TeamId = model.NewId()
p3.DisplayName = "Channel3"
p3.Name = NewTestId()
p3.Type = model.ChannelTypePrivate
_, nErr = ss.Channel().Save(&p3, -1)
require.NoError(t, nErr)
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
d4, nErr := ss.Channel().CreateDirectChannel(u1, u2)
require.NoError(t, nErr)
defer func() {
ss.Channel().PermanentDeleteMembersByChannel(d4.Id)
ss.Channel().PermanentDelete(d4.Id)
}()
var openStartCount int64
openStartCount, nErr = ss.Channel().AnalyticsDeletedTypeCount("", model.ChannelTypeOpen)
require.NoError(t, nErr, nErr)
var privateStartCount int64
privateStartCount, nErr = ss.Channel().AnalyticsDeletedTypeCount("", model.ChannelTypePrivate)
require.NoError(t, nErr, nErr)
var directStartCount int64
directStartCount, nErr = ss.Channel().AnalyticsDeletedTypeCount("", model.ChannelTypeDirect)
require.NoError(t, nErr, nErr)
nErr = ss.Channel().Delete(o1.Id, model.GetMillis())
require.NoError(t, nErr, "channel should have been deleted")
nErr = ss.Channel().Delete(o2.Id, model.GetMillis())
require.NoError(t, nErr, "channel should have been deleted")
nErr = ss.Channel().Delete(p3.Id, model.GetMillis())
require.NoError(t, nErr, "channel should have been deleted")
nErr = ss.Channel().Delete(d4.Id, model.GetMillis())
require.NoError(t, nErr, "channel should have been deleted")
var count int64
count, nErr = ss.Channel().AnalyticsDeletedTypeCount("", model.ChannelTypeOpen)
require.NoError(t, err, nErr)
assert.Equal(t, openStartCount+2, count, "Wrong open channel deleted count.")
count, nErr = ss.Channel().AnalyticsDeletedTypeCount("", model.ChannelTypePrivate)
require.NoError(t, nErr, nErr)
assert.Equal(t, privateStartCount+1, count, "Wrong private channel deleted count.")
count, nErr = ss.Channel().AnalyticsDeletedTypeCount("", model.ChannelTypeDirect)
require.NoError(t, nErr, nErr)
assert.Equal(t, directStartCount+1, count, "Wrong direct channel deleted count.")
}
func testChannelStoreGetPinnedPosts(t *testing.T, ss store.Store) {
ch1 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
o1, nErr := ss.Channel().Save(ch1, -1)
require.NoError(t, nErr)
p1, err := ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: o1.Id,
Message: "test",
IsPinned: true,
})
require.NoError(t, err)
pl, errGet := ss.Channel().GetPinnedPosts(o1.Id)
require.NoError(t, errGet, errGet)
require.NotNil(t, pl.Posts[p1.Id], "didn't return relevant pinned posts")
ch2 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
o2, nErr := ss.Channel().Save(ch2, -1)
require.NoError(t, nErr)
_, err = ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: o2.Id,
Message: "test",
})
require.NoError(t, err)
pl, errGet = ss.Channel().GetPinnedPosts(o2.Id)
require.NoError(t, errGet, errGet)
require.Empty(t, pl.Posts, "wasn't supposed to return posts")
t.Run("with correct ReplyCount", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
userId := model.NewId()
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: userId,
Message: "message",
IsPinned: true,
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: userId,
Message: "message",
IsPinned: true,
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post3, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: userId,
RootId: post1.Id,
Message: "message",
IsPinned: true,
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
posts, err := ss.Channel().GetPinnedPosts(channel.Id)
require.NoError(t, err)
require.Len(t, posts.Posts, 3)
require.Equal(t, posts.Posts[post1.Id].ReplyCount, int64(1))
require.Equal(t, posts.Posts[post2.Id].ReplyCount, int64(0))
require.Equal(t, posts.Posts[post3.Id].ReplyCount, int64(1))
})
}
func testChannelStoreGetPinnedPostCount(t *testing.T, ss store.Store) {
ch1 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
o1, nErr := ss.Channel().Save(ch1, -1)
require.NoError(t, nErr)
_, err := ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: o1.Id,
Message: "test",
IsPinned: true,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: o1.Id,
Message: "test",
IsPinned: true,
})
require.NoError(t, err)
count, errGet := ss.Channel().GetPinnedPostCount(o1.Id, true)
require.NoError(t, errGet, errGet)
require.EqualValues(t, 2, count, "didn't return right count")
ch2 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
o2, nErr := ss.Channel().Save(ch2, -1)
require.NoError(t, nErr)
_, err = ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: o2.Id,
Message: "test",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: o2.Id,
Message: "test",
})
require.NoError(t, err)
count, errGet = ss.Channel().GetPinnedPostCount(o2.Id, true)
require.NoError(t, errGet, errGet)
require.EqualValues(t, 0, count, "should return 0")
}
func testChannelStoreMaxChannelsPerTeam(t *testing.T, ss store.Store) {
channel := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Channel",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(channel, 0)
assert.Error(t, nErr)
var ltErr *store.ErrLimitExceeded
assert.True(t, errors.As(nErr, <Err))
channel.Id = ""
_, nErr = ss.Channel().Save(channel, 1)
assert.NoError(t, nErr)
}
func testChannelStoreGetChannelsByScheme(t *testing.T, ss store.Store) {
// Create some schemes.
s1 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
s2 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
s1, err := ss.Scheme().Save(s1)
require.NoError(t, err)
s2, err = ss.Scheme().Save(s2)
require.NoError(t, err)
// Create and save some teams.
c1 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
SchemeId: &s1.Id,
}
c2 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
SchemeId: &s1.Id,
}
c3 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, _ = ss.Channel().Save(c1, 100)
_, _ = ss.Channel().Save(c2, 100)
_, _ = ss.Channel().Save(c3, 100)
// Get the channels by a valid Scheme ID.
d1, err := ss.Channel().GetChannelsByScheme(s1.Id, 0, 100)
assert.NoError(t, err)
assert.Len(t, d1, 2)
// Get the channels by a valid Scheme ID where there aren't any matching Channel.
d2, err := ss.Channel().GetChannelsByScheme(s2.Id, 0, 100)
assert.NoError(t, err)
assert.Empty(t, d2)
// Get the channels by an invalid Scheme ID.
d3, err := ss.Channel().GetChannelsByScheme(model.NewId(), 0, 100)
assert.NoError(t, err)
assert.Empty(t, d3)
}
func testChannelStoreMigrateChannelMembers(t *testing.T, ss store.Store) {
s1 := model.NewId()
c1 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
SchemeId: &s1,
}
c1, _ = ss.Channel().Save(c1, 100)
cm1 := &model.ChannelMember{
ChannelId: c1.Id,
UserId: model.NewId(),
ExplicitRoles: "channel_admin channel_user",
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
cm2 := &model.ChannelMember{
ChannelId: c1.Id,
UserId: model.NewId(),
ExplicitRoles: "channel_user",
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
cm3 := &model.ChannelMember{
ChannelId: c1.Id,
UserId: model.NewId(),
ExplicitRoles: "something_else",
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
cm1, _ = ss.Channel().SaveMember(cm1)
cm2, _ = ss.Channel().SaveMember(cm2)
cm3, _ = ss.Channel().SaveMember(cm3)
lastDoneChannelId := strings.Repeat("0", 26)
lastDoneUserId := strings.Repeat("0", 26)
for {
data, err := ss.Channel().MigrateChannelMembers(lastDoneChannelId, lastDoneUserId)
if assert.NoError(t, err) {
if data == nil {
break
}
lastDoneChannelId = data["ChannelId"]
lastDoneUserId = data["UserId"]
}
}
ss.Channel().ClearCaches()
cm1b, err := ss.Channel().GetMember(context.Background(), cm1.ChannelId, cm1.UserId)
assert.NoError(t, err)
assert.Equal(t, "", cm1b.ExplicitRoles)
assert.False(t, cm1b.SchemeGuest)
assert.True(t, cm1b.SchemeUser)
assert.True(t, cm1b.SchemeAdmin)
cm2b, err := ss.Channel().GetMember(context.Background(), cm2.ChannelId, cm2.UserId)
assert.NoError(t, err)
assert.Equal(t, "", cm2b.ExplicitRoles)
assert.False(t, cm1b.SchemeGuest)
assert.True(t, cm2b.SchemeUser)
assert.False(t, cm2b.SchemeAdmin)
cm3b, err := ss.Channel().GetMember(context.Background(), cm3.ChannelId, cm3.UserId)
assert.NoError(t, err)
assert.Equal(t, "something_else", cm3b.ExplicitRoles)
assert.False(t, cm1b.SchemeGuest)
assert.False(t, cm3b.SchemeUser)
assert.False(t, cm3b.SchemeAdmin)
}
func testResetAllChannelSchemes(t *testing.T, ss store.Store) {
s1 := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
s1, err := ss.Scheme().Save(s1)
require.NoError(t, err)
c1 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
SchemeId: &s1.Id,
}
c2 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
SchemeId: &s1.Id,
}
c1, _ = ss.Channel().Save(c1, 100)
c2, _ = ss.Channel().Save(c2, 100)
assert.Equal(t, s1.Id, *c1.SchemeId)
assert.Equal(t, s1.Id, *c2.SchemeId)
err = ss.Channel().ResetAllChannelSchemes()
assert.NoError(t, err)
c1, _ = ss.Channel().Get(c1.Id, true)
c2, _ = ss.Channel().Get(c2.Id, true)
assert.Equal(t, "", *c1.SchemeId)
assert.Equal(t, "", *c2.SchemeId)
}
func testChannelStoreClearAllCustomRoleAssignments(t *testing.T, ss store.Store) {
c := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
c, _ = ss.Channel().Save(c, 100)
m1 := &model.ChannelMember{
ChannelId: c.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
ExplicitRoles: "system_user_access_token channel_user channel_admin",
}
m2 := &model.ChannelMember{
ChannelId: c.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
ExplicitRoles: "channel_user custom_role channel_admin another_custom_role",
}
m3 := &model.ChannelMember{
ChannelId: c.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
ExplicitRoles: "channel_user",
}
m4 := &model.ChannelMember{
ChannelId: c.Id,
UserId: model.NewId(),
NotifyProps: model.GetDefaultChannelNotifyProps(),
ExplicitRoles: "custom_only",
}
_, err := ss.Channel().SaveMember(m1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(m2)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(m3)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(m4)
require.NoError(t, err)
require.NoError(t, ss.Channel().ClearAllCustomRoleAssignments())
member, err := ss.Channel().GetMember(context.Background(), m1.ChannelId, m1.UserId)
require.NoError(t, err)
assert.Equal(t, m1.ExplicitRoles, member.Roles)
member, err = ss.Channel().GetMember(context.Background(), m2.ChannelId, m2.UserId)
require.NoError(t, err)
assert.Equal(t, "channel_user channel_admin", member.Roles)
member, err = ss.Channel().GetMember(context.Background(), m3.ChannelId, m3.UserId)
require.NoError(t, err)
assert.Equal(t, m3.ExplicitRoles, member.Roles)
member, err = ss.Channel().GetMember(context.Background(), m4.ChannelId, m4.UserId)
require.NoError(t, err)
assert.Equal(t, "", member.Roles)
}
// testMaterializedPublicChannels tests edge cases involving the triggers and stored procedures
// that materialize the PublicChannels table.
func testMaterializedPublicChannels(t *testing.T, ss store.Store, s SqlStore) {
teamId := model.NewId()
// o1 is a public channel on the team
o1 := model.Channel{
TeamId: teamId,
DisplayName: "Open Channel",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, nErr := ss.Channel().Save(&o1, -1)
require.NoError(t, nErr)
// o2 is another public channel on the team
o2 := model.Channel{
TeamId: teamId,
DisplayName: "Open Channel 2",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
t.Run("o1 and o2 initially listed in public channels", func(t *testing.T) {
channels, channelErr := ss.Channel().SearchInTeam(teamId, "", true)
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{&o1, &o2}, channels)
})
o1.DeleteAt = model.GetMillis()
o1.UpdateAt = o1.DeleteAt
e := ss.Channel().Delete(o1.Id, o1.DeleteAt)
require.NoError(t, e, "channel should have been deleted")
t.Run("o1 still listed in public channels when marked as deleted", func(t *testing.T) {
channels, channelErr := ss.Channel().SearchInTeam(teamId, "", true)
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{&o1, &o2}, channels)
})
ss.Channel().PermanentDelete(o1.Id)
t.Run("o1 no longer listed in public channels when permanently deleted", func(t *testing.T) {
channels, channelErr := ss.Channel().SearchInTeam(teamId, "", true)
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{&o2}, channels)
})
o2.Type = model.ChannelTypePrivate
_, err := ss.Channel().Update(&o2)
require.NoError(t, err)
t.Run("o2 no longer listed since now private", func(t *testing.T) {
channels, channelErr := ss.Channel().SearchInTeam(teamId, "", true)
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{}, channels)
})
o2.Type = model.ChannelTypeOpen
_, err = ss.Channel().Update(&o2)
require.NoError(t, err)
t.Run("o2 listed once again since now public", func(t *testing.T) {
channels, channelErr := ss.Channel().SearchInTeam(teamId, "", true)
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{&o2}, channels)
})
// o3 is a public channel on the team that already existed in the PublicChannels table.
o3 := model.Channel{
Id: model.NewId(),
TeamId: teamId,
DisplayName: "Open Channel 3",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, execerr := s.GetMasterX().NamedExec(`
INSERT INTO
PublicChannels(Id, DeleteAt, TeamId, DisplayName, Name, Header, Purpose)
VALUES
(:id, :deleteat, :teamid, :displayname, :name, :header, :purpose);
`, map[string]any{
"id": o3.Id,
"deleteat": o3.DeleteAt,
"teamid": o3.TeamId,
"displayname": o3.DisplayName,
"name": o3.Name,
"header": o3.Header,
"purpose": o3.Purpose,
})
require.NoError(t, execerr)
o3.DisplayName = "Open Channel 3 - Modified"
_, execerr = s.GetMasterX().NamedExec(`
INSERT INTO
Channels(Id, CreateAt, UpdateAt, DeleteAt, TeamId, Type, DisplayName, Name, Header, Purpose, LastPostAt, LastRootPostAt, TotalMsgCount, ExtraUpdateAt, CreatorId, TotalMsgCountRoot)
VALUES
(:id, :createat, :updateat, :deleteat, :teamid, :type, :displayname, :name, :header, :purpose, :lastpostat, :lastrootpostat, :totalmsgcount, :extraupdateat, :creatorid, 0);
`, map[string]any{
"id": o3.Id,
"createat": o3.CreateAt,
"updateat": o3.UpdateAt,
"deleteat": o3.DeleteAt,
"teamid": o3.TeamId,
"type": o3.Type,
"displayname": o3.DisplayName,
"name": o3.Name,
"header": o3.Header,
"purpose": o3.Purpose,
"lastpostat": o3.LastPostAt,
"lastrootpostat": o3.LastRootPostAt,
"totalmsgcount": o3.TotalMsgCount,
"extraupdateat": o3.ExtraUpdateAt,
"creatorid": o3.CreatorId,
})
require.NoError(t, execerr)
t.Run("verify o3 INSERT converted to UPDATE", func(t *testing.T) {
channels, channelErr := ss.Channel().SearchInTeam(teamId, "", true)
require.NoError(t, channelErr)
require.Equal(t, model.ChannelList{&o2, &o3}, channels)
})
// o4 is a public channel on the team that existed in the Channels table but was omitted from the PublicChannels table.
o4 := model.Channel{
TeamId: teamId,
DisplayName: "Open Channel 4",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
_, nErr = ss.Channel().Save(&o4, -1)
require.NoError(t, nErr)
_, execerr = s.GetMasterX().Exec(`
DELETE FROM
PublicChannels
WHERE
Id = ?
`, o4.Id)
require.NoError(t, execerr)
o4.DisplayName += " - Modified"
_, err = ss.Channel().Update(&o4)
require.NoError(t, err)
t.Run("verify o4 UPDATE converted to INSERT", func(t *testing.T) {
channels, err := ss.Channel().SearchInTeam(teamId, "", true)
require.NoError(t, err)
require.Equal(t, model.ChannelList{&o2, &o3, &o4}, channels)
})
}
func testChannelStoreGetAllChannelsForExportAfter(t *testing.T, ss store.Store) {
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
c1 := model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&c1, -1)
require.NoError(t, nErr)
d1, err := ss.Channel().GetAllChannelsForExportAfter(10000, strings.Repeat("0", 26))
assert.NoError(t, err)
found := false
for _, c := range d1 {
if c.Id == c1.Id {
found = true
assert.Equal(t, t1.Id, c.TeamId)
assert.Nil(t, c.SchemeId)
assert.Equal(t, t1.Name, c.TeamName)
}
}
assert.True(t, found)
}
func testChannelStoreGetChannelMembersForExport(t *testing.T, ss store.Store) {
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
c1 := model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&c1, -1)
require.NoError(t, nErr)
c2 := model.Channel{}
c2.TeamId = model.NewId()
c2.DisplayName = "Channel2"
c2.Name = NewTestId()
c2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(&c2, -1)
require.NoError(t, nErr)
u1 := model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err = ss.User().Save(&u1)
require.NoError(t, err)
m1 := model.ChannelMember{}
m1.ChannelId = c1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{}
m2.ChannelId = c2.Id
m2.UserId = u1.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
d1, err := ss.Channel().GetChannelMembersForExport(u1.Id, t1.Id)
assert.NoError(t, err)
assert.Len(t, d1, 1)
cmfe1 := d1[0]
assert.Equal(t, c1.Name, cmfe1.ChannelName)
assert.Equal(t, c1.Id, cmfe1.ChannelId)
assert.Equal(t, u1.Id, cmfe1.UserId)
}
func testChannelStoreRemoveAllDeactivatedMembers(t *testing.T, ss store.Store, s SqlStore) {
// Set up all the objects needed in the store.
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
c1 := model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&c1, -1)
require.NoError(t, nErr)
u1 := model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err = ss.User().Save(&u1)
require.NoError(t, err)
u2 := model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(&u2)
require.NoError(t, err)
u3 := model.User{}
u3.Email = MakeEmail()
u3.Nickname = model.NewId()
_, err = ss.User().Save(&u3)
require.NoError(t, err)
m1 := model.ChannelMember{}
m1.ChannelId = c1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m1)
require.NoError(t, err)
m2 := model.ChannelMember{}
m2.ChannelId = c1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m2)
require.NoError(t, err)
m3 := model.ChannelMember{}
m3.ChannelId = c1.Id
m3.UserId = u3.Id
m3.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(&m3)
require.NoError(t, err)
// Get all the channel members. Check there are 3.
d1, err := ss.Channel().GetMembers(c1.Id, 0, 1000)
assert.NoError(t, err)
assert.Len(t, d1, 3)
// Deactivate users 1 & 2.
u1.DeleteAt = model.GetMillis()
u2.DeleteAt = model.GetMillis()
_, err = ss.User().Update(&u1, true)
require.NoError(t, err)
_, err = ss.User().Update(&u2, true)
require.NoError(t, err)
// Remove all deactivated users from the channel.
assert.NoError(t, ss.Channel().RemoveAllDeactivatedMembers(c1.Id))
// Get all the channel members. Check there is now only 1: m3.
d2, err := ss.Channel().GetMembers(c1.Id, 0, 1000)
assert.NoError(t, err)
assert.Len(t, d2, 1)
assert.Equal(t, u3.Id, d2[0].UserId)
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testChannelStoreExportAllDirectChannels(t *testing.T, ss store.Store, s SqlStore) {
teamId := model.NewId()
o1 := model.Channel{}
o1.TeamId = teamId
o1.DisplayName = "Name" + model.NewId()
o1.Name = NewTestId()
o1.Type = model.ChannelTypeDirect
userIds := []string{model.NewId(), model.NewId(), model.NewId()}
o2 := model.Channel{}
o2.Name = model.GetGroupNameFromUserIds(userIds)
o2.DisplayName = "GroupChannel" + model.NewId()
o2.Name = NewTestId()
o2.Type = model.ChannelTypeGroup
_, nErr := ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
ss.Channel().SaveDirectChannel(&o1, &m1, &m2)
d1, nErr := ss.Channel().GetAllDirectChannelsForExportAfter(10000, strings.Repeat("0", 26))
assert.NoError(t, nErr)
assert.Len(t, d1, 2)
assert.ElementsMatch(t, []string{o1.DisplayName, o2.DisplayName}, []string{d1[0].DisplayName, d1[1].DisplayName})
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testChannelStoreExportAllDirectChannelsExcludePrivateAndPublic(t *testing.T, ss store.Store, s SqlStore) {
teamId := model.NewId()
o1 := model.Channel{}
o1.TeamId = teamId
o1.DisplayName = "The Direct Channel" + model.NewId()
o1.Name = NewTestId()
o1.Type = model.ChannelTypeDirect
o2 := model.Channel{}
o2.TeamId = teamId
o2.DisplayName = "Channel2" + model.NewId()
o2.Name = NewTestId()
o2.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&o2, -1)
require.NoError(t, nErr)
o3 := model.Channel{}
o3.TeamId = teamId
o3.DisplayName = "Channel3" + model.NewId()
o3.Name = NewTestId()
o3.Type = model.ChannelTypePrivate
_, nErr = ss.Channel().Save(&o3, -1)
require.NoError(t, nErr)
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
ss.Channel().SaveDirectChannel(&o1, &m1, &m2)
d1, nErr := ss.Channel().GetAllDirectChannelsForExportAfter(10000, strings.Repeat("0", 26))
assert.NoError(t, nErr)
assert.Len(t, d1, 1)
assert.Equal(t, o1.DisplayName, d1[0].DisplayName)
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testChannelStoreExportAllDirectChannelsDeletedChannel(t *testing.T, ss store.Store, s SqlStore) {
teamId := model.NewId()
o1 := model.Channel{}
o1.TeamId = teamId
o1.DisplayName = "Different Name" + model.NewId()
o1.Name = NewTestId()
o1.Type = model.ChannelTypeDirect
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
ss.Channel().SaveDirectChannel(&o1, &m1, &m2)
o1.DeleteAt = 1
nErr = ss.Channel().SetDeleteAt(o1.Id, 1, 1)
require.NoError(t, nErr, "channel should have been deleted")
d1, nErr := ss.Channel().GetAllDirectChannelsForExportAfter(10000, strings.Repeat("0", 26))
assert.NoError(t, nErr)
assert.Equal(t, 0, len(d1))
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testChannelStoreGetChannelsBatchForIndexing(t *testing.T, ss store.Store) {
// Set up all the objects needed
c1 := &model.Channel{}
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
time.Sleep(10 * time.Millisecond)
c2 := &model.Channel{}
c2.DisplayName = "Channel2"
c2.Name = NewTestId()
c2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(c2, -1)
require.NoError(t, nErr)
time.Sleep(10 * time.Millisecond)
c3 := &model.Channel{}
c3.DisplayName = "Channel3"
c3.Name = NewTestId()
c3.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(c3, -1)
require.NoError(t, nErr)
c4 := &model.Channel{}
c4.DisplayName = "Channel4"
c4.Name = NewTestId()
c4.Type = model.ChannelTypePrivate
_, nErr = ss.Channel().Save(c4, -1)
require.NoError(t, nErr)
c5 := &model.Channel{}
c5.DisplayName = "Channel5"
c5.Name = NewTestId()
c5.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(c5, -1)
require.NoError(t, nErr)
time.Sleep(10 * time.Millisecond)
c6 := &model.Channel{}
c6.DisplayName = "Channel6"
c6.Name = NewTestId()
c6.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(c6, -1)
require.NoError(t, nErr)
// First and last channel should be outside the range
channels, err := ss.Channel().GetChannelsBatchForIndexing(c1.CreateAt, "", 4)
assert.NoError(t, err)
assert.Len(t, channels, 4)
// From 4th createat+id
channels, err = ss.Channel().GetChannelsBatchForIndexing(channels[3].CreateAt, channels[3].Id, 5)
assert.NoError(t, err)
assert.Len(t, channels, 2)
// Testing the limit
channels, err = ss.Channel().GetChannelsBatchForIndexing(channels[1].CreateAt, channels[1].Id, 1)
assert.NoError(t, err)
assert.Len(t, channels, 0)
}
func testGroupSyncedChannelCount(t *testing.T, ss store.Store) {
channel1, nErr := ss.Channel().Save(&model.Channel{
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypePrivate,
GroupConstrained: model.NewBool(true),
}, 999)
require.NoError(t, nErr)
require.True(t, channel1.IsGroupConstrained())
defer ss.Channel().PermanentDelete(channel1.Id)
channel2, nErr := ss.Channel().Save(&model.Channel{
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypePrivate,
}, 999)
require.NoError(t, nErr)
require.False(t, channel2.IsGroupConstrained())
defer ss.Channel().PermanentDelete(channel2.Id)
count, err := ss.Channel().GroupSyncedChannelCount()
require.NoError(t, err)
require.GreaterOrEqual(t, count, int64(1))
channel2.GroupConstrained = model.NewBool(true)
channel2, err = ss.Channel().Update(channel2)
require.NoError(t, err)
require.True(t, channel2.IsGroupConstrained())
countAfter, err := ss.Channel().GroupSyncedChannelCount()
require.NoError(t, err)
require.GreaterOrEqual(t, countAfter, count+1)
}
func testSetShared(t *testing.T, ss store.Store) {
channel := &model.Channel{
TeamId: model.NewId(),
DisplayName: "test_share_flag",
Name: "test_share_flag",
Type: model.ChannelTypeOpen,
}
channelSaved, err := ss.Channel().Save(channel, 999)
require.NoError(t, err)
t.Run("Check default", func(t *testing.T) {
assert.False(t, channelSaved.IsShared())
})
t.Run("Set Shared flag", func(t *testing.T) {
err := ss.Channel().SetShared(channelSaved.Id, true)
require.NoError(t, err)
channelMod, err := ss.Channel().Get(channelSaved.Id, false)
require.NoError(t, err)
assert.True(t, channelMod.IsShared())
})
t.Run("Set Shared for invalid id", func(t *testing.T) {
err := ss.Channel().SetShared(model.NewId(), true)
require.Error(t, err)
})
}
func testGetTeamForChannel(t *testing.T, ss store.Store) {
team, err := ss.Team().Save(&model.Team{
Name: "myteam",
DisplayName: "DisplayName",
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel := &model.Channel{
TeamId: team.Id,
DisplayName: "test_share_flag",
Name: "test_share_flag",
Type: model.ChannelTypeOpen,
}
channelSaved, err := ss.Channel().Save(channel, 999)
require.NoError(t, err)
got, err := ss.Channel().GetTeamForChannel(channelSaved.Id)
require.NoError(t, err)
assert.Equal(t, team.Id, got.Id)
_, err = ss.Channel().GetTeamForChannel("notfound")
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
}
func testChannelPostCountsByDuration(t *testing.T, ss store.Store) {
team, err := ss.Team().Save(&model.Team{
Name: model.NewId(),
DisplayName: "DisplayName",
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
defer func() { ss.Team().PermanentDelete(team.Id) }()
channel := &model.Channel{
TeamId: team.Id,
DisplayName: "test_share_flag",
Name: "test_share_flag",
Type: model.ChannelTypeOpen,
}
channelSaved, err := ss.Channel().Save(channel, 999)
require.NoError(t, err)
defer func() { ss.Channel().PermanentDelete(channelSaved.Id) }()
userID := model.NewId()
_, err = ss.Post().Save(&model.Post{
UserId: userID,
ChannelId: channel.Id,
Message: "test",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userID,
ChannelId: channel.Id,
Message: "test",
Props: model.StringInterface{
"from_bot": true,
},
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userID,
ChannelId: channel.Id,
Message: "test",
Props: model.StringInterface{
"from_webhook": true,
},
})
require.NoError(t, err)
dpc, err := ss.Channel().PostCountsByDuration([]string{channelSaved.Id}, 0, &userID, model.PostsByDay, time.Now().Location())
require.NoError(t, err)
require.Len(t, dpc, 1)
require.Equal(t, channel.Id, dpc[0].ChannelID)
require.Equal(t, 1, dpc[0].PostCount)
}
func testGetTopInactiveChannels(t *testing.T, ss store.Store) {
team, err := ss.Team().Save(&model.Team{
Name: model.NewId(),
DisplayName: "DisplayName",
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
defer func() { ss.Team().PermanentDelete(team.Id) }()
channelPublic0 := &model.Channel{
TeamId: team.Id,
DisplayName: "test_share_flag asdf",
Name: "test_share_flag_public0",
Type: model.ChannelTypeOpen,
CreateAt: 1,
}
channelSaved0, err := ss.Channel().Save(channelPublic0, 999)
require.NoError(t, err)
defer func() { ss.Channel().PermanentDelete(channelSaved0.Id) }()
channelPublic1 := &model.Channel{
TeamId: team.Id,
DisplayName: "test_share_flag",
Name: "test_share_flag",
Type: model.ChannelTypeOpen,
CreateAt: 1,
}
channelSaved1, err := ss.Channel().Save(channelPublic1, 999)
require.NoError(t, err)
defer func() { ss.Channel().PermanentDelete(channelSaved1.Id) }()
// create private channel
c3 := model.Channel{}
c3.TeamId = team.Id
c3.DisplayName = "Channel3" + model.NewId()
c3.Name = NewTestId()
c3.Type = model.ChannelTypePrivate
c3.CreateAt = 1
channelPrivate, nErr := ss.Channel().Save(&c3, -1)
require.NoError(t, nErr)
// create private channel with post
c3NoPost := model.Channel{}
c3NoPost.TeamId = team.Id
c3NoPost.DisplayName = "Channel3" + model.NewId()
c3NoPost.Name = NewTestId()
c3NoPost.Type = model.ChannelTypePrivate
c3NoPost.CreateAt = 1
channelPrivateNoPost, nErr := ss.Channel().Save(&c3NoPost, -1)
require.NoError(t, nErr)
// create dm channel
u1 := model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err = ss.User().Save(&u1)
require.NoError(t, err)
u2 := model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(&u2)
require.NoError(t, err)
uBot := model.User{Id: model.NewId()}
_, nErr = ss.Channel().CreateDirectChannel(&u1, &u2)
require.NoError(t, nErr)
// add u1, u2 to channels
cm1 := &model.ChannelMember{ChannelId: channelPrivate.Id, UserId: u1.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}
_, err = ss.Channel().SaveMember(cm1)
require.NoError(t, err)
cm1NoPost := &model.ChannelMember{ChannelId: channelPrivateNoPost.Id, UserId: u1.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}
_, err = ss.Channel().SaveMember(cm1NoPost)
require.NoError(t, err)
cm1Public := &model.ChannelMember{ChannelId: channelPublic1.Id, UserId: u1.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}
_, err = ss.Channel().SaveMember(cm1Public)
require.NoError(t, err)
cm2 := &model.ChannelMember{ChannelId: channelPublic0.Id, UserId: u2.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}
_, err = ss.Channel().SaveMember(cm2)
require.NoError(t, err)
cmBot := &model.ChannelMember{ChannelId: channelPublic0.Id, UserId: uBot.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}
_, err = ss.Channel().SaveMember(cmBot)
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: u1.Id,
ChannelId: channelPrivate.Id,
Message: "test",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: u1.Id,
ChannelId: channelPrivate.Id,
Message: "test1",
})
require.NoError(t, err)
// create posts in channel public 0
postToCheckLastUpdateAt, err := ss.Post().Save(&model.Post{
UserId: u2.Id,
ChannelId: channelSaved0.Id,
Message: "test",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: channelPublic1.Id,
Message: "test",
Props: model.StringInterface{
"from_bot": true,
},
})
require.NoError(t, err)
// create posts in channel public 1
for i := 0; i < 3; i++ {
_, err = ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: channelPublic1.Id,
Message: "test",
})
require.NoError(t, err)
}
// for u1
t.Run("top inactive channels for team - u1 ", func(t *testing.T) {
topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForTeamSince(team.Id, u1.Id, 2, 0, 10)
require.NoError(t, err)
require.Len(t, topInactiveChannels.Items, 4)
require.Equal(t, topInactiveChannels.Items[0].ID, channelPrivateNoPost.Id)
require.Equal(t, topInactiveChannels.Items[1].ID, channelSaved0.Id)
require.Equal(t, topInactiveChannels.Items[1].LastActivityAt, postToCheckLastUpdateAt.CreateAt)
require.Equal(t, topInactiveChannels.Items[2].ID, channelPrivate.Id)
require.Equal(t, topInactiveChannels.Items[3].ID, channelPublic1.Id)
// test bot posts are counted
require.Equal(t, topInactiveChannels.Items[3].MessageCount, int64(4))
// participants
require.Equal(t, topInactiveChannels.Items[2].Participants[0], u1.Id)
require.Equal(t, topInactiveChannels.Items[3].Participants[0], u1.Id)
})
t.Run("top inactive channels for user - u1 ", func(t *testing.T) {
topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForUserSince(team.Id, u1.Id, 2, 0, 10)
require.NoError(t, err)
require.Len(t, topInactiveChannels.Items, 3)
require.Equal(t, topInactiveChannels.Items[0].ID, channelPrivateNoPost.Id)
require.Equal(t, topInactiveChannels.Items[1].ID, channelPrivate.Id)
require.Equal(t, topInactiveChannels.Items[2].ID, channelPublic1.Id)
})
// for u2
t.Run("top inactive channels for team - u2 ", func(t *testing.T) {
topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForTeamSince(team.Id, u2.Id, 2, 0, 10)
require.NoError(t, err)
require.Len(t, topInactiveChannels.Items, 2)
require.Equal(t, topInactiveChannels.Items[0].ID, channelSaved0.Id)
require.Equal(t, topInactiveChannels.Items[0].LastActivityAt, postToCheckLastUpdateAt.CreateAt)
require.Equal(t, topInactiveChannels.Items[1].ID, channelPublic1.Id)
})
t.Run("top inactive channels for user - u2 ", func(t *testing.T) {
topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForUserSince(team.Id, u2.Id, 2, 0, 10)
require.NoError(t, err)
require.Len(t, topInactiveChannels.Items, 1)
require.Equal(t, topInactiveChannels.Items[0].ID, channelPublic0.Id)
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"database/sql"
"errors"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestChannelStoreCategories(t *testing.T, ss store.Store, s SqlStore) {
t.Run("CreateInitialSidebarCategories", func(t *testing.T) { testCreateInitialSidebarCategories(t, ss) })
t.Run("CreateSidebarCategory", func(t *testing.T) { testCreateSidebarCategory(t, ss) })
t.Run("GetSidebarCategory", func(t *testing.T) { testGetSidebarCategory(t, ss, s) })
t.Run("GetSidebarCategories", func(t *testing.T) { testGetSidebarCategories(t, ss) })
t.Run("UpdateSidebarCategories", func(t *testing.T) { testUpdateSidebarCategories(t, ss) })
t.Run("ClearSidebarOnTeamLeave", func(t *testing.T) { testClearSidebarOnTeamLeave(t, ss, s) })
t.Run("DeleteSidebarCategory", func(t *testing.T) { testDeleteSidebarCategory(t, ss, s) })
t.Run("UpdateSidebarChannelsByPreferences", func(t *testing.T) { testUpdateSidebarChannelsByPreferences(t, ss) })
t.Run("SidebarCategoryDeadlock", func(t *testing.T) { testSidebarCategoryDeadlock(t, ss) })
}
func setupTeam(t *testing.T, ss store.Store, userIds ...string) *model.Team {
team, err := ss.Team().Save(&model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
assert.NoError(t, err)
members := make([]*model.TeamMember, 0, len(userIds))
for _, userId := range userIds {
members = append(members, &model.TeamMember{
TeamId: team.Id,
UserId: userId,
})
}
if len(members) > 0 {
_, err = ss.Team().SaveMultipleMembers(members, len(userIds)+1)
assert.NoError(t, err)
}
return team
}
func testCreateInitialSidebarCategories(t *testing.T, ss store.Store) {
t.Run("should create initial favorites/channels/DMs categories", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
assert.NoError(t, nErr)
require.Len(t, res.Categories, 3)
assert.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type)
assert.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type)
assert.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type)
res2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
assert.NoError(t, err)
assert.Equal(t, res, res2)
})
t.Run("should create initial favorites/channels/DMs categories for multiple users", func(t *testing.T) {
userId := model.NewId()
userId2 := model.NewId()
team := setupTeam(t, ss, userId, userId2)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
res, nErr = ss.Channel().CreateInitialSidebarCategories(userId2, opts)
assert.NoError(t, nErr)
assert.Len(t, res.Categories, 3)
assert.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type)
assert.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type)
assert.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type)
res2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId2, team.Id)
assert.NoError(t, err)
assert.Equal(t, res, res2)
})
t.Run("should create initial favorites/channels/DMs categories on different teams", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
team2 := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
opts = &store.SidebarCategorySearchOpts{
TeamID: team2.Id,
ExcludeTeam: false,
}
res, nErr = ss.Channel().CreateInitialSidebarCategories(userId, opts)
assert.NoError(t, nErr)
assert.Len(t, res.Categories, 3)
assert.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type)
assert.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type)
assert.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type)
res2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team2.Id)
assert.NoError(t, err)
assert.Equal(t, res, res2)
})
t.Run("shouldn't create additional categories when ones already exist", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Equal(t, res, initialCategories)
// Calling CreateInitialSidebarCategories a second time shouldn't create any new categories
res, nErr = ss.Channel().CreateInitialSidebarCategories(userId, opts)
assert.NoError(t, nErr)
assert.NotEmpty(t, res)
res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
assert.NoError(t, err)
assert.Equal(t, initialCategories.Categories, res.Categories)
})
t.Run("shouldn't create additional categories when ones already exist even when ran simultaneously", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
_, _ = ss.Channel().CreateInitialSidebarCategories(userId, opts)
}()
}
wg.Wait()
res, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
assert.NoError(t, err)
assert.Len(t, res.Categories, 3)
})
t.Run("should populate the Favorites category with regular channels", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Set up two channels, one favorited and one not
channel1, nErr := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
Type: model.ChannelTypeOpen,
Name: "channel1",
}, 1000)
require.NoError(t, nErr)
_, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel1.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
channel2, nErr := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
Type: model.ChannelTypeOpen,
Name: "channel2",
}, 1000)
require.NoError(t, nErr)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel2.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
nErr = ss.Preference().Save(model.Preferences{
{
UserId: userId,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel1.Id,
Value: "true",
},
})
require.NoError(t, nErr)
// Create the categories
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
categories, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.Len(t, categories.Categories, 3)
assert.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
assert.Equal(t, []string{channel1.Id}, categories.Categories[0].Channels)
assert.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type)
assert.Equal(t, []string{channel2.Id}, categories.Categories[1].Channels)
// Get and check the categories for channels
categories2, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, nErr)
require.Equal(t, categories, categories2)
})
t.Run("should populate the Favorites category in alphabetical order", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Set up two channels
channel1, nErr := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
Type: model.ChannelTypeOpen,
Name: "channel1",
DisplayName: "zebra",
}, 1000)
require.NoError(t, nErr)
_, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel1.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
channel2, nErr := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
Type: model.ChannelTypeOpen,
Name: "channel2",
DisplayName: "aardvark",
}, 1000)
require.NoError(t, nErr)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel2.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
nErr = ss.Preference().Save(model.Preferences{
{
UserId: userId,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel1.Id,
Value: "true",
},
{
UserId: userId,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel2.Id,
Value: "true",
},
})
require.NoError(t, nErr)
// Create the categories
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
categories, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.Len(t, categories.Categories, 3)
assert.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
assert.Equal(t, []string{channel2.Id, channel1.Id}, categories.Categories[0].Channels)
// Get and check the categories for channels
categories2, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, nErr)
require.Equal(t, categories, categories2)
})
t.Run("should populate the Favorites category with DMs and GMs", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
otherUserId1 := model.NewId()
otherUserId2 := model.NewId()
// Set up two direct channels, one favorited and one not
dmChannel1, err := ss.Channel().SaveDirectChannel(
&model.Channel{
Name: model.GetDMNameFromIds(userId, otherUserId1),
Type: model.ChannelTypeDirect,
},
&model.ChannelMember{
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
&model.ChannelMember{
UserId: otherUserId1,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
)
require.NoError(t, err)
dmChannel2, err := ss.Channel().SaveDirectChannel(
&model.Channel{
Name: model.GetDMNameFromIds(userId, otherUserId2),
Type: model.ChannelTypeDirect,
},
&model.ChannelMember{
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
&model.ChannelMember{
UserId: otherUserId2,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
)
require.NoError(t, err)
err = ss.Preference().Save(model.Preferences{
{
UserId: userId,
Category: model.PreferenceCategoryFavoriteChannel,
Name: dmChannel1.Id,
Value: "true",
},
})
require.NoError(t, err)
// Create the categories
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
categories, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.Len(t, categories.Categories, 3)
assert.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
assert.Equal(t, []string{dmChannel1.Id}, categories.Categories[0].Channels)
assert.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type)
assert.Equal(t, []string{dmChannel2.Id}, categories.Categories[2].Channels)
// Get and check the categories for channels
categories2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Equal(t, categories, categories2)
})
t.Run("should not populate the Favorites category with channels from other teams", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
team2 := setupTeam(t, ss, userId)
// Set up a channel on another team and favorite it
channel1, nErr := ss.Channel().Save(&model.Channel{
TeamId: team2.Id,
Type: model.ChannelTypeOpen,
Name: "channel1",
}, 1000)
require.NoError(t, nErr)
_, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel1.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
nErr = ss.Preference().Save(model.Preferences{
{
UserId: userId,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel1.Id,
Value: "true",
},
})
require.NoError(t, nErr)
// Create the categories
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
categories, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.Len(t, categories.Categories, 3)
assert.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
assert.Equal(t, []string{}, categories.Categories[0].Channels)
assert.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type)
assert.Equal(t, []string{}, categories.Categories[1].Channels)
// Get and check the categories for channels
categories2, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, nErr)
require.Equal(t, categories, categories2)
})
t.Run("graphQL path to create initial favorites/channels/DMs categories on different teams", func(t *testing.T) {
userId := model.NewId()
t1 := &model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
InviteId: model.NewId(),
}
t1, err := ss.Team().Save(t1)
require.NoError(t, err)
m1 := &model.TeamMember{TeamId: t1.Id, UserId: userId}
_, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
t2 := &model.Team{
DisplayName: "DisplayName2",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
InviteId: model.NewId(),
}
t2, err = ss.Team().Save(t2)
require.NoError(t, err)
m2 := &model.TeamMember{TeamId: t2.Id, UserId: userId}
_, nErr = ss.Team().SaveMember(m2, -1)
require.NoError(t, nErr)
opts := &store.SidebarCategorySearchOpts{
TeamID: t1.Id,
ExcludeTeam: true,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
for _, cat := range res.Categories {
assert.Equal(t, t2.Id, cat.TeamId)
}
})
}
func testCreateSidebarCategory(t *testing.T, ss store.Store) {
t.Run("Creating category without initial categories should fail", func(t *testing.T) {
userId := model.NewId()
teamId := model.NewId()
// Create the category
created, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
DisplayName: model.NewId(),
},
})
require.Error(t, err)
var errNotFound *store.ErrNotFound
require.ErrorAs(t, err, &errNotFound)
require.Nil(t, created)
})
t.Run("should place the new category second if Favorites comes first", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
// Create the category
created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
DisplayName: model.NewId(),
},
})
require.NoError(t, err)
// Confirm that it comes second
res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Len(t, res.Categories, 4)
assert.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type)
assert.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type)
assert.Equal(t, created.Id, res.Categories[1].Id)
})
t.Run("should place the new category first if Favorites is not first", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
// Re-arrange the categories so that Favorites comes last
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Len(t, categories.Categories, 3)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
err = ss.Channel().UpdateSidebarCategoryOrder(userId, team.Id, []string{
categories.Categories[1].Id,
categories.Categories[2].Id,
categories.Categories[0].Id,
})
require.NoError(t, err)
// Create the category
created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
DisplayName: model.NewId(),
},
})
require.NoError(t, err)
// Confirm that it comes first
res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Len(t, res.Categories, 4)
assert.Equal(t, model.SidebarCategoryCustom, res.Categories[0].Type)
assert.Equal(t, created.Id, res.Categories[0].Id)
})
t.Run("should create the category with its channels", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
// Create some channels
channel1, err := ss.Channel().Save(&model.Channel{
Type: model.ChannelTypeOpen,
TeamId: team.Id,
Name: model.NewId(),
}, 100)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
Type: model.ChannelTypeOpen,
TeamId: team.Id,
Name: model.NewId(),
}, 100)
require.NoError(t, err)
// Create the category
created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
DisplayName: model.NewId(),
},
Channels: []string{channel2.Id, channel1.Id},
})
require.NoError(t, err)
assert.Equal(t, []string{channel2.Id, channel1.Id}, created.Channels)
// Get the channel again to ensure that the SidebarChannels were saved correctly
res2, err := ss.Channel().GetSidebarCategory(created.Id)
require.NoError(t, err)
assert.Equal(t, []string{channel2.Id, channel1.Id}, res2.Channels)
})
t.Run("should remove any channels from their previous categories", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Len(t, categories.Categories, 3)
favoritesCategory := categories.Categories[0]
require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type)
channelsCategory := categories.Categories[1]
require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
// Create some channels
channel1, nErr := ss.Channel().Save(&model.Channel{
Type: model.ChannelTypeOpen,
TeamId: team.Id,
Name: model.NewId(),
}, 100)
require.NoError(t, nErr)
channel2, nErr := ss.Channel().Save(&model.Channel{
Type: model.ChannelTypeOpen,
TeamId: team.Id,
Name: model.NewId(),
}, 100)
require.NoError(t, nErr)
// Assign them to categories
favoritesCategory.Channels = []string{channel1.Id}
channelsCategory.Channels = []string{channel2.Id}
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
favoritesCategory,
channelsCategory,
})
require.NoError(t, err)
// Create the category
created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
DisplayName: model.NewId(),
},
Channels: []string{channel2.Id, channel1.Id},
})
require.NoError(t, err)
assert.Equal(t, []string{channel2.Id, channel1.Id}, created.Channels)
// Confirm that the channels were removed from their original categories
res2, err := ss.Channel().GetSidebarCategory(favoritesCategory.Id)
require.NoError(t, err)
assert.Equal(t, []string{}, res2.Channels)
res2, err = ss.Channel().GetSidebarCategory(channelsCategory.Id)
require.NoError(t, err)
assert.Equal(t, []string{}, res2.Channels)
})
}
func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) {
t.Run("should return a custom category with its Channels field set", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
channelId1 := model.NewId()
channelId2 := model.NewId()
channelId3 := model.NewId()
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
// Create a category and assign some channels to it
created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
UserId: userId,
TeamId: team.Id,
DisplayName: model.NewId(),
},
Channels: []string{channelId1, channelId2, channelId3},
})
require.NoError(t, err)
require.NotNil(t, created)
// Ensure that they're returned in order
res2, err := ss.Channel().GetSidebarCategory(created.Id)
assert.NoError(t, err)
assert.Equal(t, created.Id, res2.Id)
assert.Equal(t, model.SidebarCategoryCustom, res2.Type)
assert.Equal(t, created.DisplayName, res2.DisplayName)
assert.Equal(t, []string{channelId1, channelId2, channelId3}, res2.Channels)
})
t.Run("should return any orphaned channels with the Channels category", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Create the initial categories and find the channels category
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
channelsCategory := categories.Categories[1]
require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
// Join some channels
channel1, nErr := ss.Channel().Save(&model.Channel{
Name: "channel1",
DisplayName: "DEF",
TeamId: team.Id,
Type: model.ChannelTypePrivate,
}, 10)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: channel1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
channel2, nErr := ss.Channel().Save(&model.Channel{
Name: "channel2",
DisplayName: "ABC",
TeamId: team.Id,
Type: model.ChannelTypeOpen,
}, 10)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: channel2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
// Confirm that they're not in the Channels category in the DB
var count int64
countErr := s.GetMasterX().Get(&count, `
SELECT
COUNT(*)
FROM
SidebarChannels
WHERE
CategoryId = ?`, channelsCategory.Id)
require.NoError(t, countErr)
assert.Equal(t, int64(0), count)
// Ensure that the Channels are returned in alphabetical order
res2, err := ss.Channel().GetSidebarCategory(channelsCategory.Id)
assert.NoError(t, err)
assert.Equal(t, channelsCategory.Id, res2.Id)
assert.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
assert.Equal(t, []string{channel2.Id, channel1.Id}, res2.Channels)
})
t.Run("shouldn't return orphaned channels on another team with the Channels category", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Create the initial categories and find the channels category
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type)
channelsCategory := categories.Categories[1]
// Join a channel on another team
channel1, nErr := ss.Channel().Save(&model.Channel{
Name: "abc",
TeamId: model.NewId(),
Type: model.ChannelTypeOpen,
}, 10)
require.NoError(t, nErr)
defer ss.Channel().PermanentDelete(channel1.Id)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: channel1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
// Ensure that no channels are returned
res2, err := ss.Channel().GetSidebarCategory(channelsCategory.Id)
assert.NoError(t, err)
assert.Equal(t, channelsCategory.Id, res2.Id)
assert.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
assert.Len(t, res2.Channels, 0)
})
t.Run("shouldn't return non-orphaned channels with the Channels category", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
// Create the initial categories and find the channels category
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
favoritesCategory := categories.Categories[0]
require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type)
channelsCategory := categories.Categories[1]
require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
// Join some channels
channel1, nErr := ss.Channel().Save(&model.Channel{
Name: "channel1",
DisplayName: "DEF",
TeamId: team.Id,
Type: model.ChannelTypePrivate,
}, 10)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: channel1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
channel2, nErr := ss.Channel().Save(&model.Channel{
Name: "channel2",
DisplayName: "ABC",
TeamId: team.Id,
Type: model.ChannelTypeOpen,
}, 10)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: channel2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
// And assign one to another category
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: favoritesCategory.SidebarCategory,
Channels: []string{channel2.Id},
},
})
require.NoError(t, err)
// Ensure that the correct channel is returned in the Channels category
res2, err := ss.Channel().GetSidebarCategory(channelsCategory.Id)
assert.NoError(t, err)
assert.Equal(t, channelsCategory.Id, res2.Id)
assert.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
assert.Equal(t, []string{channel1.Id}, res2.Channels)
})
t.Run("should return any orphaned DM channels with the Direct Messages category", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Create the initial categories and find the DMs category
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type)
dmsCategory := categories.Categories[2]
// Create a DM
otherUserId := model.NewId()
dmChannel, nErr := ss.Channel().SaveDirectChannel(
&model.Channel{
Name: model.GetDMNameFromIds(userId, otherUserId),
Type: model.ChannelTypeDirect,
},
&model.ChannelMember{
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
&model.ChannelMember{
UserId: otherUserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
)
require.NoError(t, nErr)
// Ensure that the DM is returned
res2, err := ss.Channel().GetSidebarCategory(dmsCategory.Id)
assert.NoError(t, err)
assert.Equal(t, dmsCategory.Id, res2.Id)
assert.Equal(t, model.SidebarCategoryDirectMessages, res2.Type)
assert.Equal(t, []string{dmChannel.Id}, res2.Channels)
})
t.Run("should return any orphaned GM channels with the Direct Messages category", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Create the initial categories and find the DMs category
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type)
dmsCategory := categories.Categories[2]
// Create a GM
gmChannel, nErr := ss.Channel().Save(&model.Channel{
Name: "abc",
TeamId: "",
Type: model.ChannelTypeGroup,
}, 10)
require.NoError(t, nErr)
defer ss.Channel().PermanentDelete(gmChannel.Id)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: gmChannel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
// Ensure that the DM is returned
res2, err := ss.Channel().GetSidebarCategory(dmsCategory.Id)
assert.NoError(t, err)
assert.Equal(t, dmsCategory.Id, res2.Id)
assert.Equal(t, model.SidebarCategoryDirectMessages, res2.Type)
assert.Equal(t, []string{gmChannel.Id}, res2.Channels)
})
t.Run("should return orphaned DM channels in the DMs category which are in a custom category on another team", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Create the initial categories and find the DMs category
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type)
dmsCategory := categories.Categories[2]
// Create a DM
otherUserId := model.NewId()
dmChannel, nErr := ss.Channel().SaveDirectChannel(
&model.Channel{
Name: model.GetDMNameFromIds(userId, otherUserId),
Type: model.ChannelTypeDirect,
},
&model.ChannelMember{
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
&model.ChannelMember{
UserId: otherUserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
)
require.NoError(t, nErr)
// Create another team and assign the DM to a custom category on that team
otherTeam := setupTeam(t, ss, userId)
opts = &store.SidebarCategorySearchOpts{
TeamID: otherTeam.Id,
ExcludeTeam: false,
}
res, nErr = ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
_, err = ss.Channel().CreateSidebarCategory(userId, otherTeam.Id, &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
UserId: userId,
TeamId: team.Id,
},
Channels: []string{dmChannel.Id},
})
require.NoError(t, err)
// Ensure that the DM is returned with the DMs category on the original team
res2, err := ss.Channel().GetSidebarCategory(dmsCategory.Id)
assert.NoError(t, err)
assert.Equal(t, dmsCategory.Id, res2.Id)
assert.Equal(t, model.SidebarCategoryDirectMessages, res2.Type)
assert.Equal(t, []string{dmChannel.Id}, res2.Channels)
})
}
func testGetSidebarCategories(t *testing.T, ss store.Store) {
t.Run("should return channels in the same order between different ways of getting categories", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
channelIds := []string{
model.NewId(),
model.NewId(),
model.NewId(),
}
newCategory, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{
Channels: channelIds,
})
require.NoError(t, err)
require.NotNil(t, newCategory)
gotCategory, err := ss.Channel().GetSidebarCategory(newCategory.Id)
require.NoError(t, err)
res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Len(t, res.Categories, 4)
require.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type)
// This looks unnecessary, but I was getting different results from some of these before
assert.Equal(t, newCategory.Channels, res.Categories[1].Channels)
assert.Equal(t, gotCategory.Channels, res.Categories[1].Channels)
assert.Equal(t, channelIds, res.Categories[1].Channels)
})
t.Run("should not return categories for teams deleted, or no longer a member", func(t *testing.T) {
userId := model.NewId()
teamMember1 := setupTeam(t, ss, userId)
teamMember2 := setupTeam(t, ss, userId)
teamDeleted := setupTeam(t, ss, userId)
teamDeleted.DeleteAt = model.GetMillis()
ss.Team().Update(teamDeleted)
teamNotMember := setupTeam(t, ss)
teamDeletedMember := setupTeam(t, ss, userId)
members, err := ss.Team().GetMembersByIds(teamDeletedMember.Id, []string{userId}, nil)
require.NoError(t, err)
require.NotEmpty(t, members)
member := members[0]
member.DeleteAt = model.GetMillis()
ss.Team().UpdateMember(member)
teamIds := []string{
teamMember1.Id,
teamMember2.Id,
teamDeleted.Id,
teamNotMember.Id,
teamDeletedMember.Id,
}
for _, id := range teamIds {
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, &store.SidebarCategorySearchOpts{TeamID: id})
require.NoError(t, nErr)
require.NotEmpty(t, res)
}
opts := &store.SidebarCategorySearchOpts{
TeamID: teamMember1.Id,
ExcludeTeam: false,
}
// Team member and not exclude
res, err := ss.Channel().GetSidebarCategories(userId, opts)
require.NoError(t, err)
assert.Equal(t, 3, len(res.Categories))
// No team member and not exclude
opts.TeamID = teamDeleted.Id
res, err = ss.Channel().GetSidebarCategories(userId, opts)
require.NoError(t, err)
assert.Equal(t, 0, len(res.Categories))
// No team member and exclude
opts.ExcludeTeam = true
res, err = ss.Channel().GetSidebarCategories(userId, opts)
require.NoError(t, err)
assert.Equal(t, 6, len(res.Categories))
// Team member and exclude
opts.TeamID = teamMember1.Id
res, err = ss.Channel().GetSidebarCategories(userId, opts)
require.NoError(t, err)
assert.Equal(t, 3, len(res.Categories))
})
}
func testUpdateSidebarCategories(t *testing.T, ss store.Store) {
t.Run("ensure the query to update SidebarCategories hasn't been polluted by UpdateSidebarCategoryOrder", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Create the initial categories
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, err := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, err)
require.NotEmpty(t, res)
initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
favoritesCategory := initialCategories.Categories[0]
channelsCategory := initialCategories.Categories[1]
dmsCategory := initialCategories.Categories[2]
// And then update one of them
updated, _, err := ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
channelsCategory,
})
require.NoError(t, err)
assert.Equal(t, channelsCategory, updated[0])
assert.Equal(t, "Channels", updated[0].DisplayName)
// And then reorder the categories
err = ss.Channel().UpdateSidebarCategoryOrder(userId, team.Id, []string{dmsCategory.Id, favoritesCategory.Id, channelsCategory.Id})
require.NoError(t, err)
// Which somehow blanks out stuff because ???
got, err := ss.Channel().GetSidebarCategory(favoritesCategory.Id)
require.NoError(t, err)
assert.Equal(t, "Favorites", got.DisplayName)
})
t.Run("categories should be returned in their original order", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Create the initial categories
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, err := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, err)
require.NotEmpty(t, res)
initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
favoritesCategory := initialCategories.Categories[0]
channelsCategory := initialCategories.Categories[1]
dmsCategory := initialCategories.Categories[2]
// And then update them
updatedCategories, _, err := ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
favoritesCategory,
channelsCategory,
dmsCategory,
})
assert.NoError(t, err)
assert.Equal(t, favoritesCategory.Id, updatedCategories[0].Id)
assert.Equal(t, channelsCategory.Id, updatedCategories[1].Id)
assert.Equal(t, dmsCategory.Id, updatedCategories[2].Id)
})
t.Run("should silently fail to update read only fields", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
favoritesCategory := initialCategories.Categories[0]
channelsCategory := initialCategories.Categories[1]
dmsCategory := initialCategories.Categories[2]
customCategory, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{})
require.NoError(t, err)
categoriesToUpdate := []*model.SidebarCategoryWithChannels{
// Try to change the type of Favorites
{
SidebarCategory: model.SidebarCategory{
Id: favoritesCategory.Id,
DisplayName: "something else",
},
Channels: favoritesCategory.Channels,
},
// Try to change the type of Channels
{
SidebarCategory: model.SidebarCategory{
Id: channelsCategory.Id,
Type: model.SidebarCategoryDirectMessages,
},
Channels: channelsCategory.Channels,
},
// Try to change the Channels of DMs
{
SidebarCategory: dmsCategory.SidebarCategory,
Channels: []string{"fakechannel"},
},
// Try to change the UserId/TeamId of a custom category
{
SidebarCategory: model.SidebarCategory{
Id: customCategory.Id,
UserId: model.NewId(),
TeamId: model.NewId(),
Sorting: customCategory.Sorting,
DisplayName: customCategory.DisplayName,
},
Channels: customCategory.Channels,
},
}
updatedCategories, _, err := ss.Channel().UpdateSidebarCategories(userId, team.Id, categoriesToUpdate)
assert.NoError(t, err)
assert.NotEqual(t, "Favorites", categoriesToUpdate[0].DisplayName)
assert.Equal(t, "Favorites", updatedCategories[0].DisplayName)
assert.NotEqual(t, model.SidebarCategoryChannels, categoriesToUpdate[1].Type)
assert.Equal(t, model.SidebarCategoryChannels, updatedCategories[1].Type)
assert.NotEqual(t, []string{}, categoriesToUpdate[2].Channels)
assert.Equal(t, []string{}, updatedCategories[2].Channels)
assert.NotEqual(t, userId, categoriesToUpdate[3].UserId)
assert.Equal(t, userId, updatedCategories[3].UserId)
})
t.Run("should add and remove favorites preferences based on the Favorites category", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Create the initial categories and find the favorites category
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
favoritesCategory := categories.Categories[0]
require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type)
// Join a channel
channel, nErr := ss.Channel().Save(&model.Channel{
Name: "channel",
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}, 10)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: channel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
// Assign it to favorites
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: favoritesCategory.SidebarCategory,
Channels: []string{channel.Id},
},
})
assert.NoError(t, err)
res2, nErr := ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, channel.Id)
assert.NoError(t, nErr)
assert.NotNil(t, res2)
assert.Equal(t, "true", res2.Value)
// And then remove it
channelsCategory := categories.Categories[1]
require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: []string{channel.Id},
},
})
assert.NoError(t, err)
res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, channel.Id)
assert.Error(t, nErr)
assert.True(t, errors.Is(nErr, sql.ErrNoRows))
assert.Nil(t, res2)
})
t.Run("should add and remove favorites preferences for DMs", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Create the initial categories and find the favorites category
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
favoritesCategory := categories.Categories[0]
require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type)
// Create a direct channel
otherUserId := model.NewId()
dmChannel, nErr := ss.Channel().SaveDirectChannel(
&model.Channel{
Name: model.GetDMNameFromIds(userId, otherUserId),
Type: model.ChannelTypeDirect,
},
&model.ChannelMember{
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
&model.ChannelMember{
UserId: otherUserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
)
assert.NoError(t, nErr)
// Assign it to favorites
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: favoritesCategory.SidebarCategory,
Channels: []string{dmChannel.Id},
},
})
assert.NoError(t, err)
res2, nErr := ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, dmChannel.Id)
assert.NoError(t, nErr)
assert.NotNil(t, res2)
assert.Equal(t, "true", res2.Value)
// And then remove it
dmsCategory := categories.Categories[2]
require.Equal(t, model.SidebarCategoryDirectMessages, dmsCategory.Type)
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: dmsCategory.SidebarCategory,
Channels: []string{dmChannel.Id},
},
})
assert.NoError(t, err)
res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, dmChannel.Id)
assert.Error(t, nErr)
assert.True(t, errors.Is(nErr, sql.ErrNoRows))
assert.Nil(t, res2)
})
t.Run("should add and remove favorites preferences, even if the channel is already favorited in preferences", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
team2 := setupTeam(t, ss, userId)
// Create the initial categories and find the favorites categories in each team
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
favoritesCategory := categories.Categories[0]
require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type)
opts = &store.SidebarCategorySearchOpts{
TeamID: team2.Id,
ExcludeTeam: false,
}
res, nErr = ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team2.Id)
require.NoError(t, err)
favoritesCategory2 := categories2.Categories[0]
require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory2.Type)
// Create a direct channel
otherUserId := model.NewId()
dmChannel, nErr := ss.Channel().SaveDirectChannel(
&model.Channel{
Name: model.GetDMNameFromIds(userId, otherUserId),
Type: model.ChannelTypeDirect,
},
&model.ChannelMember{
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
&model.ChannelMember{
UserId: otherUserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
)
assert.NoError(t, nErr)
// Assign it to favorites on the first team. The favorites preference gets set for all teams.
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: favoritesCategory.SidebarCategory,
Channels: []string{dmChannel.Id},
},
})
assert.NoError(t, err)
res2, nErr := ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, dmChannel.Id)
assert.NoError(t, nErr)
assert.NotNil(t, res2)
assert.Equal(t, "true", res2.Value)
// Assign it to favorites on the second team. The favorites preference is already set.
updated, _, err := ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: favoritesCategory2.SidebarCategory,
Channels: []string{dmChannel.Id},
},
})
assert.NoError(t, err)
assert.Equal(t, []string{dmChannel.Id}, updated[0].Channels)
res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, dmChannel.Id)
assert.NoError(t, nErr)
assert.NotNil(t, res2)
assert.Equal(t, "true", res2.Value)
// Remove it from favorites on the first team. This clears the favorites preference for all teams.
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: favoritesCategory.SidebarCategory,
Channels: []string{},
},
})
assert.NoError(t, err)
res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, dmChannel.Id)
require.Error(t, nErr)
assert.Nil(t, res2)
// Remove it from favorites on the second team. The favorites preference was already deleted.
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: favoritesCategory2.SidebarCategory,
Channels: []string{},
},
})
assert.NoError(t, err)
res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, dmChannel.Id)
require.Error(t, nErr)
assert.Nil(t, res2)
})
t.Run("should not affect other users' favorites preferences", func(t *testing.T) {
userId := model.NewId()
userId2 := model.NewId()
team := setupTeam(t, ss, userId, userId2)
// Create the initial categories and find the favorites category
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
favoritesCategory := categories.Categories[0]
require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type)
channelsCategory := categories.Categories[1]
require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
// Create the other users' categories
res, nErr = ss.Channel().CreateInitialSidebarCategories(userId2, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
categories2, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId2, team.Id)
require.NoError(t, err)
favoritesCategory2 := categories2.Categories[0]
require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory2.Type)
channelsCategory2 := categories2.Categories[1]
require.Equal(t, model.SidebarCategoryChannels, channelsCategory2.Type)
// Have both users join a channel
channel, nErr := ss.Channel().Save(&model.Channel{
Name: "channel",
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}, 10)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: channel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId2,
ChannelId: channel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
// Have user1 favorite it
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: favoritesCategory.SidebarCategory,
Channels: []string{channel.Id},
},
{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: []string{},
},
})
assert.NoError(t, err)
res2, nErr := ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, channel.Id)
assert.NoError(t, nErr)
assert.NotNil(t, res2)
assert.Equal(t, "true", res2.Value)
res2, nErr = ss.Preference().Get(userId2, model.PreferenceCategoryFavoriteChannel, channel.Id)
assert.True(t, errors.Is(nErr, sql.ErrNoRows))
assert.Nil(t, res2)
// And user2 favorite it
_, _, err = ss.Channel().UpdateSidebarCategories(userId2, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: favoritesCategory2.SidebarCategory,
Channels: []string{channel.Id},
},
{
SidebarCategory: channelsCategory2.SidebarCategory,
Channels: []string{},
},
})
assert.NoError(t, err)
res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, channel.Id)
assert.NoError(t, nErr)
assert.NotNil(t, res2)
assert.Equal(t, "true", res2.Value)
res2, nErr = ss.Preference().Get(userId2, model.PreferenceCategoryFavoriteChannel, channel.Id)
assert.NoError(t, nErr)
assert.NotNil(t, res2)
assert.Equal(t, "true", res2.Value)
// And then user1 unfavorite it
_, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: []string{channel.Id},
},
{
SidebarCategory: favoritesCategory.SidebarCategory,
Channels: []string{},
},
})
assert.NoError(t, err)
res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, channel.Id)
assert.True(t, errors.Is(nErr, sql.ErrNoRows))
assert.Nil(t, res2)
res2, nErr = ss.Preference().Get(userId2, model.PreferenceCategoryFavoriteChannel, channel.Id)
assert.NoError(t, nErr)
assert.NotNil(t, res2)
assert.Equal(t, "true", res2.Value)
// And finally user2 favorite it
_, _, err = ss.Channel().UpdateSidebarCategories(userId2, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: channelsCategory2.SidebarCategory,
Channels: []string{channel.Id},
},
{
SidebarCategory: favoritesCategory2.SidebarCategory,
Channels: []string{},
},
})
assert.NoError(t, err)
res2, nErr = ss.Preference().Get(userId, model.PreferenceCategoryFavoriteChannel, channel.Id)
assert.True(t, errors.Is(nErr, sql.ErrNoRows))
assert.Nil(t, res2)
res2, nErr = ss.Preference().Get(userId2, model.PreferenceCategoryFavoriteChannel, channel.Id)
assert.True(t, errors.Is(nErr, sql.ErrNoRows))
assert.Nil(t, res2)
})
t.Run("channels removed from Channels or DMs categories should be re-added", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Create some channels
channel, nErr := ss.Channel().Save(&model.Channel{
Name: "channel",
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}, 10)
require.NoError(t, nErr)
_, err := ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: channel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
otherUserId := model.NewId()
dmChannel, nErr := ss.Channel().SaveDirectChannel(
&model.Channel{
Name: model.GetDMNameFromIds(userId, otherUserId),
Type: model.ChannelTypeDirect,
},
&model.ChannelMember{
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
&model.ChannelMember{
UserId: otherUserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
)
require.NoError(t, nErr)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
// And some categories
initialCategories, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, nErr)
channelsCategory := initialCategories.Categories[1]
dmsCategory := initialCategories.Categories[2]
require.Equal(t, []string{channel.Id}, channelsCategory.Channels)
require.Equal(t, []string{dmChannel.Id}, dmsCategory.Channels)
// Try to save the categories with no channels in them
categoriesToUpdate := []*model.SidebarCategoryWithChannels{
{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: []string{},
},
{
SidebarCategory: dmsCategory.SidebarCategory,
Channels: []string{},
},
}
updatedCategories, _, nErr := ss.Channel().UpdateSidebarCategories(userId, team.Id, categoriesToUpdate)
assert.NoError(t, nErr)
// The channels should still exist in the category because they would otherwise be orphaned
assert.Equal(t, []string{channel.Id}, updatedCategories[0].Channels)
assert.Equal(t, []string{dmChannel.Id}, updatedCategories[1].Channels)
})
t.Run("should be able to move DMs into and out of custom categories", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
otherUserId := model.NewId()
dmChannel, nErr := ss.Channel().SaveDirectChannel(
&model.Channel{
Name: model.GetDMNameFromIds(userId, otherUserId),
Type: model.ChannelTypeDirect,
},
&model.ChannelMember{
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
&model.ChannelMember{
UserId: otherUserId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
},
)
require.NoError(t, nErr)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
// The DM should start in the DMs category
initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
dmsCategory := initialCategories.Categories[2]
require.Equal(t, []string{dmChannel.Id}, dmsCategory.Channels)
// Now move the DM into a custom category
customCategory, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{})
require.NoError(t, err)
categoriesToUpdate := []*model.SidebarCategoryWithChannels{
{
SidebarCategory: dmsCategory.SidebarCategory,
Channels: []string{},
},
{
SidebarCategory: customCategory.SidebarCategory,
Channels: []string{dmChannel.Id},
},
}
updatedCategories, _, err := ss.Channel().UpdateSidebarCategories(userId, team.Id, categoriesToUpdate)
assert.NoError(t, err)
assert.Equal(t, dmsCategory.Id, updatedCategories[0].Id)
assert.Equal(t, []string{}, updatedCategories[0].Channels)
assert.Equal(t, customCategory.Id, updatedCategories[1].Id)
assert.Equal(t, []string{dmChannel.Id}, updatedCategories[1].Channels)
updatedDmsCategory, err := ss.Channel().GetSidebarCategory(dmsCategory.Id)
require.NoError(t, err)
assert.Equal(t, []string{}, updatedDmsCategory.Channels)
updatedCustomCategory, err := ss.Channel().GetSidebarCategory(customCategory.Id)
require.NoError(t, err)
assert.Equal(t, []string{dmChannel.Id}, updatedCustomCategory.Channels)
// And move it back out of the custom category
categoriesToUpdate = []*model.SidebarCategoryWithChannels{
{
SidebarCategory: dmsCategory.SidebarCategory,
Channels: []string{dmChannel.Id},
},
{
SidebarCategory: customCategory.SidebarCategory,
Channels: []string{},
},
}
updatedCategories, _, err = ss.Channel().UpdateSidebarCategories(userId, team.Id, categoriesToUpdate)
assert.NoError(t, err)
assert.Equal(t, dmsCategory.Id, updatedCategories[0].Id)
assert.Equal(t, []string{dmChannel.Id}, updatedCategories[0].Channels)
assert.Equal(t, customCategory.Id, updatedCategories[1].Id)
assert.Equal(t, []string{}, updatedCategories[1].Channels)
updatedDmsCategory, err = ss.Channel().GetSidebarCategory(dmsCategory.Id)
require.NoError(t, err)
assert.Equal(t, []string{dmChannel.Id}, updatedDmsCategory.Channels)
updatedCustomCategory, err = ss.Channel().GetSidebarCategory(customCategory.Id)
require.NoError(t, err)
assert.Equal(t, []string{}, updatedCustomCategory.Channels)
})
t.Run("should successfully move channels between categories", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Join a channel
channel, nErr := ss.Channel().Save(&model.Channel{
Name: "channel",
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}, 10)
require.NoError(t, nErr)
_, err := ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: channel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
// And then create the initial categories so that it includes the channel
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
initialCategories, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, nErr)
channelsCategory := initialCategories.Categories[1]
require.Equal(t, []string{channel.Id}, channelsCategory.Channels)
customCategory, nErr := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{})
require.NoError(t, nErr)
// Move the channel one way
updatedCategories, _, nErr := ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: []string{},
},
{
SidebarCategory: customCategory.SidebarCategory,
Channels: []string{channel.Id},
},
})
assert.NoError(t, nErr)
assert.Equal(t, []string{}, updatedCategories[0].Channels)
assert.Equal(t, []string{channel.Id}, updatedCategories[1].Channels)
// And then the other
updatedCategories, _, nErr = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: []string{channel.Id},
},
{
SidebarCategory: customCategory.SidebarCategory,
Channels: []string{},
},
})
assert.NoError(t, nErr)
assert.Equal(t, []string{channel.Id}, updatedCategories[0].Channels)
assert.Equal(t, []string{}, updatedCategories[1].Channels)
})
t.Run("should correctly return the original categories that were modified", func(t *testing.T) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
// Join a channel
channel, nErr := ss.Channel().Save(&model.Channel{
Name: "channel",
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}, 10)
require.NoError(t, nErr)
_, err := ss.Channel().SaveMember(&model.ChannelMember{
UserId: userId,
ChannelId: channel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
// And then create the initial categories so that Channels includes the channel
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
initialCategories, nErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, nErr)
channelsCategory := initialCategories.Categories[1]
require.Equal(t, []string{channel.Id}, channelsCategory.Channels)
customCategory, nErr := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
DisplayName: "originalName",
},
})
require.NoError(t, nErr)
// Rename the custom category
updatedCategories, originalCategories, nErr := ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: model.SidebarCategory{
Id: customCategory.Id,
DisplayName: "updatedName",
},
},
})
require.NoError(t, nErr)
require.Equal(t, len(updatedCategories), len(originalCategories))
assert.Equal(t, "originalName", originalCategories[0].DisplayName)
assert.Equal(t, "updatedName", updatedCategories[0].DisplayName)
// Move a channel
updatedCategories, originalCategories, nErr = ss.Channel().UpdateSidebarCategories(userId, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: []string{},
},
{
SidebarCategory: customCategory.SidebarCategory,
Channels: []string{channel.Id},
},
})
require.NoError(t, nErr)
require.Equal(t, len(updatedCategories), len(originalCategories))
require.Equal(t, updatedCategories[0].Id, originalCategories[0].Id)
require.Equal(t, updatedCategories[1].Id, originalCategories[1].Id)
assert.Equal(t, []string{channel.Id}, originalCategories[0].Channels)
assert.Equal(t, []string{}, updatedCategories[0].Channels)
assert.Equal(t, []string{}, originalCategories[1].Channels)
assert.Equal(t, []string{channel.Id}, updatedCategories[1].Channels)
})
}
func setupInitialSidebarCategories(t *testing.T, ss store.Store) (string, string) {
userId := model.NewId()
team := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
res, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
require.NoError(t, err)
require.Len(t, res.Categories, 3)
return userId, team.Id
}
func testClearSidebarOnTeamLeave(t *testing.T, ss store.Store, s SqlStore) {
t.Run("should delete all sidebar categories and channels on the team", func(t *testing.T) {
userId, teamId := setupInitialSidebarCategories(t, ss)
user := &model.User{
Id: userId,
}
// Create some channels and assign them to a custom category
channel1, nErr := ss.Channel().Save(&model.Channel{
Name: model.NewId(),
TeamId: teamId,
Type: model.ChannelTypeOpen,
}, 1000)
require.NoError(t, nErr)
dmChannel1, nErr := ss.Channel().CreateDirectChannel(user, &model.User{
Id: model.NewId(),
})
require.NoError(t, nErr)
_, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{
Channels: []string{channel1.Id, dmChannel1.Id},
})
require.NoError(t, err)
// Confirm that we start with the right number of categories and SidebarChannels entries
var count int64
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarCategories WHERE UserId = ?", userId)
require.NoError(t, err)
require.Equal(t, int64(4), count)
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarChannels WHERE UserId = ?", userId)
require.NoError(t, err)
require.Equal(t, int64(2), count)
// Leave the team
err = ss.Channel().ClearSidebarOnTeamLeave(userId, teamId)
assert.NoError(t, err)
// Confirm that all the categories and SidebarChannel entries have been deleted
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarCategories WHERE UserId = ?", userId)
require.NoError(t, err)
assert.Equal(t, int64(0), count)
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarChannels WHERE UserId = ?", userId)
require.NoError(t, err)
assert.Equal(t, int64(0), count)
})
t.Run("should not delete sidebar categories and channels on another the team", func(t *testing.T) {
userId, teamId := setupInitialSidebarCategories(t, ss)
user := &model.User{
Id: userId,
}
// Create some channels and assign them to a custom category
channel1, nErr := ss.Channel().Save(&model.Channel{
Name: model.NewId(),
TeamId: teamId,
Type: model.ChannelTypeOpen,
}, 1000)
require.NoError(t, nErr)
dmChannel1, nErr := ss.Channel().CreateDirectChannel(user, &model.User{
Id: model.NewId(),
})
require.NoError(t, nErr)
_, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{
Channels: []string{channel1.Id, dmChannel1.Id},
})
require.NoError(t, err)
// Confirm that we start with the right number of categories and SidebarChannels entries
var count int64
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarCategories WHERE UserId = ?", userId)
require.NoError(t, err)
require.Equal(t, int64(4), count)
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarChannels WHERE UserId = ?", userId)
require.NoError(t, err)
require.Equal(t, int64(2), count)
// Leave another team
err = ss.Channel().ClearSidebarOnTeamLeave(userId, model.NewId())
assert.NoError(t, err)
// Confirm that nothing has been deleted
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarCategories WHERE UserId = ?", userId)
require.NoError(t, err)
assert.Equal(t, int64(4), count)
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarChannels WHERE UserId = ?", userId)
require.NoError(t, err)
assert.Equal(t, int64(2), count)
})
t.Run("MM-30314 should not delete channels on another team under specific circumstances", func(t *testing.T) {
userId, teamId := setupInitialSidebarCategories(t, ss)
user := &model.User{
Id: userId,
}
user2 := &model.User{
Id: model.NewId(),
}
// Create a second team and set up the sidebar categories for it
team2 := setupTeam(t, ss, userId)
opts := &store.SidebarCategorySearchOpts{
TeamID: team2.Id,
ExcludeTeam: false,
}
res, err := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, err)
require.NotEmpty(t, res)
res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team2.Id)
require.NoError(t, err)
require.Len(t, res.Categories, 3)
// On the first team, create some channels and assign them to a custom category
channel1, nErr := ss.Channel().Save(&model.Channel{
Name: model.NewId(),
TeamId: teamId,
Type: model.ChannelTypeOpen,
}, 1000)
require.NoError(t, nErr)
dmChannel1, nErr := ss.Channel().CreateDirectChannel(user, user2)
require.NoError(t, nErr)
_, err = ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{
Channels: []string{channel1.Id, dmChannel1.Id},
})
require.NoError(t, err)
// Do the same on the second team
channel2, nErr := ss.Channel().Save(&model.Channel{
Name: model.NewId(),
TeamId: team2.Id,
Type: model.ChannelTypeOpen,
}, 1000)
require.NoError(t, nErr)
_, err = ss.Channel().CreateSidebarCategory(userId, team2.Id, &model.SidebarCategoryWithChannels{
Channels: []string{channel2.Id, dmChannel1.Id},
})
require.NoError(t, err)
// Confirm that we start with the right number of categories and SidebarChannels entries
var count int64
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarCategories WHERE UserId = ?", userId)
require.NoError(t, err)
require.Equal(t, int64(8), count)
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarChannels WHERE UserId = ?", userId)
require.NoError(t, err)
require.Equal(t, int64(4), count)
// Leave the first team
err = ss.Channel().ClearSidebarOnTeamLeave(userId, teamId)
assert.NoError(t, err)
// Confirm that we have the correct number of categories and SidebarChannels entries left over
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarCategories WHERE UserId = ?", userId)
require.NoError(t, err)
assert.Equal(t, int64(4), count)
err = s.GetMasterX().Get(&count, "SELECT COUNT(*) FROM SidebarChannels WHERE UserId = ?", userId)
require.NoError(t, err)
assert.Equal(t, int64(2), count)
// Confirm that the categories on the second team are unchanged
res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team2.Id)
require.NoError(t, err)
assert.Len(t, res.Categories, 4)
assert.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type)
assert.Equal(t, []string{channel2.Id, dmChannel1.Id}, res.Categories[1].Channels)
})
}
func testDeleteSidebarCategory(t *testing.T, ss store.Store, s SqlStore) {
t.Run("should correctly remove an empty category", func(t *testing.T) {
userId, teamId := setupInitialSidebarCategories(t, ss)
defer ss.User().PermanentDelete(userId)
newCategory, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{})
require.NoError(t, err)
require.NotNil(t, newCategory)
// Ensure that the category was created properly
res, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId)
require.NoError(t, err)
require.Len(t, res.Categories, 4)
// Then delete it and confirm that was done correctly
err = ss.Channel().DeleteSidebarCategory(newCategory.Id)
assert.NoError(t, err)
res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId)
require.NoError(t, err)
require.Len(t, res.Categories, 3)
})
t.Run("should correctly remove a category and its channels", func(t *testing.T) {
userId, teamId := setupInitialSidebarCategories(t, ss)
defer ss.User().PermanentDelete(userId)
user := &model.User{
Id: userId,
}
// Create some channels
channel1, nErr := ss.Channel().Save(&model.Channel{
Name: model.NewId(),
TeamId: teamId,
Type: model.ChannelTypeOpen,
}, 1000)
require.NoError(t, nErr)
defer ss.Channel().PermanentDelete(channel1.Id)
channel2, nErr := ss.Channel().Save(&model.Channel{
Name: model.NewId(),
TeamId: teamId,
Type: model.ChannelTypePrivate,
}, 1000)
require.NoError(t, nErr)
defer ss.Channel().PermanentDelete(channel2.Id)
dmChannel1, nErr := ss.Channel().CreateDirectChannel(user, &model.User{
Id: model.NewId(),
})
require.NoError(t, nErr)
defer ss.Channel().PermanentDelete(dmChannel1.Id)
// Assign some of those channels to a custom category
newCategory, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{
Channels: []string{channel1.Id, channel2.Id, dmChannel1.Id},
})
require.NoError(t, err)
require.NotNil(t, newCategory)
// Ensure that the categories are set up correctly
res, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId)
require.NoError(t, err)
require.Len(t, res.Categories, 4)
require.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type)
require.Equal(t, []string{channel1.Id, channel2.Id, dmChannel1.Id}, res.Categories[1].Channels)
// Actually delete the channel
err = ss.Channel().DeleteSidebarCategory(newCategory.Id)
assert.NoError(t, err)
// Confirm that the category was deleted...
res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId)
assert.NoError(t, err)
assert.Len(t, res.Categories, 3)
// ...and that the corresponding SidebarChannel entries were deleted
var count int64
countErr := s.GetMasterX().Get(&count, `
SELECT
COUNT(*)
FROM
SidebarChannels
WHERE
CategoryId = ?`, newCategory.Id)
require.NoError(t, countErr)
assert.Equal(t, int64(0), count)
})
t.Run("should not allow you to remove non-custom categories", func(t *testing.T) {
userId, teamId := setupInitialSidebarCategories(t, ss)
defer ss.User().PermanentDelete(userId)
res, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userId, teamId)
require.NoError(t, err)
require.Len(t, res.Categories, 3)
require.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type)
require.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type)
require.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type)
err = ss.Channel().DeleteSidebarCategory(res.Categories[0].Id)
assert.Error(t, err)
err = ss.Channel().DeleteSidebarCategory(res.Categories[1].Id)
assert.Error(t, err)
err = ss.Channel().DeleteSidebarCategory(res.Categories[2].Id)
assert.Error(t, err)
})
}
func testUpdateSidebarChannelsByPreferences(t *testing.T, ss store.Store) {
t.Run("Should be able to update sidebar channels", func(t *testing.T) {
userId := model.NewId()
teamId := model.NewId()
opts := &store.SidebarCategorySearchOpts{
TeamID: teamId,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
require.NoError(t, nErr)
require.NotEmpty(t, res)
channel, nErr := ss.Channel().Save(&model.Channel{
Name: "channel",
Type: model.ChannelTypeOpen,
TeamId: teamId,
}, 10)
require.NoError(t, nErr)
err := ss.Channel().UpdateSidebarChannelsByPreferences(model.Preferences{
model.Preference{
Name: channel.Id,
Category: model.PreferenceCategoryFavoriteChannel,
Value: "true",
},
})
assert.NoError(t, err)
})
t.Run("Should not panic if channel is not found", func(t *testing.T) {
userId := model.NewId()
teamId := model.NewId()
opts := &store.SidebarCategorySearchOpts{
TeamID: teamId,
ExcludeTeam: false,
}
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
assert.NoError(t, nErr)
require.NotEmpty(t, res)
require.NotPanics(t, func() {
_ = ss.Channel().UpdateSidebarChannelsByPreferences(model.Preferences{
model.Preference{
Name: "fakeid",
Category: model.PreferenceCategoryFavoriteChannel,
Value: "true",
},
})
})
})
}
// testSidebarCategoryDeadlock tries to delete and update a category at the same time
// in the hope of triggering a deadlock. This is a best-effort test case, and is not guaranteed
// to catch a bug.
func testSidebarCategoryDeadlock(t *testing.T, ss store.Store) {
userID := model.NewId()
team := setupTeam(t, ss, userID)
// Join a channel
channel, err := ss.Channel().Save(&model.Channel{
Name: "channel",
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}, 10)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
UserId: userID,
ChannelId: channel.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
// And then create the initial categories so that it includes the channel
opts := &store.SidebarCategorySearchOpts{
TeamID: team.Id,
ExcludeTeam: false,
}
res, err := ss.Channel().CreateInitialSidebarCategories(userID, opts)
require.NoError(t, err)
require.NotEmpty(t, res)
initialCategories, err := ss.Channel().GetSidebarCategoriesForTeamForUser(userID, team.Id)
require.NoError(t, err)
channelsCategory := initialCategories.Categories[1]
require.Equal(t, []string{channel.Id}, channelsCategory.Channels)
customCategory, err := ss.Channel().CreateSidebarCategory(userID, team.Id, &model.SidebarCategoryWithChannels{})
require.NoError(t, err)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
_, _, err := ss.Channel().UpdateSidebarCategories(userID, team.Id, []*model.SidebarCategoryWithChannels{
{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: []string{},
},
{
SidebarCategory: customCategory.SidebarCategory,
Channels: []string{channel.Id},
},
})
if err != nil {
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
}
}()
go func() {
defer wg.Done()
err := ss.Channel().DeleteSidebarCategory(customCategory.Id)
require.NoError(t, err)
}()
wg.Wait()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestClusterDiscoveryStore(t *testing.T, ss store.Store) {
t.Run("", func(t *testing.T) { testClusterDiscoveryStore(t, ss) })
t.Run("Delete", func(t *testing.T) { testClusterDiscoveryStoreDelete(t, ss) })
t.Run("LastPing", func(t *testing.T) { testClusterDiscoveryStoreLastPing(t, ss) })
t.Run("Exists", func(t *testing.T) { testClusterDiscoveryStoreExists(t, ss) })
t.Run("ClusterDiscoveryGetStore", func(t *testing.T) { testClusterDiscoveryGetStore(t, ss) })
}
func testClusterDiscoveryStore(t *testing.T, ss store.Store) {
discovery := &model.ClusterDiscovery{
ClusterName: "cluster_name",
Hostname: "hostname" + model.NewId(),
Type: "test_test",
}
err := ss.ClusterDiscovery().Save(discovery)
require.NoError(t, err)
err = ss.ClusterDiscovery().Cleanup()
require.NoError(t, err)
}
func testClusterDiscoveryStoreDelete(t *testing.T, ss store.Store) {
discovery := &model.ClusterDiscovery{
ClusterName: "cluster_name",
Hostname: "hostname" + model.NewId(),
Type: "test_test",
}
err := ss.ClusterDiscovery().Save(discovery)
require.NoError(t, err)
_, err = ss.ClusterDiscovery().Delete(discovery)
require.NoError(t, err)
}
func testClusterDiscoveryStoreLastPing(t *testing.T, ss store.Store) {
discovery := &model.ClusterDiscovery{
ClusterName: "cluster_name_lastPing",
Hostname: "hostname" + model.NewId(),
Type: "test_test_lastPing" + model.NewId(),
}
err := ss.ClusterDiscovery().Save(discovery)
require.NoError(t, err)
err = ss.ClusterDiscovery().SetLastPingAt(discovery)
require.NoError(t, err)
ttime := model.GetMillis()
time.Sleep(1 * time.Second)
err = ss.ClusterDiscovery().SetLastPingAt(discovery)
require.NoError(t, err)
list, err := ss.ClusterDiscovery().GetAll(discovery.Type, "cluster_name_lastPing")
require.NoError(t, err)
assert.Len(t, list, 1)
require.Less(t, int64(500), list[0].LastPingAt-ttime)
discovery2 := &model.ClusterDiscovery{
ClusterName: "cluster_name_missing",
Hostname: "hostname" + model.NewId(),
Type: "test_test_missing",
}
err = ss.ClusterDiscovery().SetLastPingAt(discovery2)
require.NoError(t, err)
}
func testClusterDiscoveryStoreExists(t *testing.T, ss store.Store) {
discovery := &model.ClusterDiscovery{
ClusterName: "cluster_name_Exists",
Hostname: "hostname" + model.NewId(),
Type: "test_test_Exists" + model.NewId(),
}
err := ss.ClusterDiscovery().Save(discovery)
require.NoError(t, err)
val, err := ss.ClusterDiscovery().Exists(discovery)
require.NoError(t, err)
assert.True(t, val)
discovery.ClusterName = "cluster_name_Exists2"
val, err = ss.ClusterDiscovery().Exists(discovery)
require.NoError(t, err)
assert.False(t, val)
}
func testClusterDiscoveryGetStore(t *testing.T, ss store.Store) {
testType1 := model.NewId()
discovery1 := &model.ClusterDiscovery{
ClusterName: "cluster_name",
Hostname: "hostname1",
Type: testType1,
}
require.NoError(t, ss.ClusterDiscovery().Save(discovery1))
discovery2 := &model.ClusterDiscovery{
ClusterName: "cluster_name",
Hostname: "hostname2",
Type: testType1,
}
require.NoError(t, ss.ClusterDiscovery().Save(discovery2))
discovery3 := &model.ClusterDiscovery{
ClusterName: "cluster_name",
Hostname: "hostname3",
Type: testType1,
CreateAt: 1,
LastPingAt: 1,
}
require.NoError(t, ss.ClusterDiscovery().Save(discovery3))
testType2 := model.NewId()
discovery4 := &model.ClusterDiscovery{
ClusterName: "cluster_name",
Hostname: "hostname1",
Type: testType2,
}
require.NoError(t, ss.ClusterDiscovery().Save(discovery4))
list, err := ss.ClusterDiscovery().GetAll(testType1, "cluster_name")
require.NoError(t, err)
assert.Len(t, list, 2)
list, err = ss.ClusterDiscovery().GetAll(testType2, "cluster_name")
require.NoError(t, err)
assert.Len(t, list, 1)
list, err = ss.ClusterDiscovery().GetAll(model.NewId(), "cluster_name")
require.NoError(t, err)
assert.Empty(t, list)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestCommandStore(t *testing.T, ss store.Store) {
t.Run("Save", func(t *testing.T) { testCommandStoreSave(t, ss) })
t.Run("Get", func(t *testing.T) { testCommandStoreGet(t, ss) })
t.Run("GetByTeam", func(t *testing.T) { testCommandStoreGetByTeam(t, ss) })
t.Run("GetByTrigger", func(t *testing.T) { testCommandStoreGetByTrigger(t, ss) })
t.Run("Delete", func(t *testing.T) { testCommandStoreDelete(t, ss) })
t.Run("DeleteByTeam", func(t *testing.T) { testCommandStoreDeleteByTeam(t, ss) })
t.Run("DeleteByUser", func(t *testing.T) { testCommandStoreDeleteByUser(t, ss) })
t.Run("Update", func(t *testing.T) { testCommandStoreUpdate(t, ss) })
t.Run("CommandCount", func(t *testing.T) { testCommandCount(t, ss) })
}
func testCommandStoreSave(t *testing.T, ss store.Store) {
o1 := model.Command{}
o1.CreatorId = model.NewId()
o1.Method = model.CommandMethodPost
o1.TeamId = model.NewId()
o1.URL = "http://nowhere.com/"
o1.Trigger = "trigger"
_, nErr := ss.Command().Save(&o1)
require.NoError(t, nErr)
_, err := ss.Command().Save(&o1)
require.Error(t, err, "shouldn't be able to update from save")
}
func testCommandStoreGet(t *testing.T, ss store.Store) {
o1 := &model.Command{}
o1.CreatorId = model.NewId()
o1.Method = model.CommandMethodPost
o1.TeamId = model.NewId()
o1.URL = "http://nowhere.com/"
o1.Trigger = "trigger"
o1, nErr := ss.Command().Save(o1)
require.NoError(t, nErr)
r1, nErr := ss.Command().Get(o1.Id)
require.NoError(t, nErr)
require.Equal(t, r1.CreateAt, o1.CreateAt, "invalid returned command")
_, err := ss.Command().Get("123")
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
}
func testCommandStoreGetByTeam(t *testing.T, ss store.Store) {
o1 := &model.Command{}
o1.CreatorId = model.NewId()
o1.Method = model.CommandMethodPost
o1.TeamId = model.NewId()
o1.URL = "http://nowhere.com/"
o1.Trigger = "trigger"
o1, nErr := ss.Command().Save(o1)
require.NoError(t, nErr)
r1, nErr := ss.Command().GetByTeam(o1.TeamId)
require.NoError(t, nErr)
require.NotEmpty(t, r1, "no command returned")
require.Equal(t, r1[0].CreateAt, o1.CreateAt, "invalid returned command")
result, nErr := ss.Command().GetByTeam("123")
require.NoError(t, nErr)
require.Empty(t, result, "no commands should have returned")
}
func testCommandStoreGetByTrigger(t *testing.T, ss store.Store) {
o1 := &model.Command{}
o1.CreatorId = model.NewId()
o1.Method = model.CommandMethodPost
o1.TeamId = model.NewId()
o1.URL = "http://nowhere.com/"
o1.Trigger = "trigger1"
o2 := &model.Command{}
o2.CreatorId = model.NewId()
o2.Method = model.CommandMethodPost
o2.TeamId = model.NewId()
o2.URL = "http://nowhere.com/"
o2.Trigger = "trigger1"
o1, nErr := ss.Command().Save(o1)
require.NoError(t, nErr)
_, nErr = ss.Command().Save(o2)
require.NoError(t, nErr)
var r1 *model.Command
r1, nErr = ss.Command().GetByTrigger(o1.TeamId, o1.Trigger)
require.NoError(t, nErr)
require.Equal(t, r1.Id, o1.Id, "invalid returned command")
nErr = ss.Command().Delete(o1.Id, model.GetMillis())
require.NoError(t, nErr)
_, err := ss.Command().GetByTrigger(o1.TeamId, o1.Trigger)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
}
func testCommandStoreDelete(t *testing.T, ss store.Store) {
o1 := &model.Command{}
o1.CreatorId = model.NewId()
o1.Method = model.CommandMethodPost
o1.TeamId = model.NewId()
o1.URL = "http://nowhere.com/"
o1.Trigger = "trigger"
o1, nErr := ss.Command().Save(o1)
require.NoError(t, nErr)
r1, nErr := ss.Command().Get(o1.Id)
require.NoError(t, nErr)
require.Equal(t, r1.CreateAt, o1.CreateAt, "invalid returned command")
nErr = ss.Command().Delete(o1.Id, model.GetMillis())
require.NoError(t, nErr)
_, err := ss.Command().Get(o1.Id)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
}
func testCommandStoreDeleteByTeam(t *testing.T, ss store.Store) {
o1 := &model.Command{}
o1.CreatorId = model.NewId()
o1.Method = model.CommandMethodPost
o1.TeamId = model.NewId()
o1.URL = "http://nowhere.com/"
o1.Trigger = "trigger"
o1, nErr := ss.Command().Save(o1)
require.NoError(t, nErr)
r1, nErr := ss.Command().Get(o1.Id)
require.NoError(t, nErr)
require.Equal(t, r1.CreateAt, o1.CreateAt, "invalid returned command")
nErr = ss.Command().PermanentDeleteByTeam(o1.TeamId)
require.NoError(t, nErr)
_, err := ss.Command().Get(o1.Id)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
}
func testCommandStoreDeleteByUser(t *testing.T, ss store.Store) {
o1 := &model.Command{}
o1.CreatorId = model.NewId()
o1.Method = model.CommandMethodPost
o1.TeamId = model.NewId()
o1.URL = "http://nowhere.com/"
o1.Trigger = "trigger"
o1, nErr := ss.Command().Save(o1)
require.NoError(t, nErr)
r1, nErr := ss.Command().Get(o1.Id)
require.NoError(t, nErr)
require.Equal(t, r1.CreateAt, o1.CreateAt, "invalid returned command")
nErr = ss.Command().PermanentDeleteByUser(o1.CreatorId)
require.NoError(t, nErr)
_, err := ss.Command().Get(o1.Id)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
}
func testCommandStoreUpdate(t *testing.T, ss store.Store) {
o1 := &model.Command{}
o1.CreatorId = model.NewId()
o1.Method = model.CommandMethodPost
o1.TeamId = model.NewId()
o1.URL = "http://nowhere.com/"
o1.Trigger = "trigger"
o1, nErr := ss.Command().Save(o1)
require.NoError(t, nErr)
o1.Token = model.NewId()
_, nErr = ss.Command().Update(o1)
require.NoError(t, nErr)
o1.URL = "junk"
_, err := ss.Command().Update(o1)
require.Error(t, err)
}
func testCommandCount(t *testing.T, ss store.Store) {
o1 := &model.Command{}
o1.CreatorId = model.NewId()
o1.Method = model.CommandMethodPost
o1.TeamId = model.NewId()
o1.URL = "http://nowhere.com/"
o1.Trigger = "trigger"
o1, nErr := ss.Command().Save(o1)
require.NoError(t, nErr)
r1, nErr := ss.Command().AnalyticsCommandCount("")
require.NoError(t, nErr)
require.NotZero(t, r1, "should be at least 1 command")
r2, nErr := ss.Command().AnalyticsCommandCount(o1.TeamId)
require.NoError(t, nErr)
require.Equal(t, r2, int64(1), "should be 1 command")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestCommandWebhookStore(t *testing.T, ss store.Store) {
t.Run("", func(t *testing.T) { testCommandWebhookStore(t, ss) })
}
func testCommandWebhookStore(t *testing.T, ss store.Store) {
cws := ss.CommandWebhook()
h1 := &model.CommandWebhook{}
h1.CommandId = model.NewId()
h1.UserId = model.NewId()
h1.ChannelId = model.NewId()
h1, err := cws.Save(h1)
require.NoError(t, err)
var r1 *model.CommandWebhook
r1, nErr := cws.Get(h1.Id)
require.NoError(t, nErr)
assert.Equal(t, *r1, *h1, "invalid returned webhook")
_, nErr = cws.Get("123")
var nfErr *store.ErrNotFound
require.True(t, errors.As(nErr, &nfErr), "Should have set the status as not found for missing id")
h2 := &model.CommandWebhook{}
h2.CreateAt = model.GetMillis() - 2*model.CommandWebhookLifetime
h2.CommandId = model.NewId()
h2.UserId = model.NewId()
h2.ChannelId = model.NewId()
h2, err = cws.Save(h2)
require.NoError(t, err)
_, nErr = cws.Get(h2.Id)
require.Error(t, nErr, "Should have set the status as not found for expired webhook")
require.True(t, errors.As(nErr, &nfErr), "Should have set the status as not found for expired webhook")
cws.Cleanup()
_, nErr = cws.Get(h1.Id)
require.NoError(t, nErr, "Should have no error getting unexpired webhook")
_, nErr = cws.Get(h2.Id)
require.True(t, errors.As(nErr, &nfErr), "Should have set the status as not found for expired webhook")
nErr = cws.TryUse(h1.Id, 1)
require.NoError(t, nErr, "Should be able to use webhook once")
nErr = cws.TryUse(h1.Id, 1)
require.Error(t, nErr, "Should be able to use webhook once")
var invErr *store.ErrInvalidInput
require.True(t, errors.As(nErr, &invErr), "Should be able to use webhook once")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func cleanupStoreState(t *testing.T, ss store.Store) {
//remove existing users
allUsers, err := ss.User().GetAll()
require.NoError(t, err, "error cleaning all test users", err)
for _, u := range allUsers {
err = ss.User().PermanentDelete(u.Id)
require.NoError(t, err, "failed cleaning up test user %s", u.Username)
//remove all posts by this user
nErr := ss.Post().PermanentDeleteByUser(u.Id)
require.NoError(t, nErr, "failed cleaning all posts of test user %s", u.Username)
}
//remove existing channels
allChannels, nErr := ss.Channel().GetAllChannels(0, 100000, store.ChannelSearchOpts{IncludeDeleted: true})
require.NoError(t, nErr, "error cleaning all test channels", nErr)
for _, channel := range allChannels {
nErr = ss.Channel().PermanentDelete(channel.Id)
require.NoError(t, nErr, "failed cleaning up test channel %s", channel.Id)
}
//remove existing teams
allTeams, nErr := ss.Team().GetAll()
require.NoError(t, nErr, "error cleaning all test teams", nErr)
for _, team := range allTeams {
err := ss.Team().PermanentDelete(team.Id)
require.NoError(t, err, "failed cleaning up test team %s", team.Id)
}
}
func TestComplianceStore(t *testing.T, ss store.Store) {
t.Run("", func(t *testing.T) { testComplianceStore(t, ss) })
t.Run("ComplianceExport", func(t *testing.T) { testComplianceExport(t, ss) })
t.Run("ComplianceExportDirectMessages", func(t *testing.T) { testComplianceExportDirectMessages(t, ss) })
t.Run("MessageExportPublicChannel", func(t *testing.T) { testMessageExportPublicChannel(t, ss) })
t.Run("MessageExportPrivateChannel", func(t *testing.T) { testMessageExportPrivateChannel(t, ss) })
t.Run("MessageExportDirectMessageChannel", func(t *testing.T) { testMessageExportDirectMessageChannel(t, ss) })
t.Run("MessageExportGroupMessageChannel", func(t *testing.T) { testMessageExportGroupMessageChannel(t, ss) })
t.Run("MessageEditExportMessage", func(t *testing.T) { testEditExportMessage(t, ss) })
t.Run("MessageEditAfterExportMessage", func(t *testing.T) { testEditAfterExportMessage(t, ss) })
t.Run("MessageDeleteExportMessage", func(t *testing.T) { testDeleteExportMessage(t, ss) })
t.Run("MessageDeleteAfterExportMessage", func(t *testing.T) { testDeleteAfterExportMessage(t, ss) })
}
func testComplianceStore(t *testing.T, ss store.Store) {
compliance1 := &model.Compliance{Desc: "Audit for federal subpoena case #22443", UserId: model.NewId(), Status: model.ComplianceStatusFailed, StartAt: model.GetMillis() - 1, EndAt: model.GetMillis() + 1, Type: model.ComplianceTypeAdhoc}
_, err := ss.Compliance().Save(compliance1)
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)
compliance2 := &model.Compliance{Desc: "Audit for federal subpoena case #11458", UserId: model.NewId(), Status: model.ComplianceStatusRunning, StartAt: model.GetMillis() - 1, EndAt: model.GetMillis() + 1, Type: model.ComplianceTypeAdhoc}
_, err = ss.Compliance().Save(compliance2)
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)
compliances, _ := ss.Compliance().GetAll(0, 1000)
require.Equal(t, model.ComplianceStatusRunning, compliances[0].Status)
require.Equal(t, compliance2.Id, compliances[0].Id)
compliance2.Status = model.ComplianceStatusFailed
_, err = ss.Compliance().Update(compliance2)
require.NoError(t, err)
compliances, _ = ss.Compliance().GetAll(0, 1000)
require.Equal(t, model.ComplianceStatusFailed, compliances[0].Status)
require.Equal(t, compliance2.Id, compliances[0].Id)
compliances, _ = ss.Compliance().GetAll(0, 1)
require.Len(t, compliances, 1)
compliances, _ = ss.Compliance().GetAll(1, 1)
require.Len(t, compliances, 1)
rc2, _ := ss.Compliance().Get(compliance2.Id)
require.Equal(t, compliance2.Status, rc2.Status)
}
func testComplianceExport(t *testing.T, ss store.Store) {
time.Sleep(100 * time.Millisecond)
const (
limit = 30000
)
t1 := &model.Team{}
t1.DisplayName = "DisplayName"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
t1, err := ss.Team().Save(t1)
require.NoError(t, err)
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Username = model.NewId()
u1, err = ss.User().Save(u1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: t1.Id, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Username = model.NewId()
u2, err = ss.User().Save(u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: t1.Id, UserId: u2.Id}, -1)
require.NoError(t, nErr)
c1 := &model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel2"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, nErr = ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = u1.Id
o1.CreateAt = model.GetMillis()
o1.Message = NewTestId()
o1, nErr = ss.Post().Save(o1)
require.NoError(t, nErr)
o1a := &model.Post{}
o1a.ChannelId = c1.Id
o1a.UserId = u1.Id
o1a.CreateAt = o1.CreateAt + 10
o1a.Message = NewTestId()
_, nErr = ss.Post().Save(o1a)
require.NoError(t, nErr)
o2 := &model.Post{}
o2.ChannelId = c1.Id
o2.UserId = u1.Id
o2.CreateAt = o1.CreateAt + 20
o2.Message = NewTestId()
_, nErr = ss.Post().Save(o2)
require.NoError(t, nErr)
o2a := &model.Post{}
o2a.ChannelId = c1.Id
o2a.UserId = u2.Id
o2a.CreateAt = o1.CreateAt + 30
o2a.Message = NewTestId()
o2a, nErr = ss.Post().Save(o2a)
require.NoError(t, nErr)
time.Sleep(100 * time.Millisecond)
cr1 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o2a.CreateAt + 1}
cposts, _, nErr := ss.Compliance().ComplianceExport(cr1, model.ComplianceExportCursor{}, limit)
require.NoError(t, nErr)
assert.Len(t, cposts, 4)
assert.Equal(t, cposts[0].PostId, o1.Id)
assert.Equal(t, cposts[3].PostId, o2a.Id)
// Test limit
cposts, _, nErr = ss.Compliance().ComplianceExport(cr1, model.ComplianceExportCursor{}, 2)
require.NoError(t, nErr)
assert.Len(t, cposts, 2)
cr2 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o2a.CreateAt + 1, Emails: u2.Email}
cposts, _, nErr = ss.Compliance().ComplianceExport(cr2, model.ComplianceExportCursor{}, limit)
require.NoError(t, nErr)
assert.Len(t, cposts, 1)
assert.Equal(t, cposts[0].PostId, o2a.Id)
cr3 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o2a.CreateAt + 1, Emails: u2.Email + ", " + u1.Email}
cposts, _, nErr = ss.Compliance().ComplianceExport(cr3, model.ComplianceExportCursor{}, limit)
require.NoError(t, nErr)
assert.Len(t, cposts, 4)
assert.Equal(t, cposts[0].PostId, o1.Id)
assert.Equal(t, cposts[3].PostId, o2a.Id)
cr4 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o2a.CreateAt + 1, Keywords: o2a.Message}
cposts, _, nErr = ss.Compliance().ComplianceExport(cr4, model.ComplianceExportCursor{}, limit)
require.NoError(t, nErr)
assert.Len(t, cposts, 1)
assert.Equal(t, cposts[0].PostId, o2a.Id)
cr5 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o2a.CreateAt + 1, Keywords: o2a.Message + " " + o1.Message}
cposts, _, nErr = ss.Compliance().ComplianceExport(cr5, model.ComplianceExportCursor{}, limit)
require.NoError(t, nErr)
assert.Len(t, cposts, 2)
assert.Equal(t, cposts[0].PostId, o1.Id)
cr6 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o2a.CreateAt + 1, Emails: u2.Email + ", " + u1.Email, Keywords: o2a.Message + " " + o1.Message}
cposts, _, nErr = ss.Compliance().ComplianceExport(cr6, model.ComplianceExportCursor{}, limit)
require.NoError(t, nErr)
assert.Len(t, cposts, 2)
assert.Equal(t, cposts[0].PostId, o1.Id)
assert.Equal(t, cposts[1].PostId, o2a.Id)
t.Run("multiple batches", func(t *testing.T) {
cr7 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o2a.CreateAt + 1}
cursor := model.ComplianceExportCursor{}
cposts, cursor, nErr = ss.Compliance().ComplianceExport(cr7, cursor, 2)
require.NoError(t, nErr)
assert.Len(t, cposts, 2)
assert.Equal(t, cposts[0].PostId, o1.Id)
assert.Equal(t, cposts[1].PostId, o1a.Id)
cposts, _, nErr = ss.Compliance().ComplianceExport(cr7, cursor, 3)
require.NoError(t, nErr)
assert.Len(t, cposts, 2)
assert.Equal(t, cposts[0].PostId, o2.Id)
assert.Equal(t, cposts[1].PostId, o2a.Id)
})
}
func testComplianceExportDirectMessages(t *testing.T, ss store.Store) {
defer cleanupStoreState(t, ss)
time.Sleep(100 * time.Millisecond)
const (
limit = 30000
)
t1 := &model.Team{}
t1.DisplayName = "DisplayName"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
t1, err := ss.Team().Save(t1)
require.NoError(t, err)
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Username = model.NewId()
u1, err = ss.User().Save(u1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: t1.Id, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Username = model.NewId()
u2, err = ss.User().Save(u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: t1.Id, UserId: u2.Id}, -1)
require.NoError(t, nErr)
c1 := &model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel2"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, nErr = ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
cDM, nErr := ss.Channel().CreateDirectChannel(u1, u2)
require.NoError(t, nErr)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = u1.Id
o1.CreateAt = model.GetMillis()
o1.Message = NewTestId()
o1, nErr = ss.Post().Save(o1)
require.NoError(t, nErr)
o1a := &model.Post{}
o1a.ChannelId = c1.Id
o1a.UserId = u1.Id
o1a.CreateAt = o1.CreateAt + 10
o1a.Message = NewTestId()
_, nErr = ss.Post().Save(o1a)
require.NoError(t, nErr)
o2 := &model.Post{}
o2.ChannelId = c1.Id
o2.UserId = u1.Id
o2.CreateAt = o1.CreateAt + 20
o2.Message = NewTestId()
_, nErr = ss.Post().Save(o2)
require.NoError(t, nErr)
o2a := &model.Post{}
o2a.ChannelId = c1.Id
o2a.UserId = u2.Id
o2a.CreateAt = o1.CreateAt + 30
o2a.Message = NewTestId()
_, nErr = ss.Post().Save(o2a)
require.NoError(t, nErr)
o3 := &model.Post{}
o3.ChannelId = cDM.Id
o3.UserId = u1.Id
o3.CreateAt = o1.CreateAt + 40
o3.Message = NewTestId()
o3, nErr = ss.Post().Save(o3)
require.NoError(t, nErr)
time.Sleep(100 * time.Millisecond)
cr1 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o3.CreateAt + 1, Emails: u1.Email}
cposts, _, nErr := ss.Compliance().ComplianceExport(cr1, model.ComplianceExportCursor{}, limit)
require.NoError(t, nErr)
assert.Len(t, cposts, 4)
assert.Equal(t, cposts[0].PostId, o1.Id)
assert.Equal(t, cposts[len(cposts)-1].PostId, o3.Id)
t.Run("mix of channel and direct messages", func(t *testing.T) {
// This will "cross the boundary" between the two queries
cursor := model.ComplianceExportCursor{}
cr2 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o3.CreateAt + 1, Emails: u1.Email}
cposts, cursor, nErr = ss.Compliance().ComplianceExport(cr2, cursor, 2)
require.NoError(t, nErr)
assert.Len(t, cposts, 2)
assert.Equal(t, cposts[0].PostId, o1.Id)
assert.Equal(t, cposts[len(cposts)-1].PostId, o1a.Id)
cposts, _, nErr = ss.Compliance().ComplianceExport(cr2, cursor, 2)
require.NoError(t, nErr)
assert.Len(t, cposts, 2)
assert.Equal(t, cposts[0].PostId, o2.Id)
assert.Equal(t, cposts[len(cposts)-1].PostId, o3.Id)
// This will exhaust the first query before moving to the next one
cursor = model.ComplianceExportCursor{}
cr3 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o3.CreateAt + 1, Emails: u1.Email}
cposts, cursor, nErr = ss.Compliance().ComplianceExport(cr3, cursor, 3)
require.NoError(t, nErr)
assert.Len(t, cposts, 3)
assert.Equal(t, cposts[0].PostId, o1.Id)
assert.Equal(t, cposts[len(cposts)-1].PostId, o2.Id)
cposts, _, nErr = ss.Compliance().ComplianceExport(cr3, cursor, 2)
require.NoError(t, nErr)
assert.Len(t, cposts, 1)
assert.Equal(t, cposts[0].PostId, o3.Id)
})
t.Run("timestamp collision", func(t *testing.T) {
time.Sleep(100 * time.Millisecond)
nowMillis := model.GetMillis()
createPost := func(createAt int64) {
post := &model.Post{}
post.ChannelId = c1.Id
post.UserId = u1.Id
post.CreateAt = createAt
post.Message = NewTestId()
_, nErr = ss.Post().Save(post)
require.NoError(t, nErr)
}
for i := 0; i < 3; i++ {
createPost(nowMillis)
}
for i := 0; i < 2; i++ {
createPost(nowMillis + 1)
}
cursor := model.ComplianceExportCursor{}
cr4 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: nowMillis, EndAt: nowMillis + 2}
cposts, cursor, nErr = ss.Compliance().ComplianceExport(cr4, cursor, 2)
require.NoError(t, nErr)
assert.Len(t, cposts, 2)
cr5 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: nowMillis, EndAt: nowMillis + 2}
cposts, _, nErr = ss.Compliance().ComplianceExport(cr5, cursor, 3)
require.NoError(t, nErr)
assert.Len(t, cposts, 3)
// range should be [inclusive, exclusive)
cursor = model.ComplianceExportCursor{}
cr6 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: nowMillis, EndAt: nowMillis + 1}
cposts, _, nErr = ss.Compliance().ComplianceExport(cr6, cursor, 5)
require.NoError(t, nErr)
assert.Len(t, cposts, 3)
})
}
func testMessageExportPublicChannel(t *testing.T, ss store.Store) {
defer cleanupStoreState(t, ss)
// get the starting number of message export entries
startTime := model.GetMillis()
messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10)
require.NoError(t, err)
assert.Equal(t, 0, len(messages))
// need a team
team := &model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, err = ss.Team().Save(team)
require.NoError(t, err)
// and two users that are a part of that team
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err = ss.User().Save(user1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user1.Id,
}, -1)
require.NoError(t, nErr)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user2.Id,
}, -1)
require.NoError(t, nErr)
// need a public channel
channel := &model.Channel{
TeamId: team.Id,
Name: model.NewId(),
DisplayName: "Public Channel",
Type: model.ChannelTypeOpen,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
// user1 posts twice in the public channel
post1 := &model.Post{
ChannelId: channel.Id,
UserId: user1.Id,
CreateAt: startTime,
Message: NewTestId(),
}
post1, err = ss.Post().Save(post1)
require.NoError(t, err)
post2 := &model.Post{
ChannelId: channel.Id,
UserId: user1.Id,
CreateAt: startTime + 10,
Message: NewTestId(),
}
post2, err = ss.Post().Save(post2)
require.NoError(t, err)
// fetch the message exports for both posts that user1 sent
messageExportMap := map[string]model.MessageExport{}
messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10)
require.NoError(t, err)
assert.Equal(t, 2, len(messages))
for _, v := range messages {
messageExportMap[*v.PostId] = *v
}
// post1 was made by user1 in channel1 and team1
assert.Equal(t, post1.Id, *messageExportMap[post1.Id].PostId)
assert.Equal(t, post1.CreateAt, *messageExportMap[post1.Id].PostCreateAt)
assert.Equal(t, post1.Message, *messageExportMap[post1.Id].PostMessage)
assert.Equal(t, channel.Id, *messageExportMap[post1.Id].ChannelId)
assert.Equal(t, channel.DisplayName, *messageExportMap[post1.Id].ChannelDisplayName)
assert.Equal(t, user1.Id, *messageExportMap[post1.Id].UserId)
assert.Equal(t, user1.Email, *messageExportMap[post1.Id].UserEmail)
assert.Equal(t, user1.Username, *messageExportMap[post1.Id].Username)
// post2 was made by user1 in channel1 and team1
assert.Equal(t, post2.Id, *messageExportMap[post2.Id].PostId)
assert.Equal(t, post2.CreateAt, *messageExportMap[post2.Id].PostCreateAt)
assert.Equal(t, post2.Message, *messageExportMap[post2.Id].PostMessage)
assert.Equal(t, channel.Id, *messageExportMap[post2.Id].ChannelId)
assert.Equal(t, channel.DisplayName, *messageExportMap[post2.Id].ChannelDisplayName)
assert.Equal(t, user1.Id, *messageExportMap[post2.Id].UserId)
assert.Equal(t, user1.Email, *messageExportMap[post2.Id].UserEmail)
assert.Equal(t, user1.Username, *messageExportMap[post2.Id].Username)
}
func testMessageExportPrivateChannel(t *testing.T, ss store.Store) {
defer cleanupStoreState(t, ss)
// get the starting number of message export entries
startTime := model.GetMillis()
messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10)
require.NoError(t, err)
assert.Equal(t, 0, len(messages))
// need a team
team := &model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, err = ss.Team().Save(team)
require.NoError(t, err)
// and two users that are a part of that team
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err = ss.User().Save(user1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user1.Id,
}, -1)
require.NoError(t, nErr)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user2.Id,
}, -1)
require.NoError(t, nErr)
// need a private channel
channel := &model.Channel{
TeamId: team.Id,
Name: model.NewId(),
DisplayName: "Private Channel",
Type: model.ChannelTypePrivate,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
// user1 posts twice in the private channel
post1 := &model.Post{
ChannelId: channel.Id,
UserId: user1.Id,
CreateAt: startTime,
Message: NewTestId(),
}
post1, err = ss.Post().Save(post1)
require.NoError(t, err)
post2 := &model.Post{
ChannelId: channel.Id,
UserId: user1.Id,
CreateAt: startTime + 10,
Message: NewTestId(),
}
post2, err = ss.Post().Save(post2)
require.NoError(t, err)
// fetch the message exports for both posts that user1 sent
messageExportMap := map[string]model.MessageExport{}
messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10)
require.NoError(t, err)
assert.Equal(t, 2, len(messages))
for _, v := range messages {
messageExportMap[*v.PostId] = *v
}
// post1 was made by user1 in channel1 and team1
assert.Equal(t, post1.Id, *messageExportMap[post1.Id].PostId)
assert.Equal(t, post1.CreateAt, *messageExportMap[post1.Id].PostCreateAt)
assert.Equal(t, post1.Message, *messageExportMap[post1.Id].PostMessage)
assert.Equal(t, channel.Id, *messageExportMap[post1.Id].ChannelId)
assert.Equal(t, channel.DisplayName, *messageExportMap[post1.Id].ChannelDisplayName)
assert.Equal(t, channel.Type, *messageExportMap[post1.Id].ChannelType)
assert.Equal(t, user1.Id, *messageExportMap[post1.Id].UserId)
assert.Equal(t, user1.Email, *messageExportMap[post1.Id].UserEmail)
assert.Equal(t, user1.Username, *messageExportMap[post1.Id].Username)
// post2 was made by user1 in channel1 and team1
assert.Equal(t, post2.Id, *messageExportMap[post2.Id].PostId)
assert.Equal(t, post2.CreateAt, *messageExportMap[post2.Id].PostCreateAt)
assert.Equal(t, post2.Message, *messageExportMap[post2.Id].PostMessage)
assert.Equal(t, channel.Id, *messageExportMap[post2.Id].ChannelId)
assert.Equal(t, channel.DisplayName, *messageExportMap[post2.Id].ChannelDisplayName)
assert.Equal(t, channel.Type, *messageExportMap[post2.Id].ChannelType)
assert.Equal(t, user1.Id, *messageExportMap[post2.Id].UserId)
assert.Equal(t, user1.Email, *messageExportMap[post2.Id].UserEmail)
assert.Equal(t, user1.Username, *messageExportMap[post2.Id].Username)
}
func testMessageExportDirectMessageChannel(t *testing.T, ss store.Store) {
defer cleanupStoreState(t, ss)
// get the starting number of message export entries
startTime := model.GetMillis()
messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10)
require.NoError(t, err)
assert.Equal(t, 0, len(messages))
// need a team
team := &model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, err = ss.Team().Save(team)
require.NoError(t, err)
// and two users that are a part of that team
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err = ss.User().Save(user1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user1.Id,
}, -1)
require.NoError(t, nErr)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user2.Id,
}, -1)
require.NoError(t, nErr)
// as well as a DM channel between those users
directMessageChannel, nErr := ss.Channel().CreateDirectChannel(user1, user2)
require.NoError(t, nErr)
// user1 also sends a DM to user2
post := &model.Post{
ChannelId: directMessageChannel.Id,
UserId: user1.Id,
CreateAt: startTime + 20,
Message: NewTestId(),
}
post, err = ss.Post().Save(post)
require.NoError(t, err)
// fetch the message export for the post that user1 sent
messageExportMap := map[string]model.MessageExport{}
messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10)
require.NoError(t, err)
assert.Equal(t, 1, len(messages))
for _, v := range messages {
messageExportMap[*v.PostId] = *v
}
// post is a DM between user1 and user2
// there is no channel display name for direct messages, so we sub in the string "Direct Message" instead
assert.Equal(t, post.Id, *messageExportMap[post.Id].PostId)
assert.Equal(t, post.CreateAt, *messageExportMap[post.Id].PostCreateAt)
assert.Equal(t, post.Message, *messageExportMap[post.Id].PostMessage)
assert.Equal(t, directMessageChannel.Id, *messageExportMap[post.Id].ChannelId)
assert.Equal(t, "Direct Message", *messageExportMap[post.Id].ChannelDisplayName)
assert.Equal(t, user1.Id, *messageExportMap[post.Id].UserId)
assert.Equal(t, user1.Email, *messageExportMap[post.Id].UserEmail)
assert.Equal(t, user1.Username, *messageExportMap[post.Id].Username)
}
func testMessageExportGroupMessageChannel(t *testing.T, ss store.Store) {
defer cleanupStoreState(t, ss)
// get the starting number of message export entries
startTime := model.GetMillis()
messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10)
require.NoError(t, err)
assert.Equal(t, 0, len(messages))
// need a team
team := &model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, err = ss.Team().Save(team)
require.NoError(t, err)
// and three users that are a part of that team
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err = ss.User().Save(user1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user1.Id,
}, -1)
require.NoError(t, nErr)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user2.Id,
}, -1)
require.NoError(t, nErr)
user3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, err = ss.User().Save(user3)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user3.Id,
}, -1)
require.NoError(t, nErr)
// can't create a group channel directly, because importing app creates an import cycle, so we have to fake it
groupMessageChannel := &model.Channel{
TeamId: team.Id,
Name: model.NewId(),
Type: model.ChannelTypeGroup,
}
groupMessageChannel, nErr = ss.Channel().Save(groupMessageChannel, -1)
require.NoError(t, nErr)
// user1 posts in the GM
post := &model.Post{
ChannelId: groupMessageChannel.Id,
UserId: user1.Id,
CreateAt: startTime + 20,
Message: NewTestId(),
}
post, err = ss.Post().Save(post)
require.NoError(t, err)
// fetch the message export for the post that user1 sent
messageExportMap := map[string]model.MessageExport{}
messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10)
require.NoError(t, err)
assert.Equal(t, 1, len(messages))
for _, v := range messages {
messageExportMap[*v.PostId] = *v
}
// post is a DM between user1 and user2
// there is no channel display name for direct messages, so we sub in the string "Direct Message" instead
assert.Equal(t, post.Id, *messageExportMap[post.Id].PostId)
assert.Equal(t, post.CreateAt, *messageExportMap[post.Id].PostCreateAt)
assert.Equal(t, post.Message, *messageExportMap[post.Id].PostMessage)
assert.Equal(t, groupMessageChannel.Id, *messageExportMap[post.Id].ChannelId)
assert.Equal(t, "Group Message", *messageExportMap[post.Id].ChannelDisplayName)
assert.Equal(t, user1.Id, *messageExportMap[post.Id].UserId)
assert.Equal(t, user1.Email, *messageExportMap[post.Id].UserEmail)
assert.Equal(t, user1.Username, *messageExportMap[post.Id].Username)
}
// post,edit,export
func testEditExportMessage(t *testing.T, ss store.Store) {
defer cleanupStoreState(t, ss)
// get the starting number of message export entries
startTime := model.GetMillis()
messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10)
require.NoError(t, err)
assert.Equal(t, 0, len(messages))
// need a team
team := &model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, err = ss.Team().Save(team)
require.NoError(t, err)
// need a user part of that team
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err = ss.User().Save(user1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user1.Id,
}, -1)
require.NoError(t, nErr)
// need a public channel
channel := &model.Channel{
TeamId: team.Id,
Name: model.NewId(),
DisplayName: "Public Channel",
Type: model.ChannelTypeOpen,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
// user1 posts in the public channel
post1 := &model.Post{
ChannelId: channel.Id,
UserId: user1.Id,
CreateAt: startTime,
Message: NewTestId(),
}
post1, err = ss.Post().Save(post1)
require.NoError(t, err)
//user 1 edits the previous post
post1e := post1.Clone()
post1e.Message = "edit " + post1.Message
post1e, err = ss.Post().Update(post1e, post1)
require.NoError(t, err)
// fetch the message exports from the start
messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10)
require.NoError(t, err)
assert.Equal(t, 2, len(messages))
for _, v := range messages {
if *v.PostDeleteAt > 0 {
// post1 was made by user1 in channel1 and team1
assert.Equal(t, post1.Id, *v.PostId)
assert.Equal(t, post1.OriginalId, *v.PostOriginalId)
assert.Equal(t, post1.CreateAt, *v.PostCreateAt)
assert.Equal(t, post1.UpdateAt, *v.PostUpdateAt)
assert.Equal(t, post1.Message, *v.PostMessage)
assert.Equal(t, channel.Id, *v.ChannelId)
assert.Equal(t, channel.DisplayName, *v.ChannelDisplayName)
assert.Equal(t, user1.Id, *v.UserId)
assert.Equal(t, user1.Email, *v.UserEmail)
assert.Equal(t, user1.Username, *v.Username)
} else {
// post1e was made by user1 in channel1 and team1
assert.Equal(t, post1e.Id, *v.PostId)
assert.Equal(t, post1e.CreateAt, *v.PostCreateAt)
assert.Equal(t, post1e.UpdateAt, *v.PostUpdateAt)
assert.Equal(t, post1e.Message, *v.PostMessage)
assert.Equal(t, channel.Id, *v.ChannelId)
assert.Equal(t, channel.DisplayName, *v.ChannelDisplayName)
assert.Equal(t, user1.Id, *v.UserId)
assert.Equal(t, user1.Email, *v.UserEmail)
assert.Equal(t, user1.Username, *v.Username)
}
}
}
// post, export, edit, export
func testEditAfterExportMessage(t *testing.T, ss store.Store) {
defer cleanupStoreState(t, ss)
// get the starting number of message export entries
startTime := model.GetMillis()
messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10)
require.NoError(t, err)
assert.Equal(t, 0, len(messages))
// need a team
team := &model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, err = ss.Team().Save(team)
require.NoError(t, err)
// need a user part of that team
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err = ss.User().Save(user1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user1.Id,
}, -1)
require.NoError(t, nErr)
// need a public channel
channel := &model.Channel{
TeamId: team.Id,
Name: model.NewId(),
DisplayName: "Public Channel",
Type: model.ChannelTypeOpen,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
// user1 posts in the public channel
post1 := &model.Post{
ChannelId: channel.Id,
UserId: user1.Id,
CreateAt: startTime,
Message: NewTestId(),
}
post1, err = ss.Post().Save(post1)
require.NoError(t, err)
// fetch the message exports from the start
messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10)
require.NoError(t, err)
assert.Equal(t, 1, len(messages))
v := messages[0]
// post1 was made by user1 in channel1 and team1
assert.Equal(t, post1.Id, *v.PostId)
assert.Equal(t, post1.OriginalId, *v.PostOriginalId)
assert.Equal(t, post1.CreateAt, *v.PostCreateAt)
assert.Equal(t, post1.UpdateAt, *v.PostUpdateAt)
assert.Equal(t, post1.Message, *v.PostMessage)
assert.Equal(t, channel.Id, *v.ChannelId)
assert.Equal(t, channel.DisplayName, *v.ChannelDisplayName)
assert.Equal(t, user1.Id, *v.UserId)
assert.Equal(t, user1.Email, *v.UserEmail)
assert.Equal(t, user1.Username, *v.Username)
postEditTime := post1.UpdateAt + 1
//user 1 edits the previous post
post1e := post1.Clone()
post1e.EditAt = postEditTime
post1e.Message = "edit " + post1.Message
post1e, err = ss.Post().Update(post1e, post1)
require.NoError(t, err)
// fetch the message exports after edit
messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: postEditTime - 1}, 10)
require.NoError(t, err)
assert.Equal(t, 2, len(messages))
for _, v := range messages {
if *v.PostDeleteAt > 0 {
// post1 was made by user1 in channel1 and team1
assert.Equal(t, post1.Id, *v.PostId)
assert.Equal(t, post1.OriginalId, *v.PostOriginalId)
assert.Equal(t, post1.CreateAt, *v.PostCreateAt)
assert.Equal(t, post1.UpdateAt, *v.PostUpdateAt)
assert.Equal(t, post1.Message, *v.PostMessage)
assert.Equal(t, channel.Id, *v.ChannelId)
assert.Equal(t, channel.DisplayName, *v.ChannelDisplayName)
assert.Equal(t, user1.Id, *v.UserId)
assert.Equal(t, user1.Email, *v.UserEmail)
assert.Equal(t, user1.Username, *v.Username)
} else {
// post1e was made by user1 in channel1 and team1
assert.Equal(t, post1e.Id, *v.PostId)
assert.Equal(t, post1e.CreateAt, *v.PostCreateAt)
assert.Equal(t, post1e.UpdateAt, *v.PostUpdateAt)
assert.Equal(t, post1e.Message, *v.PostMessage)
assert.Equal(t, channel.Id, *v.ChannelId)
assert.Equal(t, channel.DisplayName, *v.ChannelDisplayName)
assert.Equal(t, user1.Id, *v.UserId)
assert.Equal(t, user1.Email, *v.UserEmail)
assert.Equal(t, user1.Username, *v.Username)
}
}
}
// post, delete, export
func testDeleteExportMessage(t *testing.T, ss store.Store) {
defer cleanupStoreState(t, ss)
// get the starting number of message export entries
startTime := model.GetMillis()
messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10)
require.NoError(t, err)
assert.Equal(t, 0, len(messages))
// need a team
team := &model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, err = ss.Team().Save(team)
require.NoError(t, err)
// need a user part of that team
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err = ss.User().Save(user1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user1.Id,
}, -1)
require.NoError(t, nErr)
// need a public channel
channel := &model.Channel{
TeamId: team.Id,
Name: model.NewId(),
DisplayName: "Public Channel",
Type: model.ChannelTypeOpen,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
// user1 posts in the public channel
post1 := &model.Post{
ChannelId: channel.Id,
UserId: user1.Id,
CreateAt: startTime,
Message: NewTestId(),
}
post1, err = ss.Post().Save(post1)
require.NoError(t, err)
//user 1 deletes the previous post
postDeleteTime := post1.UpdateAt + 1
err = ss.Post().Delete(post1.Id, postDeleteTime, user1.Id)
require.NoError(t, err)
// fetch the message exports from the start
messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10)
require.NoError(t, err)
assert.Equal(t, 1, len(messages))
v := messages[0]
// post1 was made and deleted by user1 in channel1 and team1
assert.Equal(t, post1.Id, *v.PostId)
assert.Equal(t, post1.OriginalId, *v.PostOriginalId)
assert.Equal(t, post1.CreateAt, *v.PostCreateAt)
assert.Equal(t, postDeleteTime, *v.PostUpdateAt)
assert.NotNil(t, v.PostProps)
props := map[string]any{}
e := json.Unmarshal([]byte(*v.PostProps), &props)
require.NoError(t, e)
_, ok := props[model.PostPropsDeleteBy]
assert.True(t, ok)
assert.Equal(t, post1.Message, *v.PostMessage)
assert.Equal(t, channel.Id, *v.ChannelId)
assert.Equal(t, channel.DisplayName, *v.ChannelDisplayName)
assert.Equal(t, user1.Id, *v.UserId)
assert.Equal(t, user1.Email, *v.UserEmail)
assert.Equal(t, user1.Username, *v.Username)
}
// post,export,delete,export
func testDeleteAfterExportMessage(t *testing.T, ss store.Store) {
defer cleanupStoreState(t, ss)
// get the starting number of message export entries
startTime := model.GetMillis()
messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10)
require.NoError(t, err)
assert.Equal(t, 0, len(messages))
// need a team
team := &model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, err = ss.Team().Save(team)
require.NoError(t, err)
// need a user part of that team
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err = ss.User().Save(user1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user1.Id,
}, -1)
require.NoError(t, nErr)
// need a public channel
channel := &model.Channel{
TeamId: team.Id,
Name: model.NewId(),
DisplayName: "Public Channel",
Type: model.ChannelTypeOpen,
}
channel, nErr = ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
// user1 posts in the public channel
post1 := &model.Post{
ChannelId: channel.Id,
UserId: user1.Id,
CreateAt: startTime,
Message: NewTestId(),
}
post1, err = ss.Post().Save(post1)
require.NoError(t, err)
// fetch the message exports from the start
messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10)
require.NoError(t, err)
assert.Equal(t, 1, len(messages))
v := messages[0]
// post1 was created by user1 in channel1 and team1
assert.Equal(t, post1.Id, *v.PostId)
assert.Equal(t, post1.OriginalId, *v.PostOriginalId)
assert.Equal(t, post1.CreateAt, *v.PostCreateAt)
assert.Equal(t, post1.UpdateAt, *v.PostUpdateAt)
assert.Equal(t, post1.Message, *v.PostMessage)
assert.Equal(t, channel.Id, *v.ChannelId)
assert.Equal(t, channel.DisplayName, *v.ChannelDisplayName)
assert.Equal(t, user1.Id, *v.UserId)
assert.Equal(t, user1.Email, *v.UserEmail)
assert.Equal(t, user1.Username, *v.Username)
//user 1 deletes the previous post
postDeleteTime := post1.UpdateAt + 1
err = ss.Post().Delete(post1.Id, postDeleteTime, user1.Id)
require.NoError(t, err)
// fetch the message exports after delete
messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: postDeleteTime - 1}, 10)
require.NoError(t, err)
assert.Equal(t, 1, len(messages))
v = messages[0]
// post1 was created and deleted by user1 in channel1 and team1
assert.Equal(t, post1.Id, *v.PostId)
assert.Equal(t, post1.OriginalId, *v.PostOriginalId)
assert.Equal(t, post1.CreateAt, *v.PostCreateAt)
assert.Equal(t, postDeleteTime, *v.PostUpdateAt)
assert.NotNil(t, v.PostProps)
props := map[string]any{}
e := json.Unmarshal([]byte(*v.PostProps), &props)
require.NoError(t, e)
_, ok := props[model.PostPropsDeleteBy]
assert.True(t, ok)
assert.Equal(t, post1.Message, *v.PostMessage)
assert.Equal(t, channel.Id, *v.ChannelId)
assert.Equal(t, channel.DisplayName, *v.ChannelDisplayName)
assert.Equal(t, user1.Id, *v.UserId)
assert.Equal(t, user1.Email, *v.UserEmail)
assert.Equal(t, user1.Username, *v.Username)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestDraftStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("SaveDraft", func(t *testing.T) { testSaveDraft(t, ss) })
t.Run("UpdateDraft", func(t *testing.T) { testUpdateDraft(t, ss) })
t.Run("DeleteDraft", func(t *testing.T) { testDeleteDraft(t, ss) })
t.Run("GetDraft", func(t *testing.T) { testGetDraft(t, ss) })
t.Run("GetDraftsForUser", func(t *testing.T) { testGetDraftsForUser(t, ss) })
}
func testSaveDraft(t *testing.T, ss store.Store) {
user := &model.User{
Id: model.NewId(),
}
channel := &model.Channel{
Id: model.NewId(),
}
channel2 := &model.Channel{
Id: model.NewId(),
}
member1 := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
member2 := &model.ChannelMember{
ChannelId: channel2.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(member1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(member2)
require.NoError(t, err)
draft1 := &model.Draft{
CreateAt: 00001,
UpdateAt: 00001,
UserId: user.Id,
ChannelId: channel.Id,
Message: "draft1",
}
draft2 := &model.Draft{
CreateAt: 00005,
UpdateAt: 00005,
UserId: user.Id,
ChannelId: channel2.Id,
Message: "draft2",
}
t.Run("save drafts", func(t *testing.T) {
draftResp, err := ss.Draft().Save(draft1)
assert.NoError(t, err)
assert.Equal(t, draft1.Message, draftResp.Message)
assert.Equal(t, draft1.ChannelId, draftResp.ChannelId)
draftResp, err = ss.Draft().Save(draft2)
assert.NoError(t, err)
assert.Equal(t, draft2.Message, draftResp.Message)
assert.Equal(t, draft2.ChannelId, draftResp.ChannelId)
})
}
func testUpdateDraft(t *testing.T, ss store.Store) {
user := &model.User{
Id: model.NewId(),
}
channel := &model.Channel{
Id: model.NewId(),
}
channel2 := &model.Channel{
Id: model.NewId(),
}
member1 := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
member2 := &model.ChannelMember{
ChannelId: channel2.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(member1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(member2)
require.NoError(t, err)
draft1 := &model.Draft{
CreateAt: 00001,
UpdateAt: 00001,
UserId: user.Id,
ChannelId: channel.Id,
Message: "draft1",
}
draft2 := &model.Draft{
CreateAt: 00005,
UpdateAt: 00005,
UserId: user.Id,
ChannelId: channel2.Id,
Message: "draft2",
}
t.Run("update drafts", func(t *testing.T) {
draftResp, err := ss.Draft().Update(draft1)
assert.NoError(t, err)
assert.Equal(t, draft1.Message, draftResp.Message)
assert.Equal(t, draft1.ChannelId, draftResp.ChannelId)
draftResp, err = ss.Draft().Update(draft2)
assert.NoError(t, err)
assert.Equal(t, draft2.Message, draftResp.Message)
assert.Equal(t, draft2.ChannelId, draftResp.ChannelId)
})
}
func testDeleteDraft(t *testing.T, ss store.Store) {
user := &model.User{
Id: model.NewId(),
}
channel := &model.Channel{
Id: model.NewId(),
}
channel2 := &model.Channel{
Id: model.NewId(),
}
member1 := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
member2 := &model.ChannelMember{
ChannelId: channel2.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(member1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(member2)
require.NoError(t, err)
draft1 := &model.Draft{
CreateAt: 00001,
UpdateAt: 00001,
UserId: user.Id,
ChannelId: channel.Id,
Message: "draft1",
}
draft2 := &model.Draft{
CreateAt: 00005,
UpdateAt: 00005,
UserId: user.Id,
ChannelId: channel2.Id,
Message: "draft2",
}
_, err = ss.Draft().Save(draft1)
require.NoError(t, err)
_, err = ss.Draft().Save(draft2)
require.NoError(t, err)
t.Run("delete drafts", func(t *testing.T) {
err := ss.Draft().Delete(user.Id, channel.Id, "")
assert.NoError(t, err)
err = ss.Draft().Delete(user.Id, channel2.Id, "")
assert.NoError(t, err)
_, err = ss.Draft().Get(user.Id, channel.Id, "", false)
require.Error(t, err)
assert.IsType(t, &store.ErrNotFound{}, err)
_, err = ss.Draft().Get(user.Id, channel2.Id, "", false)
assert.Error(t, err)
assert.IsType(t, &store.ErrNotFound{}, err)
})
}
func testGetDraft(t *testing.T, ss store.Store) {
user := &model.User{
Id: model.NewId(),
}
channel := &model.Channel{
Id: model.NewId(),
}
channel2 := &model.Channel{
Id: model.NewId(),
}
member1 := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
member2 := &model.ChannelMember{
ChannelId: channel2.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(member1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(member2)
require.NoError(t, err)
draft1 := &model.Draft{
CreateAt: 00001,
UpdateAt: 00001,
UserId: user.Id,
ChannelId: channel.Id,
Message: "draft1",
}
draft2 := &model.Draft{
CreateAt: 00005,
UpdateAt: 00005,
UserId: user.Id,
ChannelId: channel2.Id,
Message: "draft2",
}
_, err = ss.Draft().Save(draft1)
require.NoError(t, err)
_, err = ss.Draft().Save(draft2)
require.NoError(t, err)
t.Run("get drafts", func(t *testing.T) {
draftResp, err := ss.Draft().Get(user.Id, channel.Id, "", false)
assert.NoError(t, err)
assert.Equal(t, draft1.Message, draftResp.Message)
assert.Equal(t, draft1.ChannelId, draftResp.ChannelId)
draftResp, err = ss.Draft().Get(user.Id, channel2.Id, "", false)
assert.NoError(t, err)
assert.Equal(t, draft2.Message, draftResp.Message)
assert.Equal(t, draft2.ChannelId, draftResp.ChannelId)
})
t.Run("get draft including deleted", func(t *testing.T) {
draftResp, err := ss.Draft().Get(user.Id, channel.Id, "", false)
assert.NoError(t, err)
assert.Equal(t, draft1.Message, draftResp.Message)
assert.Equal(t, draft1.ChannelId, draftResp.ChannelId)
err = ss.Draft().Delete(user.Id, channel.Id, "")
assert.NoError(t, err)
_, err = ss.Draft().Get(user.Id, channel.Id, "", false)
assert.Error(t, err)
assert.IsType(t, &store.ErrNotFound{}, err)
draftResp, err = ss.Draft().Get(user.Id, channel.Id, "", true)
assert.NoError(t, err)
assert.Equal(t, draft1.Message, draftResp.Message)
assert.Equal(t, draft1.ChannelId, draftResp.ChannelId)
})
}
func testGetDraftsForUser(t *testing.T, ss store.Store) {
user := &model.User{
Id: model.NewId(),
}
channel := &model.Channel{
Id: model.NewId(),
}
channel2 := &model.Channel{
Id: model.NewId(),
}
member1 := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
member2 := &model.ChannelMember{
ChannelId: channel2.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err := ss.Channel().SaveMember(member1)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(member2)
require.NoError(t, err)
draft1 := &model.Draft{
CreateAt: 00001,
UpdateAt: 00001,
UserId: user.Id,
ChannelId: channel.Id,
Message: "draft1",
}
draft2 := &model.Draft{
CreateAt: 00005,
UpdateAt: 00005,
UserId: user.Id,
ChannelId: channel2.Id,
Message: "draft2",
}
_, err = ss.Draft().Save(draft1)
require.NoError(t, err)
_, err = ss.Draft().Save(draft2)
require.NoError(t, err)
t.Run("get drafts", func(t *testing.T) {
draftResp, err := ss.Draft().GetDraftsForUser(user.Id, "")
assert.NoError(t, err)
assert.Equal(t, draft2.Message, draftResp[0].Message)
assert.Equal(t, draft2.ChannelId, draftResp[0].ChannelId)
assert.Equal(t, draft1.Message, draftResp[1].Message)
assert.Equal(t, draft1.ChannelId, draftResp[1].ChannelId)
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"testing"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEmojiStore(t *testing.T, ss store.Store) {
t.Run("EmojiSaveDelete", func(t *testing.T) { testEmojiSaveDelete(t, ss) })
t.Run("EmojiGet", func(t *testing.T) { testEmojiGet(t, ss) })
t.Run("EmojiGetByName", func(t *testing.T) { testEmojiGetByName(t, ss) })
t.Run("EmojiGetMultipleByName", func(t *testing.T) { testEmojiGetMultipleByName(t, ss) })
t.Run("EmojiGetList", func(t *testing.T) { testEmojiGetList(t, ss) })
t.Run("EmojiSearch", func(t *testing.T) { testEmojiSearch(t, ss) })
}
func testEmojiSaveDelete(t *testing.T, ss store.Store) {
emoji1 := &model.Emoji{
CreatorId: model.NewId(),
Name: model.NewId(),
}
_, err := ss.Emoji().Save(emoji1)
require.NoError(t, err)
assert.Len(t, emoji1.Id, 26, "should've set id for emoji")
emoji2 := model.Emoji{
CreatorId: model.NewId(),
Name: emoji1.Name,
}
_, err = ss.Emoji().Save(&emoji2)
require.Error(t, err, "shouldn't be able to save emoji with duplicate name")
err = ss.Emoji().Delete(emoji1, time.Now().Unix())
require.NoError(t, err)
_, err = ss.Emoji().Save(&emoji2)
require.NoError(t, err, "should be able to save emoji with duplicate name now that original has been deleted")
err = ss.Emoji().Delete(&emoji2, time.Now().Unix()+1)
require.NoError(t, err)
}
func testEmojiGet(t *testing.T, ss store.Store) {
emojis := []model.Emoji{
{
CreatorId: model.NewId(),
Name: model.NewId(),
},
{
CreatorId: model.NewId(),
Name: model.NewId(),
},
{
CreatorId: model.NewId(),
Name: model.NewId(),
},
}
for i, emoji := range emojis {
data, err := ss.Emoji().Save(&emoji)
require.NoError(t, err)
emojis[i] = *data
}
defer func() {
for _, emoji := range emojis {
err := ss.Emoji().Delete(&emoji, time.Now().Unix())
require.NoError(t, err)
}
}()
for _, emoji := range emojis {
_, err := ss.Emoji().Get(context.Background(), emoji.Id, false)
require.NoErrorf(t, err, "failed to get emoji with id %v", emoji.Id)
}
for _, emoji := range emojis {
_, err := ss.Emoji().Get(context.Background(), emoji.Id, true)
require.NoErrorf(t, err, "failed to get emoji with id %v", emoji.Id)
}
}
func testEmojiGetByName(t *testing.T, ss store.Store) {
emojis := []model.Emoji{
{
CreatorId: model.NewId(),
Name: model.NewId(),
},
{
CreatorId: model.NewId(),
Name: model.NewId(),
},
{
CreatorId: model.NewId(),
Name: model.NewId(),
},
}
for i, emoji := range emojis {
data, err := ss.Emoji().Save(&emoji)
require.NoError(t, err)
emojis[i] = *data
}
defer func() {
for _, emoji := range emojis {
err := ss.Emoji().Delete(&emoji, time.Now().Unix())
require.NoError(t, err)
}
}()
for _, emoji := range emojis {
_, err := ss.Emoji().GetByName(context.Background(), emoji.Name, true)
require.NoErrorf(t, err, "failed to get emoji with name %v", emoji.Name)
}
}
func testEmojiGetMultipleByName(t *testing.T, ss store.Store) {
emojis := []model.Emoji{
{
CreatorId: model.NewId(),
Name: model.NewId(),
},
{
CreatorId: model.NewId(),
Name: model.NewId(),
},
{
CreatorId: model.NewId(),
Name: model.NewId(),
},
}
for i, emoji := range emojis {
data, err := ss.Emoji().Save(&emoji)
require.NoError(t, err)
emojis[i] = *data
}
defer func() {
for _, emoji := range emojis {
err := ss.Emoji().Delete(&emoji, time.Now().Unix())
require.NoError(t, err)
}
}()
t.Run("one emoji", func(t *testing.T) {
received, err := ss.Emoji().GetMultipleByName([]string{emojis[0].Name})
require.NoError(t, err, "could not get emoji")
require.Len(t, received, 1, "got incorrect emoji")
require.Equal(t, *received[0], emojis[0], "got incorrect emoji")
})
t.Run("multiple emojis", func(t *testing.T) {
received, err := ss.Emoji().GetMultipleByName([]string{emojis[0].Name, emojis[1].Name, emojis[2].Name})
require.NoError(t, err, "could not get emojis")
require.Len(t, received, 3, "got incorrect emojis")
})
t.Run("one nonexistent emoji", func(t *testing.T) {
received, err := ss.Emoji().GetMultipleByName([]string{"ab"})
require.NoError(t, err, "could not get emoji", err)
require.Empty(t, received, "got incorrect emoji")
})
t.Run("multiple emojis with nonexistent names", func(t *testing.T) {
received, err := ss.Emoji().GetMultipleByName([]string{emojis[0].Name, emojis[1].Name, emojis[2].Name, "abcd", "1234"})
require.NoError(t, err, "could not get emojis")
require.Len(t, received, 3, "got incorrect emojis")
})
}
func testEmojiGetList(t *testing.T, ss store.Store) {
emojis := []model.Emoji{
{
CreatorId: model.NewId(),
Name: "00000000000000000000000000a" + model.NewId(),
},
{
CreatorId: model.NewId(),
Name: "00000000000000000000000000b" + model.NewId(),
},
{
CreatorId: model.NewId(),
Name: "00000000000000000000000000c" + model.NewId(),
},
}
for i, emoji := range emojis {
data, err := ss.Emoji().Save(&emoji)
require.NoError(t, err)
emojis[i] = *data
}
defer func() {
for _, emoji := range emojis {
err := ss.Emoji().Delete(&emoji, time.Now().Unix())
require.NoError(t, err)
}
}()
result, err := ss.Emoji().GetList(0, 100, "")
require.NoError(t, err)
for _, emoji := range emojis {
found := false
for _, savedEmoji := range result {
if emoji.Id == savedEmoji.Id {
found = true
break
}
}
require.Truef(t, found, "failed to get emoji with id %v", emoji.Id)
}
remojis, err := ss.Emoji().GetList(0, 3, model.EmojiSortByName)
assert.NoError(t, err)
assert.Equal(t, 3, len(remojis))
assert.Equal(t, emojis[0].Name, remojis[0].Name)
assert.Equal(t, emojis[1].Name, remojis[1].Name)
assert.Equal(t, emojis[2].Name, remojis[2].Name)
remojis, err = ss.Emoji().GetList(1, 2, model.EmojiSortByName)
assert.NoError(t, err)
assert.Equal(t, 2, len(remojis))
assert.Equal(t, emojis[1].Name, remojis[0].Name)
assert.Equal(t, emojis[2].Name, remojis[1].Name)
}
func testEmojiSearch(t *testing.T, ss store.Store) {
emojis := []model.Emoji{
{
CreatorId: model.NewId(),
Name: "blargh_" + model.NewId(),
},
{
CreatorId: model.NewId(),
Name: model.NewId() + "_blargh",
},
{
CreatorId: model.NewId(),
Name: model.NewId() + "_blargh_" + model.NewId(),
},
{
CreatorId: model.NewId(),
Name: model.NewId(),
},
}
for i, emoji := range emojis {
data, err := ss.Emoji().Save(&emoji)
require.NoError(t, err)
emojis[i] = *data
}
defer func() {
for _, emoji := range emojis {
err := ss.Emoji().Delete(&emoji, time.Now().Unix())
require.NoError(t, err)
}
}()
shouldFind := []bool{true, false, false, false}
result, err := ss.Emoji().Search("blargh", true, 100)
require.NoError(t, err)
for i, emoji := range emojis {
found := false
for _, savedEmoji := range result {
if emoji.Id == savedEmoji.Id {
found = true
break
}
}
assert.Equal(t, shouldFind[i], found, emoji.Name)
}
shouldFind = []bool{true, true, true, false}
result, err = ss.Emoji().Search("blargh", false, 100)
require.NoError(t, err)
for i, emoji := range emojis {
found := false
for _, savedEmoji := range result {
if emoji.Id == savedEmoji.Id {
found = true
break
}
}
assert.Equal(t, shouldFind[i], found, emoji.Name)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"fmt"
"sort"
"testing"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFileInfoStore(t *testing.T, ss store.Store, s SqlStore) {
t.Cleanup(func() {
s.GetMasterX().Exec("TRUNCATE FileInfo")
})
t.Run("FileInfoSaveGet", func(t *testing.T) { testFileInfoSaveGet(t, ss) })
t.Run("FileInfoSaveGetByPath", func(t *testing.T) { testFileInfoSaveGetByPath(t, ss) })
t.Run("FileInfoGetForPost", func(t *testing.T) { testFileInfoGetForPost(t, ss) })
t.Run("FileInfoGetForUser", func(t *testing.T) { testFileInfoGetForUser(t, ss) })
t.Run("FileInfoGetWithOptions", func(t *testing.T) { testFileInfoGetWithOptions(t, ss) })
t.Run("FileInfoAttachToPost", func(t *testing.T) { testFileInfoAttachToPost(t, ss) })
t.Run("FileInfoDeleteForPost", func(t *testing.T) { testFileInfoDeleteForPost(t, ss) })
t.Run("FileInfoPermanentDelete", func(t *testing.T) { testFileInfoPermanentDelete(t, ss) })
t.Run("FileInfoPermanentDeleteBatch", func(t *testing.T) { testFileInfoPermanentDeleteBatch(t, ss) })
t.Run("FileInfoPermanentDeleteByUser", func(t *testing.T) { testFileInfoPermanentDeleteByUser(t, ss) })
t.Run("FileInfoUpdateMinipreview", func(t *testing.T) { testFileInfoUpdateMinipreview(t, ss) })
t.Run("GetFilesBatchForIndexing", func(t *testing.T) { testFileInfoStoreGetFilesBatchForIndexing(t, ss) })
t.Run("CountAll", func(t *testing.T) { testFileInfoStoreCountAll(t, ss) })
t.Run("GetStorageUsage", func(t *testing.T) { testFileInfoGetStorageUsage(t, ss) })
t.Run("GetUptoNSizeFileTime", func(t *testing.T) { testGetUptoNSizeFileTime(t, ss, s) })
}
func testFileInfoSaveGet(t *testing.T, ss store.Store) {
info := &model.FileInfo{
CreatorId: model.NewId(),
Path: "file.txt",
}
info, err := ss.FileInfo().Save(info)
require.NoError(t, err)
require.NotEqual(t, len(info.Id), 0)
defer func() {
ss.FileInfo().PermanentDelete(info.Id)
}()
rinfo, err := ss.FileInfo().Get(info.Id)
require.NoError(t, err)
require.Equal(t, info.Id, rinfo.Id)
info2, err := ss.FileInfo().Save(&model.FileInfo{
CreatorId: model.NewId(),
Path: "file.txt",
DeleteAt: 123,
})
require.NoError(t, err)
_, err = ss.FileInfo().Get(info2.Id)
assert.Error(t, err)
defer func() {
ss.FileInfo().PermanentDelete(info2.Id)
}()
}
func testFileInfoSaveGetByPath(t *testing.T, ss store.Store) {
info := &model.FileInfo{
CreatorId: model.NewId(),
Path: fmt.Sprintf("%v/file.txt", model.NewId()),
}
info, err := ss.FileInfo().Save(info)
require.NoError(t, err)
assert.NotEqual(t, len(info.Id), 0)
defer func() {
ss.FileInfo().PermanentDelete(info.Id)
}()
rinfo, err := ss.FileInfo().GetByPath(info.Path)
require.NoError(t, err)
assert.Equal(t, info.Id, rinfo.Id)
info2, err := ss.FileInfo().Save(&model.FileInfo{
CreatorId: model.NewId(),
Path: "file.txt",
DeleteAt: 123,
})
require.NoError(t, err)
_, err = ss.FileInfo().GetByPath(info2.Id)
assert.Error(t, err)
defer func() {
ss.FileInfo().PermanentDelete(info2.Id)
}()
}
func testFileInfoGetForPost(t *testing.T, ss store.Store) {
userId := model.NewId()
postId := model.NewId()
channelId := model.NewId()
infos := []*model.FileInfo{
{
PostId: postId,
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
},
{
PostId: postId,
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
},
{
PostId: postId,
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
DeleteAt: 123,
},
{
PostId: model.NewId(),
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
},
}
for i, info := range infos {
newInfo, err := ss.FileInfo().Save(info)
require.NoError(t, err)
infos[i] = newInfo
defer func(id string) {
ss.FileInfo().PermanentDelete(id)
}(newInfo.Id)
}
testCases := []struct {
Name string
PostId string
ReadFromMaster bool
IncludeDeleted bool
AllowFromCache bool
ExpectedPosts int
}{
{
Name: "Fetch from master, without deleted and without cache",
PostId: postId,
ReadFromMaster: true,
IncludeDeleted: false,
AllowFromCache: false,
ExpectedPosts: 2,
},
{
Name: "Fetch from master, with deleted and without cache",
PostId: postId,
ReadFromMaster: true,
IncludeDeleted: true,
AllowFromCache: false,
ExpectedPosts: 3,
},
{
Name: "Fetch from master, with deleted and with cache",
PostId: postId,
ReadFromMaster: true,
IncludeDeleted: true,
AllowFromCache: true,
ExpectedPosts: 3,
},
{
Name: "Fetch from replica, without deleted and without cache",
PostId: postId,
ReadFromMaster: false,
IncludeDeleted: false,
AllowFromCache: false,
ExpectedPosts: 2,
},
{
Name: "Fetch from replica, with deleted and without cache",
PostId: postId,
ReadFromMaster: false,
IncludeDeleted: true,
AllowFromCache: false,
ExpectedPosts: 3,
},
{
Name: "Fetch from replica, with deleted and without cache",
PostId: postId,
ReadFromMaster: false,
IncludeDeleted: true,
AllowFromCache: true,
ExpectedPosts: 3,
},
{
Name: "Fetch from replica, without deleted and with cache",
PostId: postId,
ReadFromMaster: true,
IncludeDeleted: false,
AllowFromCache: true,
ExpectedPosts: 2,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
postInfos, err := ss.FileInfo().GetForPost(
tc.PostId,
tc.ReadFromMaster,
tc.IncludeDeleted,
tc.AllowFromCache,
)
require.NoError(t, err)
assert.Len(t, postInfos, tc.ExpectedPosts)
})
}
}
func testFileInfoGetForUser(t *testing.T, ss store.Store) {
userId := model.NewId()
userId2 := model.NewId()
postId := model.NewId()
channelId := model.NewId()
infos := []*model.FileInfo{
{
PostId: postId,
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
},
{
PostId: postId,
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
},
{
PostId: postId,
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
},
{
PostId: model.NewId(),
ChannelId: channelId,
CreatorId: userId2,
Path: "file.txt",
},
}
for i, info := range infos {
newInfo, err := ss.FileInfo().Save(info)
require.NoError(t, err)
infos[i] = newInfo
defer func(id string) {
ss.FileInfo().PermanentDelete(id)
}(newInfo.Id)
}
userPosts, err := ss.FileInfo().GetForUser(userId)
require.NoError(t, err)
assert.Len(t, userPosts, 3)
userPosts, err = ss.FileInfo().GetForUser(userId2)
require.NoError(t, err)
assert.Len(t, userPosts, 1)
}
func testFileInfoGetWithOptions(t *testing.T, ss store.Store) {
makePost := func(chId string, user string) *model.Post {
post := model.Post{}
post.ChannelId = chId
post.UserId = user
_, err := ss.Post().Save(&post)
require.NoError(t, err)
return &post
}
makeFile := func(post *model.Post, user string, createAt int64, idPrefix string) model.FileInfo {
id := model.NewId()
id = idPrefix + id[1:] // hacky way to get sortable Ids to confirm secondary Id sort works
fileInfo := model.FileInfo{
Id: id,
CreatorId: user,
Path: "file.txt",
CreateAt: createAt,
}
if post.Id != "" {
fileInfo.PostId = post.Id
}
if post.ChannelId != "" {
fileInfo.ChannelId = post.ChannelId
}
_, err := ss.FileInfo().Save(&fileInfo)
require.NoError(t, err)
return fileInfo
}
userId1 := model.NewId()
userId2 := model.NewId()
channelId1 := model.NewId()
channelId2 := model.NewId()
channelId3 := model.NewId()
post1_1 := makePost(channelId1, userId1) // post 1 by user 1
post1_2 := makePost(channelId3, userId1) // post 2 by user 1
post2_1 := makePost(channelId2, userId2)
post2_2 := makePost(channelId3, userId2)
epoch := time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC)
file1_1 := makeFile(post1_1, userId1, epoch.AddDate(0, 0, 1).Unix(), "a") // file 1 by user 1
file1_2 := makeFile(post1_2, userId1, epoch.AddDate(0, 0, 2).Unix(), "b") // file 2 by user 1
file1_3 := makeFile(&model.Post{}, userId1, epoch.AddDate(0, 0, 3).Unix(), "c") // file that is not attached to a post
file2_1 := makeFile(post2_1, userId2, epoch.AddDate(0, 0, 4).Unix(), "d") // file 2 by user 1
file2_2 := makeFile(post2_2, userId2, epoch.AddDate(0, 0, 5).Unix(), "e")
// delete a file
_, err := ss.FileInfo().DeleteForPost(file2_2.PostId)
require.NoError(t, err)
testCases := []struct {
Name string
Page, PerPage int
Opt *model.GetFileInfosOptions
ExpectedFileIds []string
}{
{
Name: "Get files with nil option",
Page: 0,
PerPage: 10,
Opt: nil,
ExpectedFileIds: []string{file1_1.Id, file1_2.Id, file1_3.Id, file2_1.Id},
},
{
Name: "Get files including deleted",
Page: 0,
PerPage: 10,
Opt: &model.GetFileInfosOptions{IncludeDeleted: true},
ExpectedFileIds: []string{file1_1.Id, file1_2.Id, file1_3.Id, file2_1.Id, file2_2.Id},
},
{
Name: "Get files including deleted filtered by channel",
Page: 0,
PerPage: 10,
Opt: &model.GetFileInfosOptions{
IncludeDeleted: true,
ChannelIds: []string{channelId3},
},
ExpectedFileIds: []string{file1_2.Id, file2_2.Id},
},
{
Name: "Get files including deleted filtered by channel and user",
Page: 0,
PerPage: 10,
Opt: &model.GetFileInfosOptions{
IncludeDeleted: true,
UserIds: []string{userId1},
ChannelIds: []string{channelId3},
},
ExpectedFileIds: []string{file1_2.Id},
},
{
Name: "Get files including deleted sorted by created at",
Page: 0,
PerPage: 10,
Opt: &model.GetFileInfosOptions{
IncludeDeleted: true,
SortBy: model.FileinfoSortByCreated,
},
ExpectedFileIds: []string{file1_1.Id, file1_2.Id, file1_3.Id, file2_1.Id, file2_2.Id},
},
{
Name: "Get files filtered by user ordered by created at descending",
Page: 0,
PerPage: 10,
Opt: &model.GetFileInfosOptions{
UserIds: []string{userId1},
SortBy: model.FileinfoSortByCreated,
SortDescending: true,
},
ExpectedFileIds: []string{file1_3.Id, file1_2.Id, file1_1.Id},
},
{
Name: "Get all files including deleted ordered by created descending 2nd page of 3 per page ",
Page: 1,
PerPage: 3,
Opt: &model.GetFileInfosOptions{
IncludeDeleted: true,
SortBy: model.FileinfoSortByCreated,
SortDescending: true,
},
ExpectedFileIds: []string{file1_2.Id, file1_1.Id},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
fileInfos, err := ss.FileInfo().GetWithOptions(tc.Page, tc.PerPage, tc.Opt)
require.NoError(t, err)
require.Len(t, fileInfos, len(tc.ExpectedFileIds))
for i := range tc.ExpectedFileIds {
assert.Equal(t, tc.ExpectedFileIds[i], fileInfos[i].Id)
}
})
}
}
type byFileInfoId []*model.FileInfo
func (a byFileInfoId) Len() int { return len(a) }
func (a byFileInfoId) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byFileInfoId) Less(i, j int) bool { return a[i].Id < a[j].Id }
func testFileInfoAttachToPost(t *testing.T, ss store.Store) {
t.Run("should attach files", func(t *testing.T) {
userId := model.NewId()
postId := model.NewId()
channelId := model.NewId()
info1, err := ss.FileInfo().Save(&model.FileInfo{
CreatorId: userId,
Path: "file.txt",
})
require.NoError(t, err)
info2, err := ss.FileInfo().Save(&model.FileInfo{
CreatorId: userId,
Path: "file2.txt",
})
require.NoError(t, err)
require.Equal(t, "", info1.PostId)
require.Equal(t, "", info2.PostId)
err = ss.FileInfo().AttachToPost(info1.Id, postId, channelId, userId)
assert.NoError(t, err)
info1.PostId = postId
info1.ChannelId = channelId
err = ss.FileInfo().AttachToPost(info2.Id, postId, channelId, userId)
assert.NoError(t, err)
info2.PostId = postId
info2.ChannelId = channelId
data, err := ss.FileInfo().GetForPost(postId, true, false, false)
require.NoError(t, err)
expected := []*model.FileInfo{info1, info2}
sort.Sort(byFileInfoId(expected))
sort.Sort(byFileInfoId(data))
assert.EqualValues(t, expected, data)
})
t.Run("should not attach files to multiple posts", func(t *testing.T) {
userId := model.NewId()
postId := model.NewId()
channelId := model.NewId()
info, err := ss.FileInfo().Save(&model.FileInfo{
CreatorId: userId,
Path: "file.txt",
})
require.NoError(t, err)
require.Equal(t, "", info.PostId)
err = ss.FileInfo().AttachToPost(info.Id, model.NewId(), channelId, userId)
require.NoError(t, err)
err = ss.FileInfo().AttachToPost(info.Id, postId, channelId, userId)
require.Error(t, err)
})
t.Run("should not attach files owned from a different user", func(t *testing.T) {
userId := model.NewId()
postId := model.NewId()
channelId := model.NewId()
info, err := ss.FileInfo().Save(&model.FileInfo{
CreatorId: model.NewId(),
Path: "file.txt",
})
require.NoError(t, err)
require.Equal(t, "", info.PostId)
err = ss.FileInfo().AttachToPost(info.Id, postId, channelId, userId)
assert.Error(t, err)
})
t.Run("should attach files uploaded by nouser", func(t *testing.T) {
postId := model.NewId()
channelId := model.NewId()
info, err := ss.FileInfo().Save(&model.FileInfo{
CreatorId: "nouser",
Path: "file.txt",
})
require.NoError(t, err)
assert.Equal(t, "", info.PostId)
err = ss.FileInfo().AttachToPost(info.Id, postId, channelId, model.NewId())
require.NoError(t, err)
data, err := ss.FileInfo().GetForPost(postId, true, false, false)
require.NoError(t, err)
info.PostId = postId
info.ChannelId = channelId
assert.EqualValues(t, []*model.FileInfo{info}, data)
})
}
func testFileInfoDeleteForPost(t *testing.T, ss store.Store) {
userId := model.NewId()
postId := model.NewId()
channelId := model.NewId()
infos := []*model.FileInfo{
{
PostId: postId,
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
},
{
PostId: postId,
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
},
{
PostId: postId,
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
DeleteAt: 123,
},
{
PostId: model.NewId(),
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
},
}
for i, info := range infos {
newInfo, err := ss.FileInfo().Save(info)
require.NoError(t, err)
infos[i] = newInfo
defer func(id string) {
ss.FileInfo().PermanentDelete(id)
}(newInfo.Id)
}
_, err := ss.FileInfo().DeleteForPost(postId)
require.NoError(t, err)
infos, err = ss.FileInfo().GetForPost(postId, true, false, false)
require.NoError(t, err)
assert.Empty(t, infos)
}
func testFileInfoPermanentDelete(t *testing.T, ss store.Store) {
info, err := ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
ChannelId: model.NewId(),
CreatorId: model.NewId(),
Path: "file.txt",
})
require.NoError(t, err)
err = ss.FileInfo().PermanentDelete(info.Id)
require.NoError(t, err)
}
func testFileInfoPermanentDeleteBatch(t *testing.T, ss store.Store) {
postId := model.NewId()
channelId := model.NewId()
_, err := ss.FileInfo().Save(&model.FileInfo{
PostId: postId,
ChannelId: channelId,
CreatorId: model.NewId(),
Path: "file.txt",
CreateAt: 1000,
})
require.NoError(t, err)
_, err = ss.FileInfo().Save(&model.FileInfo{
PostId: postId,
ChannelId: channelId,
CreatorId: model.NewId(),
Path: "file.txt",
CreateAt: 1200,
})
require.NoError(t, err)
_, err = ss.FileInfo().Save(&model.FileInfo{
PostId: postId,
ChannelId: channelId,
CreatorId: model.NewId(),
Path: "file.txt",
CreateAt: 2000,
})
require.NoError(t, err)
postFiles, err := ss.FileInfo().GetForPost(postId, true, false, false)
require.NoError(t, err)
assert.Len(t, postFiles, 3)
_, err = ss.FileInfo().PermanentDeleteBatch(1500, 1000)
require.NoError(t, err)
postFiles, err = ss.FileInfo().GetForPost(postId, true, false, false)
require.NoError(t, err)
assert.Len(t, postFiles, 1)
}
func testFileInfoPermanentDeleteByUser(t *testing.T, ss store.Store) {
userId := model.NewId()
postId := model.NewId()
channelId := model.NewId()
_, err := ss.FileInfo().Save(&model.FileInfo{
PostId: postId,
ChannelId: channelId,
CreatorId: userId,
Path: "file.txt",
})
require.NoError(t, err)
_, err = ss.FileInfo().PermanentDeleteByUser(userId)
require.NoError(t, err)
}
func testFileInfoUpdateMinipreview(t *testing.T, ss store.Store) {
info := &model.FileInfo{
CreatorId: model.NewId(),
Path: "image.png",
}
info, err := ss.FileInfo().Save(info)
require.NoError(t, err)
require.NotEqual(t, len(info.Id), 0)
defer func() {
ss.FileInfo().PermanentDelete(info.Id)
}()
rinfo, err := ss.FileInfo().Get(info.Id)
require.NoError(t, err)
require.Equal(t, info.Id, rinfo.Id)
require.Nil(t, rinfo.MiniPreview)
miniPreview := []byte{0x0, 0x1, 0x2}
rinfo.MiniPreview = &miniPreview
rinfo, err = ss.FileInfo().Upsert(rinfo)
require.NoError(t, err)
require.Equal(t, info.Id, rinfo.Id)
tinfo, err := ss.FileInfo().Get(info.Id)
require.NoError(t, err)
require.Equal(t, info.Id, tinfo.Id)
require.Equal(t, *tinfo.MiniPreview, miniPreview)
}
func testFileInfoStoreGetFilesBatchForIndexing(t *testing.T, ss store.Store) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "Channel1"
c1.Name = "zz" + model.NewId() + "b"
c1.Type = model.ChannelTypeOpen
c1, _ = ss.Channel().Save(c1, -1)
c2 := &model.Channel{}
c2.TeamId = model.NewId()
c2.DisplayName = "Channel2"
c2.Name = "zz" + model.NewId() + "b"
c2.Type = model.ChannelTypeOpen
c2, _ = ss.Channel().Save(c2, -1)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.Message = "zz" + model.NewId() + "AAAAAAAAAAA"
o1, err := ss.Post().Save(o1)
require.NoError(t, err)
f1, err := ss.FileInfo().Save(&model.FileInfo{
PostId: o1.Id,
ChannelId: o1.ChannelId,
CreatorId: model.NewId(),
Path: "file1.txt",
})
require.NoError(t, err)
defer func() {
ss.FileInfo().PermanentDelete(f1.Id)
}()
time.Sleep(2 * time.Millisecond)
o2 := &model.Post{}
o2.ChannelId = c2.Id
o2.UserId = model.NewId()
o2.Message = "zz" + model.NewId() + "CCCCCCCCC"
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
f2, err := ss.FileInfo().Save(&model.FileInfo{
PostId: o2.Id,
ChannelId: o2.ChannelId,
CreatorId: model.NewId(),
Path: "file2.txt",
})
require.NoError(t, err)
defer func() {
ss.FileInfo().PermanentDelete(f2.Id)
}()
time.Sleep(2 * time.Millisecond)
o3 := &model.Post{}
o3.ChannelId = c1.Id
o3.UserId = model.NewId()
o3.RootId = o1.Id
o3.Message = "zz" + model.NewId() + "QQQQQQQQQQ"
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
f3, err := ss.FileInfo().Save(&model.FileInfo{
PostId: o3.Id,
ChannelId: o3.ChannelId,
CreatorId: model.NewId(),
Path: "file3.txt",
})
require.NoError(t, err)
defer func() {
ss.FileInfo().PermanentDelete(f3.Id)
}()
// Getting all
r, err := ss.FileInfo().GetFilesBatchForIndexing(f1.CreateAt-1, "", 100)
require.NoError(t, err)
require.Len(t, r, 3, "Expected 3 posts in results. Got %v", len(r))
// Testing pagination
r, err = ss.FileInfo().GetFilesBatchForIndexing(f1.CreateAt-1, "", 2)
require.NoError(t, err)
require.Len(t, r, 2, "Expected 2 posts in results. Got %v", len(r))
r, err = ss.FileInfo().GetFilesBatchForIndexing(r[1].CreateAt, r[1].Id, 2)
require.NoError(t, err)
require.Len(t, r, 1, "Expected 1 post in results. Got %v", len(r))
r, err = ss.FileInfo().GetFilesBatchForIndexing(r[0].CreateAt, r[0].Id, 2)
require.NoError(t, err)
require.Len(t, r, 0, "Expected 0 posts in results. Got %v", len(r))
}
func testFileInfoStoreCountAll(t *testing.T, ss store.Store) {
_, err := ss.FileInfo().PermanentDeleteBatch(model.GetMillis(), 100000)
require.NoError(t, err)
f1, err := ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
ChannelId: model.NewId(),
CreatorId: model.NewId(),
Path: "file1.txt",
})
require.NoError(t, err)
_, err = ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
ChannelId: model.NewId(),
CreatorId: model.NewId(),
Path: "file2.txt",
})
require.NoError(t, err)
_, err = ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
ChannelId: model.NewId(),
CreatorId: model.NewId(),
Path: "file3.txt",
})
require.NoError(t, err)
count, err := ss.FileInfo().CountAll()
require.NoError(t, err)
require.Equal(t, int64(3), count)
_, err = ss.FileInfo().DeleteForPost(f1.PostId)
require.NoError(t, err)
count, err = ss.FileInfo().CountAll()
require.NoError(t, err)
require.Equal(t, int64(2), count)
}
func testFileInfoGetStorageUsage(t *testing.T, ss store.Store) {
_, err := ss.FileInfo().PermanentDeleteBatch(model.GetMillis(), 100000)
require.NoError(t, err)
usage, err := ss.FileInfo().GetStorageUsage(false, false)
require.NoError(t, err)
require.Equal(t, int64(0), usage)
f1, err := ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
CreatorId: model.NewId(),
Size: 10,
Path: "file1.txt",
})
require.NoError(t, err)
_, err = ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
CreatorId: model.NewId(),
Size: 10,
Path: "file2.txt",
})
require.NoError(t, err)
_, err = ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
CreatorId: model.NewId(),
Size: 10,
Path: "file3.txt",
})
require.NoError(t, err)
usage, err = ss.FileInfo().GetStorageUsage(false, false)
require.NoError(t, err)
require.Equal(t, int64(30), usage)
_, err = ss.FileInfo().DeleteForPost(f1.PostId)
require.NoError(t, err)
usage, err = ss.FileInfo().GetStorageUsage(false, false)
require.NoError(t, err)
require.Equal(t, int64(20), usage)
usage, err = ss.FileInfo().GetStorageUsage(false, true)
require.NoError(t, err)
require.Equal(t, int64(30), usage)
}
func testGetUptoNSizeFileTime(t *testing.T, ss store.Store, s SqlStore) {
_, err := ss.FileInfo().GetUptoNSizeFileTime(0)
assert.Error(t, err)
_, err = ss.FileInfo().GetUptoNSizeFileTime(-1)
assert.Error(t, err)
_, err = ss.FileInfo().PermanentDeleteBatch(model.GetMillis(), 100000)
require.NoError(t, err)
diff := int64(10000)
now := utils.MillisFromTime(time.Now()) + diff
f1, err := ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
CreatorId: model.NewId(),
Size: 10,
Path: "file1.txt",
CreateAt: now,
})
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(f1.Id)
now = now + diff
f2, err := ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
CreatorId: model.NewId(),
Size: 10,
Path: "file2.txt",
CreateAt: now,
})
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(f2.Id)
now = now + diff
f3, err := ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
CreatorId: model.NewId(),
Size: 10,
Path: "file3.txt",
CreateAt: now,
})
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(f3.Id)
now = now + diff
tmp, err := ss.FileInfo().Save(&model.FileInfo{
PostId: model.NewId(),
CreatorId: model.NewId(),
Size: 10,
Path: "file4.txt",
CreateAt: now,
})
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(tmp.Id)
createAt, err := ss.FileInfo().GetUptoNSizeFileTime(20)
require.NoError(t, err)
assert.Equal(t, f3.CreateAt, createAt)
_, err = ss.FileInfo().GetUptoNSizeFileTime(5)
assert.Error(t, err)
assert.IsType(t, &store.ErrNotFound{}, err)
createAt, err = ss.FileInfo().GetUptoNSizeFileTime(1000)
require.NoError(t, err)
assert.Equal(t, f1.CreateAt, createAt)
_, err = ss.FileInfo().DeleteForPost(f3.PostId)
require.NoError(t, err)
createAt, err = ss.FileInfo().GetUptoNSizeFileTime(20)
require.NoError(t, err)
assert.Equal(t, f2.CreateAt, createAt)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"errors"
"fmt"
"math"
"sort"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
func TestGroupStore(t *testing.T, ss store.Store) {
t.Run("Create", func(t *testing.T) { testGroupStoreCreate(t, ss) })
t.Run("CreateWithUserIds", func(t *testing.T) { testGroupCreateWithUserIds(t, ss) })
t.Run("Get", func(t *testing.T) { testGroupStoreGet(t, ss) })
t.Run("GetByName", func(t *testing.T) { testGroupStoreGetByName(t, ss) })
t.Run("GetByIDs", func(t *testing.T) { testGroupStoreGetByIDs(t, ss) })
t.Run("GetByRemoteID", func(t *testing.T) { testGroupStoreGetByRemoteID(t, ss) })
t.Run("GetAllBySource", func(t *testing.T) { testGroupStoreGetAllByType(t, ss) })
t.Run("GetByUser", func(t *testing.T) { testGroupStoreGetByUser(t, ss) })
t.Run("Update", func(t *testing.T) { testGroupStoreUpdate(t, ss) })
t.Run("Delete", func(t *testing.T) { testGroupStoreDelete(t, ss) })
t.Run("Restore", func(t *testing.T) { testGroupStoreRestore(t, ss) })
t.Run("GetMemberUsers", func(t *testing.T) { testGroupGetMemberUsers(t, ss) })
t.Run("GetMemberUsersPage", func(t *testing.T) { testGroupGetMemberUsersPage(t, ss) })
t.Run("GetMemberUsersSortedPage", func(t *testing.T) { testGroupGetMemberUsersSortedPage(t, ss) })
t.Run("GetMemberUsersInTeam", func(t *testing.T) { testGroupGetMemberUsersInTeam(t, ss) })
t.Run("GetMemberUsersNotInChannel", func(t *testing.T) { testGroupGetMemberUsersNotInChannel(t, ss) })
t.Run("UpsertMember", func(t *testing.T) { testUpsertMember(t, ss) })
t.Run("UpsertMembers", func(t *testing.T) { testUpsertMembers(t, ss) })
t.Run("DeleteMember", func(t *testing.T) { testGroupDeleteMember(t, ss) })
t.Run("DeleteMembers", func(t *testing.T) { testGroupDeleteMembers(t, ss) })
t.Run("PermanentDeleteMembersByUser", func(t *testing.T) { testGroupPermanentDeleteMembersByUser(t, ss) })
t.Run("CreateGroupSyncable", func(t *testing.T) { testCreateGroupSyncable(t, ss) })
t.Run("GetGroupSyncable", func(t *testing.T) { testGetGroupSyncable(t, ss) })
t.Run("GetAllGroupSyncablesByGroupId", func(t *testing.T) { testGetAllGroupSyncablesByGroup(t, ss) })
t.Run("UpdateGroupSyncable", func(t *testing.T) { testUpdateGroupSyncable(t, ss) })
t.Run("DeleteGroupSyncable", func(t *testing.T) { testDeleteGroupSyncable(t, ss) })
t.Run("TeamMembersToAdd", func(t *testing.T) { testTeamMembersToAdd(t, ss) })
t.Run("TeamMembersToAdd_SingleTeam", func(t *testing.T) { testTeamMembersToAddSingleTeam(t, ss) })
t.Run("ChannelMembersToAdd", func(t *testing.T) { testChannelMembersToAdd(t, ss) })
t.Run("ChannelMembersToAdd_SingleChannel", func(t *testing.T) { testChannelMembersToAddSingleChannel(t, ss) })
t.Run("TeamMembersToRemove", func(t *testing.T) { testTeamMembersToRemove(t, ss) })
t.Run("TeamMembersToRemove_SingleTeam", func(t *testing.T) { testTeamMembersToRemoveSingleTeam(t, ss) })
t.Run("ChannelMembersToRemove", func(t *testing.T) { testChannelMembersToRemove(t, ss) })
t.Run("ChannelMembersToRemove_SingleChannel", func(t *testing.T) { testChannelMembersToRemoveSingleChannel(t, ss) })
t.Run("GetGroupsByChannel", func(t *testing.T) { testGetGroupsByChannel(t, ss) })
t.Run("GetGroupsAssociatedToChannelsByTeam", func(t *testing.T) { testGetGroupsAssociatedToChannelsByTeam(t, ss) })
t.Run("GetGroupsByTeam", func(t *testing.T) { testGetGroupsByTeam(t, ss) })
t.Run("GetGroups", func(t *testing.T) { testGetGroups(t, ss) })
t.Run("TeamMembersMinusGroupMembers", func(t *testing.T) { testTeamMembersMinusGroupMembers(t, ss) })
t.Run("ChannelMembersMinusGroupMembers", func(t *testing.T) { testChannelMembersMinusGroupMembers(t, ss) })
t.Run("GetMemberCount", func(t *testing.T) { groupTestGetMemberCount(t, ss) })
t.Run("AdminRoleGroupsForSyncableMember_Channel", func(t *testing.T) { groupTestAdminRoleGroupsForSyncableMemberChannel(t, ss) })
t.Run("AdminRoleGroupsForSyncableMember_Team", func(t *testing.T) { groupTestAdminRoleGroupsForSyncableMemberTeam(t, ss) })
t.Run("PermittedSyncableAdmins_Team", func(t *testing.T) { groupTestPermittedSyncableAdminsTeam(t, ss) })
t.Run("PermittedSyncableAdmins_Channel", func(t *testing.T) { groupTestPermittedSyncableAdminsChannel(t, ss) })
t.Run("UpdateMembersRole_Team", func(t *testing.T) { groupTestpUpdateMembersRoleTeam(t, ss) })
t.Run("UpdateMembersRole_Channel", func(t *testing.T) { groupTestpUpdateMembersRoleChannel(t, ss) })
t.Run("GroupCount", func(t *testing.T) { groupTestGroupCount(t, ss) })
t.Run("GroupTeamCount", func(t *testing.T) { groupTestGroupTeamCount(t, ss) })
t.Run("GroupChannelCount", func(t *testing.T) { groupTestGroupChannelCount(t, ss) })
t.Run("GroupMemberCount", func(t *testing.T) { groupTestGroupMemberCount(t, ss) })
t.Run("DistinctGroupMemberCount", func(t *testing.T) { groupTestDistinctGroupMemberCount(t, ss) })
t.Run("GroupCountWithAllowReference", func(t *testing.T) { groupTestGroupCountWithAllowReference(t, ss) })
t.Run("GetMember", func(t *testing.T) { groupTestGetMember(t, ss) })
t.Run("GetNonMemberUsersPage", func(t *testing.T) { groupTestGetNonMemberUsersPage(t, ss) })
t.Run("DistinctGroupMemberCountForSource", func(t *testing.T) { groupTestDistinctGroupMemberCountForSource(t, ss) })
}
func testGroupStoreCreate(t *testing.T, ss store.Store) {
// Save a new group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
// Happy path
d1, err := ss.Group().Create(g1)
require.NoError(t, err)
require.Len(t, d1.Id, 26)
require.Equal(t, *g1.Name, *d1.Name)
require.Equal(t, g1.DisplayName, d1.DisplayName)
require.Equal(t, g1.Description, d1.Description)
require.Equal(t, g1.RemoteId, d1.RemoteId)
require.NotZero(t, d1.CreateAt)
require.NotZero(t, d1.UpdateAt)
require.Zero(t, d1.DeleteAt)
// Requires display name
g2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "",
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
data, err := ss.Group().Create(g2)
require.Nil(t, data)
require.Error(t, err)
var appErr *model.AppError
require.True(t, errors.As(err, &appErr))
require.Equal(t, appErr.Id, "model.group.display_name.app_error")
// Won't accept a duplicate name
g4 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
_, err = ss.Group().Create(g4)
require.NoError(t, err)
g4b := &model.Group{
Name: g4.Name,
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
data, err = ss.Group().Create(g4b)
require.Nil(t, data)
require.Error(t, err)
require.Contains(t, err.Error(), fmt.Sprintf("Group with name %s already exists", *g4b.Name))
// Fields cannot be greater than max values
g5 := &model.Group{
Name: model.NewString(strings.Repeat("x", model.GroupNameMaxLength)),
DisplayName: strings.Repeat("x", model.GroupDisplayNameMaxLength),
Description: strings.Repeat("x", model.GroupDescriptionMaxLength),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
require.Nil(t, g5.IsValidForCreate())
g5.Name = model.NewString(*g5.Name + "x")
require.Equal(t, g5.IsValidForCreate().Id, "model.group.name.invalid_length.app_error")
g5.Name = model.NewString(model.NewId())
require.Nil(t, g5.IsValidForCreate())
g5.DisplayName = g5.DisplayName + "x"
require.Equal(t, g5.IsValidForCreate().Id, "model.group.display_name.app_error")
g5.DisplayName = model.NewId()
require.Nil(t, g5.IsValidForCreate())
g5.Description = g5.Description + "x"
require.Equal(t, g5.IsValidForCreate().Id, "model.group.description.app_error")
g5.Description = model.NewId()
require.Nil(t, g5.IsValidForCreate())
// Must use a valid type
g6 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSource("fake"),
RemoteId: model.NewString(model.NewId()),
}
require.Equal(t, g6.IsValidForCreate().Id, "model.group.source.app_error")
//must use valid characters
g7 := &model.Group{
Name: model.NewString("%^#@$$"),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
require.Equal(t, g7.IsValidForCreate().Id, "model.group.name.invalid_chars.app_error")
}
func testGroupCreateWithUserIds(t *testing.T, ss store.Store) {
// Create user 1
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
// Create user 2
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, nErr := ss.User().Save(u2)
require.NoError(t, nErr)
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceCustom,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
// Save a new group
guids1 := &model.GroupWithUserIds{
Group: *g1,
UserIds: []string{user1.Id, user2.Id},
}
// Happy path
d1, err := ss.Group().CreateWithUserIds(guids1)
require.NoError(t, err)
require.Len(t, d1.Id, 26)
require.Equal(t, *guids1.Name, *d1.Name)
require.Equal(t, guids1.DisplayName, d1.DisplayName)
require.Equal(t, guids1.Description, d1.Description)
require.Equal(t, guids1.RemoteId, d1.RemoteId)
require.NotZero(t, d1.CreateAt)
require.NotZero(t, d1.UpdateAt)
require.Zero(t, d1.DeleteAt)
require.Equal(t, *model.NewInt64(2), int64(*d1.MemberCount))
// Requires display name
g2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "",
Source: model.GroupSourceCustom,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
guids2 := &model.GroupWithUserIds{
Group: *g2,
UserIds: []string{user1.Id, user2.Id},
}
data, err := ss.Group().CreateWithUserIds(guids2)
require.Nil(t, data)
require.Error(t, err)
var appErr *model.AppError
require.True(t, errors.As(err, &appErr))
require.Equal(t, appErr.Id, "model.group.display_name.app_error")
// Won't accept a duplicate name
g4 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceCustom,
RemoteId: model.NewString(model.NewId()),
}
guids4 := &model.GroupWithUserIds{
Group: *g4,
UserIds: []string{user1.Id, user2.Id},
}
_, err = ss.Group().CreateWithUserIds(guids4)
require.NoError(t, err)
g4b := &model.Group{
Name: g4.Name,
DisplayName: model.NewId(),
Source: model.GroupSourceCustom,
RemoteId: model.NewString(model.NewId()),
}
guids4b := &model.GroupWithUserIds{
Group: *g4b,
UserIds: []string{user1.Id},
}
data, err = ss.Group().CreateWithUserIds(guids4b)
require.Nil(t, data)
require.Error(t, err)
require.Contains(t, err.Error(), "unique constraint: Name")
// Fields cannot be greater than max values
g5 := &model.Group{
Name: model.NewString(strings.Repeat("x", model.GroupNameMaxLength)),
DisplayName: strings.Repeat("x", model.GroupDisplayNameMaxLength),
Description: strings.Repeat("x", model.GroupDescriptionMaxLength),
Source: model.GroupSourceCustom,
RemoteId: model.NewString(model.NewId()),
}
guids5 := &model.GroupWithUserIds{
Group: *g5,
}
require.Nil(t, guids5.IsValidForCreate())
guids5.Name = model.NewString(*guids5.Name + "x")
require.Equal(t, guids5.IsValidForCreate().Id, "model.group.name.invalid_length.app_error")
guids5.Name = model.NewString(model.NewId())
require.Nil(t, guids5.IsValidForCreate())
guids5.DisplayName = guids5.DisplayName + "x"
require.Equal(t, guids5.IsValidForCreate().Id, "model.group.display_name.app_error")
guids5.DisplayName = model.NewId()
require.Nil(t, guids5.IsValidForCreate())
guids5.Description = guids5.Description + "x"
require.Equal(t, guids5.IsValidForCreate().Id, "model.group.description.app_error")
guids5.Description = model.NewId()
require.Nil(t, guids5.IsValidForCreate())
// Must use a valid type
g6 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSource("fake"),
RemoteId: model.NewString(model.NewId()),
}
guids6 := &model.GroupWithUserIds{
Group: *g6,
}
require.Equal(t, guids6.IsValidForCreate().Id, "model.group.source.app_error")
//must use valid characters
g7 := &model.Group{
Name: model.NewString("%^#@$$"),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceCustom,
RemoteId: model.NewString(model.NewId()),
}
guids7 := &model.GroupWithUserIds{
Group: *g7,
}
require.Equal(t, guids7.IsValidForCreate().Id, "model.group.name.invalid_chars.app_error")
// Invalid user ids
g8 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceCustom,
RemoteId: model.NewString(model.NewId()),
}
guids8 := &model.GroupWithUserIds{
Group: *g8,
UserIds: []string{"1234uid"},
}
data, err = ss.Group().CreateWithUserIds(guids8)
require.Nil(t, data)
require.Error(t, err)
require.Equal(t, store.NewErrNotFound("User", "1234uid"), err)
}
func testGroupStoreGet(t *testing.T, ss store.Store) {
// Create a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
d1, err := ss.Group().Create(g1)
require.NoError(t, err)
require.Len(t, d1.Id, 26)
// Get the group
d2, err := ss.Group().Get(d1.Id)
require.NoError(t, err)
require.Equal(t, d1.Id, d2.Id)
require.Equal(t, *d1.Name, *d2.Name)
require.Equal(t, d1.DisplayName, d2.DisplayName)
require.Equal(t, d1.Description, d2.Description)
require.Equal(t, d1.RemoteId, d2.RemoteId)
require.Equal(t, d1.CreateAt, d2.CreateAt)
require.Equal(t, d1.UpdateAt, d2.UpdateAt)
require.Equal(t, d1.DeleteAt, d2.DeleteAt)
// Get an invalid group
_, err = ss.Group().Get(model.NewId())
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
}
func testGroupStoreGetByName(t *testing.T, ss store.Store) {
// Create a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
g1Opts := model.GroupSearchOpts{
FilterAllowReference: false,
}
d1, err := ss.Group().Create(g1)
require.NoError(t, err)
require.Len(t, d1.Id, 26)
// Get the group
d2, err := ss.Group().GetByName(*d1.Name, g1Opts)
require.NoError(t, err)
require.Equal(t, d1.Id, d2.Id)
require.Equal(t, *d1.Name, *d2.Name)
require.Equal(t, d1.DisplayName, d2.DisplayName)
require.Equal(t, d1.Description, d2.Description)
require.Equal(t, d1.RemoteId, d2.RemoteId)
require.Equal(t, d1.CreateAt, d2.CreateAt)
require.Equal(t, d1.UpdateAt, d2.UpdateAt)
require.Equal(t, d1.DeleteAt, d2.DeleteAt)
// Get an invalid group
_, err = ss.Group().GetByName(model.NewId(), g1Opts)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
}
func testGroupStoreGetByIDs(t *testing.T, ss store.Store) {
var group1 *model.Group
var group2 *model.Group
for i := 0; i < 2; i++ {
group := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(group)
require.NoError(t, err)
switch i {
case 0:
group1 = group
case 1:
group2 = group
}
}
groups, err := ss.Group().GetByIDs([]string{group1.Id, group2.Id})
require.NoError(t, err)
require.Len(t, groups, 2)
for i := 0; i < 2; i++ {
require.True(t, (groups[i].Id == group1.Id || groups[i].Id == group2.Id))
}
require.True(t, groups[0].Id != groups[1].Id)
}
func testGroupStoreGetByRemoteID(t *testing.T, ss store.Store) {
// Create a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
d1, err := ss.Group().Create(g1)
require.NoError(t, err)
require.Len(t, d1.Id, 26)
// Get the group
d2, err := ss.Group().GetByRemoteID(*d1.RemoteId, model.GroupSourceLdap)
require.NoError(t, err)
require.Equal(t, d1.Id, d2.Id)
require.Equal(t, *d1.Name, *d2.Name)
require.Equal(t, d1.DisplayName, d2.DisplayName)
require.Equal(t, d1.Description, d2.Description)
require.Equal(t, d1.RemoteId, d2.RemoteId)
require.Equal(t, d1.CreateAt, d2.CreateAt)
require.Equal(t, d1.UpdateAt, d2.UpdateAt)
require.Equal(t, d1.DeleteAt, d2.DeleteAt)
// Get an invalid group
_, err = ss.Group().GetByRemoteID(model.NewId(), model.GroupSource("fake"))
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
}
func testGroupStoreGetAllByType(t *testing.T, ss store.Store) {
numGroups := 10
groups := []*model.Group{}
// Create groups
for i := 0; i < numGroups; i++ {
g := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
groups = append(groups, g)
_, err := ss.Group().Create(g)
require.NoError(t, err)
}
// Returns all the groups
d1, err := ss.Group().GetAllBySource(model.GroupSourceLdap)
require.NoError(t, err)
require.Condition(t, func() bool { return len(d1) >= numGroups }, len(d1), ">=", numGroups)
for _, expectedGroup := range groups {
present := false
for _, dbGroup := range d1 {
if dbGroup.Id == expectedGroup.Id {
present = true
break
}
}
require.True(t, present)
}
}
func testGroupStoreGetByUser(t *testing.T, ss store.Store) {
// Save a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
g1, err := ss.Group().Create(g1)
require.NoError(t, err)
g2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
g2, err = ss.Group().Create(g2)
require.NoError(t, err)
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
u1, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(g1.Id, u1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(g2.Id, u1.Id)
require.NoError(t, err)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
u2, nErr = ss.User().Save(u2)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(g2.Id, u2.Id)
require.NoError(t, err)
groups, err := ss.Group().GetByUser(u1.Id)
require.NoError(t, err)
assert.Equal(t, 2, len(groups))
found1 := false
found2 := false
for _, g := range groups {
if g.Id == g1.Id {
found1 = true
}
if g.Id == g2.Id {
found2 = true
}
}
assert.True(t, found1)
assert.True(t, found2)
groups, err = ss.Group().GetByUser(u2.Id)
require.NoError(t, err)
require.Equal(t, 1, len(groups))
assert.Equal(t, g2.Id, groups[0].Id)
groups, err = ss.Group().GetByUser(model.NewId())
require.NoError(t, err)
assert.Equal(t, 0, len(groups))
}
func testGroupStoreUpdate(t *testing.T, ss store.Store) {
// Save a new group
g1 := &model.Group{
Name: model.NewString("g1-test"),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
// Create a group
d1, err := ss.Group().Create(g1)
require.NoError(t, err)
// Update happy path
g1Update := &model.Group{}
*g1Update = *g1
g1Update.Name = model.NewString(model.NewId())
g1Update.DisplayName = model.NewId()
g1Update.Description = model.NewId()
g1Update.RemoteId = model.NewString(model.NewId())
ud1, err := ss.Group().Update(g1Update)
require.NoError(t, err)
// Not changed...
require.Equal(t, d1.Id, ud1.Id)
require.Equal(t, d1.CreateAt, ud1.CreateAt)
require.Equal(t, d1.Source, ud1.Source)
// Still zero...
require.Zero(t, ud1.DeleteAt)
// Updated...
require.Equal(t, *g1Update.Name, *ud1.Name)
require.Equal(t, g1Update.DisplayName, ud1.DisplayName)
require.Equal(t, g1Update.Description, ud1.Description)
require.Equal(t, g1Update.RemoteId, ud1.RemoteId)
// Requires display name
data, err := ss.Group().Update(&model.Group{
Id: d1.Id,
Name: model.NewString(model.NewId()),
DisplayName: "",
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.Nil(t, data)
require.Error(t, err)
var appErr *model.AppError
require.True(t, errors.As(err, &appErr))
require.Equal(t, appErr.Id, "model.group.display_name.app_error")
// Create another Group
g2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
d2, err := ss.Group().Create(g2)
require.NoError(t, err)
// Can't update the name to be a duplicate of an existing group's name
_, err = ss.Group().Update(&model.Group{
Id: d2.Id,
Name: g1Update.Name,
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
})
require.Error(t, err)
require.Contains(t, err.Error(), "unique constraint: Name")
// Cannot update CreateAt
someVal := model.GetMillis()
d1.CreateAt = someVal
d3, err := ss.Group().Update(d1)
require.NoError(t, err)
require.NotEqual(t, someVal, d3.CreateAt)
// Cannot update DeleteAt to non-zero
d1.DeleteAt = 1
_, err = ss.Group().Update(d1)
require.Error(t, err)
require.Contains(t, err.Error(), "DeleteAt should be 0 when updating")
//...except for 0 for DeleteAt
d1.DeleteAt = 0
d4, err := ss.Group().Update(d1)
require.NoError(t, err)
require.Zero(t, d4.DeleteAt)
}
func testGroupStoreDelete(t *testing.T, ss store.Store) {
// Save a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
d1, err := ss.Group().Create(g1)
require.NoError(t, err)
require.Len(t, d1.Id, 26)
// Check the group is retrievable
_, err = ss.Group().Get(d1.Id)
require.NoError(t, err)
// Get the before count
d7, err := ss.Group().GetAllBySource(model.GroupSourceLdap)
require.NoError(t, err)
beforeCount := len(d7)
// Delete the group
_, err = ss.Group().Delete(d1.Id)
require.NoError(t, err)
// Check the group is deleted
d4, err := ss.Group().Get(d1.Id)
require.NoError(t, err)
require.NotZero(t, d4.DeleteAt)
// Check the after count
d5, err := ss.Group().GetAllBySource(model.GroupSourceLdap)
require.NoError(t, err)
afterCount := len(d5)
require.Condition(t, func() bool { return beforeCount == afterCount+1 }, beforeCount, "==", afterCount+1)
// Try and delete a nonexistent group
_, err = ss.Group().Delete(model.NewId())
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
// Cannot delete again
_, err = ss.Group().Delete(d1.Id)
require.True(t, errors.As(err, &nfErr))
}
func testGroupStoreRestore(t *testing.T, ss store.Store) {
// Save a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
d1, err := ss.Group().Create(g1)
require.NoError(t, err)
require.Len(t, d1.Id, 26)
// Check the group is retrievable
_, err = ss.Group().Get(d1.Id)
require.NoError(t, err)
// Delete the group
_, err = ss.Group().Delete(d1.Id)
require.NoError(t, err)
// Get the before count
d7, err := ss.Group().GetAllBySource(model.GroupSourceLdap)
require.NoError(t, err)
beforeCount := len(d7)
// restore the group
_, err = ss.Group().Restore(d1.Id)
require.NoError(t, err)
// Check the group is restored
d4, err := ss.Group().Get(d1.Id)
require.NoError(t, err)
require.Zero(t, d4.DeleteAt)
// Check the after count
d5, err := ss.Group().GetAllBySource(model.GroupSourceLdap)
require.NoError(t, err)
afterCount := len(d5)
require.Condition(t, func() bool { return beforeCount == afterCount-1 })
// Try and restore a nonexistent group
_, err = ss.Group().Delete(model.NewId())
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
// Cannot restore again
_, err = ss.Group().Restore(d1.Id)
require.True(t, errors.As(err, &nfErr))
}
func testGroupGetMemberUsers(t *testing.T, ss store.Store) {
// Save a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(group.Id, user1.Id)
require.NoError(t, err)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, nErr := ss.User().Save(u2)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(group.Id, user2.Id)
require.NoError(t, err)
// Check returns members
groupMembers, err := ss.Group().GetMemberUsers(group.Id)
require.NoError(t, err)
require.Equal(t, 2, len(groupMembers))
// Check madeup id
groupMembers, err = ss.Group().GetMemberUsers(model.NewId())
require.NoError(t, err)
require.Equal(t, 0, len(groupMembers))
// Delete a member
_, err = ss.Group().DeleteMember(group.Id, user1.Id)
require.NoError(t, err)
// Should not return deleted members
groupMembers, err = ss.Group().GetMemberUsers(group.Id)
require.NoError(t, err)
require.Equal(t, 1, len(groupMembers))
}
func testGroupGetMemberUsersPage(t *testing.T, ss store.Store) {
// Save a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
u1 := &model.User{
Email: MakeEmail(),
Username: "user1" + model.NewId(),
}
user1, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(group.Id, user1.Id)
require.NoError(t, err)
u2 := &model.User{
Email: MakeEmail(),
Username: "user2" + model.NewId(),
}
user2, nErr := ss.User().Save(u2)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(group.Id, user2.Id)
require.NoError(t, err)
u3 := &model.User{
Email: MakeEmail(),
Username: "user3" + model.NewId(),
}
user3, nErr := ss.User().Save(u3)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(group.Id, user3.Id)
require.NoError(t, err)
// Check returns members
groupMembers, err := ss.Group().GetMemberUsersPage(group.Id, 0, 100, nil)
require.NoError(t, err)
require.Equal(t, 3, len(groupMembers))
// Check page 1
groupMembers, err = ss.Group().GetMemberUsersPage(group.Id, 0, 2, nil)
require.NoError(t, err)
require.Equal(t, 2, len(groupMembers))
require.ElementsMatch(t, []*model.User{user1, user2}, groupMembers)
// Check page 2
groupMembers, err = ss.Group().GetMemberUsersPage(group.Id, 1, 2, nil)
require.NoError(t, err)
require.Equal(t, 1, len(groupMembers))
require.ElementsMatch(t, []*model.User{user3}, groupMembers)
// Check madeup id
groupMembers, err = ss.Group().GetMemberUsersPage(model.NewId(), 0, 100, nil)
require.NoError(t, err)
require.Equal(t, 0, len(groupMembers))
// Delete a member
_, err = ss.Group().DeleteMember(group.Id, user1.Id)
require.NoError(t, err)
// Should not return deleted members
groupMembers, err = ss.Group().GetMemberUsersPage(group.Id, 0, 100, nil)
require.NoError(t, err)
require.Equal(t, 2, len(groupMembers))
}
func testGroupGetMemberUsersSortedPage(t *testing.T, ss store.Store) {
// Save a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
// First by nickname, third by full name, second by username
u1 := &model.User{
Email: MakeEmail(),
Username: "y" + model.NewId(),
Nickname: "a" + model.NewId(),
FirstName: "z" + model.NewId(),
LastName: "z" + model.NewId(),
}
user1, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(group.Id, user1.Id)
require.NoError(t, err)
// Second by nickname, first by full name, third by username
u2 := &model.User{
Email: MakeEmail(),
Username: "z" + model.NewId(),
FirstName: "b" + model.NewId(),
LastName: "b" + model.NewId(),
}
user2, nErr := ss.User().Save(u2)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(group.Id, user2.Id)
require.NoError(t, err)
// Third by nickname, second by full name, first by username
u3 := &model.User{
Email: MakeEmail(),
Username: "d" + model.NewId(),
}
user3, nErr := ss.User().Save(u3)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(group.Id, user3.Id)
require.NoError(t, err)
// Check nickname ordering, paged
groupMembers, err := ss.Group().GetMemberUsersSortedPage(group.Id, 0, 2, nil, model.ShowNicknameFullName)
require.NoError(t, err)
require.Equal(t, 2, len(groupMembers))
require.ElementsMatch(t, []*model.User{user1, user2}, groupMembers)
groupMembers, err = ss.Group().GetMemberUsersSortedPage(group.Id, 1, 2, nil, model.ShowNicknameFullName)
require.NoError(t, err)
require.Equal(t, 1, len(groupMembers))
require.ElementsMatch(t, []*model.User{user3}, groupMembers)
// Check full name ordering, paged
groupMembers, err = ss.Group().GetMemberUsersSortedPage(group.Id, 0, 2, nil, model.ShowFullName)
require.NoError(t, err)
require.Equal(t, 2, len(groupMembers))
require.ElementsMatch(t, []*model.User{user2, user3}, groupMembers)
groupMembers, err = ss.Group().GetMemberUsersSortedPage(group.Id, 1, 2, nil, model.ShowFullName)
require.NoError(t, err)
require.Equal(t, 1, len(groupMembers))
require.ElementsMatch(t, []*model.User{user1}, groupMembers)
// Check username ordering
groupMembers, err = ss.Group().GetMemberUsersSortedPage(group.Id, 0, 2, nil, model.ShowUsername)
require.NoError(t, err)
require.Equal(t, 2, len(groupMembers))
require.ElementsMatch(t, []*model.User{user3, user1}, groupMembers)
groupMembers, err = ss.Group().GetMemberUsersSortedPage(group.Id, 1, 2, nil, model.ShowUsername)
require.NoError(t, err)
require.Equal(t, 1, len(groupMembers))
require.ElementsMatch(t, []*model.User{user2}, groupMembers)
}
func testGroupGetMemberUsersInTeam(t *testing.T, ss store.Store) {
// Save a team
team := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team, err := ss.Team().Save(team)
require.NoError(t, err)
// Save a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err := ss.User().Save(u1)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group.Id, user1.Id)
require.NoError(t, err)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err := ss.User().Save(u2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group.Id, user2.Id)
require.NoError(t, err)
u3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, err := ss.User().Save(u3)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group.Id, user3.Id)
require.NoError(t, err)
// returns no members when team does not exist
groupMembers, err := ss.Group().GetMemberUsersInTeam(group.Id, "non-existent-channel-id")
require.NoError(t, err)
require.Equal(t, 0, len(groupMembers))
// returns no members when group has no members in the team
groupMembers, err = ss.Group().GetMemberUsersInTeam(group.Id, team.Id)
require.NoError(t, err)
require.Equal(t, 0, len(groupMembers))
m1 := &model.TeamMember{TeamId: team.Id, UserId: user1.Id}
_, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
// returns single member in team
groupMembers, err = ss.Group().GetMemberUsersInTeam(group.Id, team.Id)
require.NoError(t, err)
require.Equal(t, 1, len(groupMembers))
m2 := &model.TeamMember{TeamId: team.Id, UserId: user2.Id}
m3 := &model.TeamMember{TeamId: team.Id, UserId: user3.Id}
_, nErr = ss.Team().SaveMember(m2, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(m3, -1)
require.NoError(t, nErr)
// returns all members when all members are in team
groupMembers, err = ss.Group().GetMemberUsersInTeam(group.Id, team.Id)
require.NoError(t, err)
require.Equal(t, 3, len(groupMembers))
}
func testGroupGetMemberUsersNotInChannel(t *testing.T, ss store.Store) {
// Save a team
team := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team, err := ss.Team().Save(team)
require.NoError(t, err)
// Save a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err := ss.User().Save(u1)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group.Id, user1.Id)
require.NoError(t, err)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err := ss.User().Save(u2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group.Id, user2.Id)
require.NoError(t, err)
u3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, err := ss.User().Save(u3)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group.Id, user3.Id)
require.NoError(t, err)
// Create Channel
channel := &model.Channel{
TeamId: team.Id,
DisplayName: "Channel",
Name: model.NewId(),
Type: model.ChannelTypeOpen, // Query does not look at type so this shouldn't matter.
}
channel, nErr := ss.Channel().Save(channel, 9999)
require.NoError(t, nErr)
// returns no members when channel does not exist
groupMembers, err := ss.Group().GetMemberUsersNotInChannel(group.Id, "non-existent-channel-id")
require.NoError(t, err)
require.Equal(t, 0, len(groupMembers))
// returns no members when group has no members in the team that the channel belongs to
groupMembers, err = ss.Group().GetMemberUsersNotInChannel(group.Id, channel.Id)
require.NoError(t, err)
require.Equal(t, 0, len(groupMembers))
m1 := &model.TeamMember{TeamId: team.Id, UserId: user1.Id}
_, nErr = ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
// returns single member in team and not in channel
groupMembers, err = ss.Group().GetMemberUsersNotInChannel(group.Id, channel.Id)
require.NoError(t, err)
require.Equal(t, 1, len(groupMembers))
m2 := &model.TeamMember{TeamId: team.Id, UserId: user2.Id}
m3 := &model.TeamMember{TeamId: team.Id, UserId: user3.Id}
_, nErr = ss.Team().SaveMember(m2, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(m3, -1)
require.NoError(t, nErr)
// returns all members when all members are in team and not in channel
groupMembers, err = ss.Group().GetMemberUsersNotInChannel(group.Id, channel.Id)
require.NoError(t, err)
require.Equal(t, 3, len(groupMembers))
cm1 := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user1.Id,
SchemeGuest: false,
SchemeUser: true,
SchemeAdmin: false,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(cm1)
require.NoError(t, err)
// returns both members not yet added to channel
groupMembers, err = ss.Group().GetMemberUsersNotInChannel(group.Id, channel.Id)
require.NoError(t, err)
require.Equal(t, 2, len(groupMembers))
cm2 := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user2.Id,
SchemeGuest: false,
SchemeUser: true,
SchemeAdmin: false,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
cm3 := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user3.Id,
SchemeGuest: false,
SchemeUser: true,
SchemeAdmin: false,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(cm2)
require.NoError(t, err)
_, err = ss.Channel().SaveMember(cm3)
require.NoError(t, err)
// returns none when all members have been added to team and channel
groupMembers, err = ss.Group().GetMemberUsersNotInChannel(group.Id, channel.Id)
require.NoError(t, err)
require.Equal(t, 0, len(groupMembers))
}
func testUpsertMember(t *testing.T, ss store.Store) {
// Create group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
// Create user
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
// Happy path
d2, err := ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
require.Equal(t, d2.GroupId, group.Id)
require.Equal(t, d2.UserId, user.Id)
require.NotZero(t, d2.CreateAt)
require.Zero(t, d2.DeleteAt)
// Duplicate composite key (GroupId, UserId)
// Ensure new CreateAt > previous CreateAt for the same (groupId, userId)
time.Sleep(2 * time.Millisecond)
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
// Invalid GroupId
_, err = ss.Group().UpsertMember(model.NewId(), user.Id)
require.Error(t, err)
require.Contains(t, err.Error(), "failed to get UserGroup with")
// Restores a deleted member
// Ensure new CreateAt > previous CreateAt for the same (groupId, userId)
time.Sleep(2 * time.Millisecond)
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
_, err = ss.Group().DeleteMember(group.Id, user.Id)
require.NoError(t, err)
groupMembers, err := ss.Group().GetMemberUsers(group.Id)
require.NoError(t, err)
beforeRestoreCount := len(groupMembers)
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
groupMembers, err = ss.Group().GetMemberUsers(group.Id)
require.NoError(t, err)
afterRestoreCount := len(groupMembers)
require.Equal(t, beforeRestoreCount+1, afterRestoreCount)
}
func testUpsertMembers(t *testing.T, ss store.Store) {
// Create group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
// Create user
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
// Create user
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, nErr := ss.User().Save(u2)
require.NoError(t, nErr)
// Happy path
m, err := ss.Group().UpsertMembers(group.Id, []string{user.Id, user2.Id})
require.NoError(t, err)
require.Equal(t, 2, len(m))
// Duplicate composite key (GroupId, UserId)
// Ensure new CreateAt > previous CreateAt for the same (groupId, userId)
// time.Sleep(2 * time.Millisecond)
_, err = ss.Group().UpsertMembers(group.Id, []string{user.Id})
require.NoError(t, err)
// Invalid GroupId
_, err = ss.Group().UpsertMembers(model.NewId(), []string{user.Id})
require.Error(t, err)
require.Contains(t, err.Error(), "failed to get UserGroup with")
// Restores a deleted member
// Ensure new CreateAt > previous CreateAt for the same (groupId, userId)
time.Sleep(2 * time.Millisecond)
_, err = ss.Group().UpsertMembers(group.Id, []string{user.Id, user2.Id})
require.NoError(t, err)
_, err = ss.Group().DeleteMembers(group.Id, []string{user.Id})
require.NoError(t, err)
groupMembers, err := ss.Group().GetMemberUsers(group.Id)
require.NoError(t, err)
beforeRestoreCount := len(groupMembers)
_, err = ss.Group().UpsertMembers(group.Id, []string{user.Id, user2.Id})
require.NoError(t, err)
groupMembers, err = ss.Group().GetMemberUsers(group.Id)
require.NoError(t, err)
afterRestoreCount := len(groupMembers)
require.Equal(t, beforeRestoreCount+1, afterRestoreCount)
}
func testGroupDeleteMember(t *testing.T, ss store.Store) {
// Create group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
// Create user
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
// Create member
d1, err := ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
// Happy path
d2, err := ss.Group().DeleteMember(group.Id, user.Id)
require.NoError(t, err)
require.Equal(t, d2.GroupId, group.Id)
require.Equal(t, d2.UserId, user.Id)
require.Equal(t, d2.CreateAt, d1.CreateAt)
require.NotZero(t, d2.DeleteAt)
// Delete an already deleted member
_, err = ss.Group().DeleteMember(group.Id, user.Id)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
// Delete with non-existent User
_, err = ss.Group().DeleteMember(group.Id, model.NewId())
require.True(t, errors.As(err, &nfErr))
// Delete non-existent Group
_, err = ss.Group().DeleteMember(model.NewId(), group.Id)
require.True(t, errors.As(err, &nfErr))
}
func testGroupDeleteMembers(t *testing.T, ss store.Store) {
// Create user
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
// Create group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
guids := &model.GroupWithUserIds{
Group: *g1,
UserIds: []string{user.Id},
}
group, err := ss.Group().CreateWithUserIds(guids)
require.NoError(t, err)
// Happy path
d2, err := ss.Group().DeleteMembers(group.Id, []string{user.Id})
require.NoError(t, err)
require.Equal(t, d2[0].GroupId, group.Id)
require.Equal(t, d2[0].UserId, user.Id)
require.NotZero(t, d2[0].DeleteAt)
// Delete an already deleted member
_, err = ss.Group().DeleteMembers(group.Id, []string{user.Id})
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
// Delete with non-existent User
_, err = ss.Group().DeleteMembers(group.Id, []string{model.NewId()})
require.True(t, errors.As(err, &nfErr))
// Delete non-existent Group
_, err = ss.Group().DeleteMembers(model.NewId(), []string{user.Id})
require.True(t, errors.As(err, &nfErr))
}
func testGroupPermanentDeleteMembersByUser(t *testing.T, ss store.Store) {
var g *model.Group
var groups []*model.Group
numberOfGroups := 5
for i := 0; i < numberOfGroups; i++ {
g = &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g)
groups = append(groups, group)
require.NoError(t, err)
}
// Create user
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user, err := ss.User().Save(u1)
require.NoError(t, err)
// Create members
for _, group := range groups {
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
}
// Happy path
err = ss.Group().PermanentDeleteMembersByUser(user.Id)
require.NoError(t, err)
}
func testCreateGroupSyncable(t *testing.T, ss store.Store) {
// Invalid GroupID
_, err := ss.Group().CreateGroupSyncable(model.NewGroupTeam("x", model.NewId(), false))
var appErr *model.AppError
require.True(t, errors.As(err, &appErr))
require.Equal(t, appErr.Id, "model.group_syncable.group_id.app_error")
// Create Group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
// Create Team
t1 := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(t1)
require.NoError(t, nErr)
// New GroupSyncable, happy path
gt1 := model.NewGroupTeam(group.Id, team.Id, false)
d1, err := ss.Group().CreateGroupSyncable(gt1)
require.NoError(t, err)
require.Equal(t, gt1.SyncableId, d1.SyncableId)
require.Equal(t, gt1.GroupId, d1.GroupId)
require.Equal(t, gt1.AutoAdd, d1.AutoAdd)
require.NotZero(t, d1.CreateAt)
require.Zero(t, d1.DeleteAt)
}
func testGetGroupSyncable(t *testing.T, ss store.Store) {
// Create a group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
// Create Team
t1 := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(t1)
require.NoError(t, nErr)
// Create GroupSyncable
gt1 := model.NewGroupTeam(group.Id, team.Id, false)
groupTeam, err := ss.Group().CreateGroupSyncable(gt1)
require.NoError(t, err)
// Get GroupSyncable
dgt, err := ss.Group().GetGroupSyncable(groupTeam.GroupId, groupTeam.SyncableId, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.Equal(t, gt1.GroupId, dgt.GroupId)
require.Equal(t, gt1.SyncableId, dgt.SyncableId)
require.Equal(t, gt1.AutoAdd, dgt.AutoAdd)
require.NotZero(t, gt1.CreateAt)
require.NotZero(t, gt1.UpdateAt)
require.Zero(t, gt1.DeleteAt)
}
func testGetAllGroupSyncablesByGroup(t *testing.T, ss store.Store) {
numGroupSyncables := 10
// Create group
g := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g)
require.NoError(t, err)
groupTeams := []*model.GroupSyncable{}
// Create groupTeams
for i := 0; i < numGroupSyncables; i++ {
// Create Team
t1 := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
var team *model.Team
team, nErr := ss.Team().Save(t1)
require.NoError(t, nErr)
// create groupteam
var groupTeam *model.GroupSyncable
gt := model.NewGroupTeam(group.Id, team.Id, false)
gt.SchemeAdmin = true
groupTeam, err = ss.Group().CreateGroupSyncable(gt)
require.NoError(t, err)
groupTeams = append(groupTeams, groupTeam)
}
// Returns all the group teams
d1, err := ss.Group().GetAllGroupSyncablesByGroupId(group.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.Condition(t, func() bool { return len(d1) >= numGroupSyncables }, len(d1), ">=", numGroupSyncables)
for _, expectedGroupTeam := range groupTeams {
present := false
for _, dbGroupTeam := range d1 {
if dbGroupTeam.GroupId == expectedGroupTeam.GroupId && dbGroupTeam.SyncableId == expectedGroupTeam.SyncableId {
require.True(t, dbGroupTeam.SchemeAdmin)
present = true
break
}
}
require.True(t, present)
}
}
func testUpdateGroupSyncable(t *testing.T, ss store.Store) {
// Create Group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
// Create Team
t1 := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(t1)
require.NoError(t, nErr)
// New GroupSyncable, happy path
gt1 := model.NewGroupTeam(group.Id, team.Id, false)
d1, err := ss.Group().CreateGroupSyncable(gt1)
require.NoError(t, err)
// Update existing group team
gt1.AutoAdd = true
d2, err := ss.Group().UpdateGroupSyncable(gt1)
require.NoError(t, err)
require.True(t, d2.AutoAdd)
// Non-existent Group
gt2 := model.NewGroupTeam(model.NewId(), team.Id, false)
_, err = ss.Group().UpdateGroupSyncable(gt2)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
// Non-existent Team
gt3 := model.NewGroupTeam(group.Id, model.NewId(), false)
_, err = ss.Group().UpdateGroupSyncable(gt3)
require.True(t, errors.As(err, &nfErr))
// Cannot update CreateAt or DeleteAt
origCreateAt := d1.CreateAt
d1.CreateAt = model.GetMillis()
d1.AutoAdd = true
d3, err := ss.Group().UpdateGroupSyncable(d1)
require.NoError(t, err)
require.Equal(t, origCreateAt, d3.CreateAt)
// Cannot update DeleteAt to arbitrary value
d1.DeleteAt = 1
_, err = ss.Group().UpdateGroupSyncable(d1)
require.Error(t, err)
require.Contains(t, err.Error(), "DeleteAt should be 0 when updating")
// Can update DeleteAt to 0
d1.DeleteAt = 0
d4, err := ss.Group().UpdateGroupSyncable(d1)
require.NoError(t, err)
require.Zero(t, d4.DeleteAt)
}
func testDeleteGroupSyncable(t *testing.T, ss store.Store) {
// Create Group
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
// Create Team
t1 := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(t1)
require.NoError(t, nErr)
// Create GroupSyncable
gt1 := model.NewGroupTeam(group.Id, team.Id, false)
groupTeam, err := ss.Group().CreateGroupSyncable(gt1)
require.NoError(t, err)
// Non-existent Group
_, err = ss.Group().DeleteGroupSyncable(model.NewId(), groupTeam.SyncableId, model.GroupSyncableTypeTeam)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
// Non-existent Team
_, err = ss.Group().DeleteGroupSyncable(groupTeam.GroupId, model.NewId(), model.GroupSyncableTypeTeam)
require.True(t, errors.As(err, &nfErr))
// Happy path...
d1, err := ss.Group().DeleteGroupSyncable(groupTeam.GroupId, groupTeam.SyncableId, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.NotZero(t, d1.DeleteAt)
require.Equal(t, d1.GroupId, groupTeam.GroupId)
require.Equal(t, d1.SyncableId, groupTeam.SyncableId)
require.Equal(t, d1.AutoAdd, groupTeam.AutoAdd)
require.Equal(t, d1.CreateAt, groupTeam.CreateAt)
require.Condition(t, func() bool { return d1.UpdateAt >= groupTeam.UpdateAt }, d1.UpdateAt, ">=", groupTeam.UpdateAt)
// Record already deleted
_, err = ss.Group().DeleteGroupSyncable(d1.GroupId, d1.SyncableId, d1.Type)
require.Error(t, err)
var invErr *store.ErrInvalidInput
require.True(t, errors.As(err, &invErr))
}
func testTeamMembersToAdd(t *testing.T, ss store.Store) {
// Create Group
group, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "TeamMembersToAdd Test Group",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
})
require.NoError(t, err)
// Create User
user := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user, nErr := ss.User().Save(user)
require.NoError(t, nErr)
// Create GroupMember
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
// Create Team
team := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
// Create GroupTeam
syncable, err := ss.Group().CreateGroupSyncable(model.NewGroupTeam(group.Id, team.Id, true))
require.NoError(t, err)
// Time before syncable was created
teamMembers, err := ss.Group().TeamMembersToAdd(syncable.CreateAt-1, nil, false)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
require.Equal(t, user.Id, teamMembers[0].UserID)
require.Equal(t, team.Id, teamMembers[0].TeamID)
// Time after syncable was created
teamMembers, err = ss.Group().TeamMembersToAdd(syncable.CreateAt+1, nil, false)
require.NoError(t, err)
require.Empty(t, teamMembers)
// Delete and restore GroupMember should return result
_, err = ss.Group().DeleteMember(group.Id, user.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
teamMembers, err = ss.Group().TeamMembersToAdd(syncable.CreateAt+1, nil, false)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
pristineSyncable := *syncable
_, err = ss.Group().UpdateGroupSyncable(syncable)
require.NoError(t, err)
// Time before syncable was updated
teamMembers, err = ss.Group().TeamMembersToAdd(syncable.UpdateAt-1, nil, false)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
require.Equal(t, user.Id, teamMembers[0].UserID)
require.Equal(t, team.Id, teamMembers[0].TeamID)
// Time after syncable was updated
teamMembers, err = ss.Group().TeamMembersToAdd(syncable.UpdateAt+1, nil, false)
require.NoError(t, err)
require.Empty(t, teamMembers)
// Only includes if auto-add
syncable.AutoAdd = false
_, err = ss.Group().UpdateGroupSyncable(syncable)
require.NoError(t, err)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, teamMembers)
// reset state of syncable and verify
_, err = ss.Group().UpdateGroupSyncable(&pristineSyncable)
require.NoError(t, err)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
// No result if Group deleted
_, err = ss.Group().Delete(group.Id)
require.NoError(t, err)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, teamMembers)
// reset state of group and verify
group.DeleteAt = 0
_, err = ss.Group().Update(group)
require.NoError(t, err)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
// No result if Team deleted
team.DeleteAt = model.GetMillis()
team, nErr = ss.Team().Update(team)
require.NoError(t, nErr)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, teamMembers)
// reset state of team and verify
team.DeleteAt = 0
team, nErr = ss.Team().Update(team)
require.NoError(t, nErr)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
// No result if GroupTeam deleted
_, err = ss.Group().DeleteGroupSyncable(group.Id, team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, teamMembers)
// reset GroupTeam and verify
_, err = ss.Group().UpdateGroupSyncable(&pristineSyncable)
require.NoError(t, err)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
// No result if GroupMember deleted
_, err = ss.Group().DeleteMember(group.Id, user.Id)
require.NoError(t, err)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, teamMembers)
// restore group member and verify
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
// adding team membership stops returning result
_, nErr = ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: user.Id,
}, 999)
require.NoError(t, nErr)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, teamMembers)
// Leaving Team should still not return result
_, nErr = ss.Team().UpdateMember(&model.TeamMember{
TeamId: team.Id,
UserId: user.Id,
DeleteAt: model.GetMillis(),
})
require.NoError(t, nErr)
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, teamMembers)
// If includeRemovedMembers is set to true, removed members should be added back in
teamMembers, err = ss.Group().TeamMembersToAdd(0, nil, true)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
}
func testTeamMembersToAddSingleTeam(t *testing.T, ss store.Store) {
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "TeamMembersToAdd Test Group",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
})
require.NoError(t, err)
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "TeamMembersToAdd Test Group",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
})
require.NoError(t, err)
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, nErr := ss.User().Save(user1)
require.NoError(t, nErr)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, nErr = ss.User().Save(user2)
require.NoError(t, nErr)
user3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, nErr = ss.User().Save(user3)
require.NoError(t, nErr)
for _, user := range []*model.User{user1, user2} {
_, err = ss.Group().UpsertMember(group1.Id, user.Id)
require.NoError(t, err)
}
_, err = ss.Group().UpsertMember(group2.Id, user3.Id)
require.NoError(t, err)
team1 := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team1, nErr = ss.Team().Save(team1)
require.NoError(t, nErr)
team2 := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team2, nErr = ss.Team().Save(team2)
require.NoError(t, nErr)
_, err = ss.Group().CreateGroupSyncable(model.NewGroupTeam(group1.Id, team1.Id, true))
require.NoError(t, err)
_, err = ss.Group().CreateGroupSyncable(model.NewGroupTeam(group2.Id, team2.Id, true))
require.NoError(t, err)
teamMembers, err := ss.Group().TeamMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, teamMembers, 3)
teamMembers, err = ss.Group().TeamMembersToAdd(0, &team1.Id, false)
require.NoError(t, err)
require.Len(t, teamMembers, 2)
teamMembers, err = ss.Group().TeamMembersToAdd(0, &team2.Id, false)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
}
func testChannelMembersToAdd(t *testing.T, ss store.Store) {
// Create Group
group, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "ChannelMembersToAdd Test Group",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
})
require.NoError(t, err)
// Create User
user := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user, nErr := ss.User().Save(user)
require.NoError(t, nErr)
// Create GroupMember
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
// Create Channel
channel := &model.Channel{
TeamId: model.NewId(),
DisplayName: "A Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen, // Query does not look at type so this shouldn't matter.
}
channel, nErr = ss.Channel().Save(channel, 9999)
require.NoError(t, nErr)
// Create GroupChannel
syncable, err := ss.Group().CreateGroupSyncable(model.NewGroupChannel(group.Id, channel.Id, true))
require.NoError(t, err)
// Time before syncable was created
channelMembers, err := ss.Group().ChannelMembersToAdd(syncable.CreateAt-1, nil, false)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
require.Equal(t, user.Id, channelMembers[0].UserID)
require.Equal(t, channel.Id, channelMembers[0].ChannelID)
// Time after syncable was created
channelMembers, err = ss.Group().ChannelMembersToAdd(syncable.CreateAt+1, nil, false)
require.NoError(t, err)
require.Empty(t, channelMembers)
// Delete and restore GroupMember should return result
_, err = ss.Group().DeleteMember(group.Id, user.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
channelMembers, err = ss.Group().ChannelMembersToAdd(syncable.CreateAt+1, nil, false)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
pristineSyncable := *syncable
_, err = ss.Group().UpdateGroupSyncable(syncable)
require.NoError(t, err)
// Time before syncable was updated
channelMembers, err = ss.Group().ChannelMembersToAdd(syncable.UpdateAt-1, nil, false)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
require.Equal(t, user.Id, channelMembers[0].UserID)
require.Equal(t, channel.Id, channelMembers[0].ChannelID)
// Time after syncable was updated
channelMembers, err = ss.Group().ChannelMembersToAdd(syncable.UpdateAt+1, nil, false)
require.NoError(t, err)
require.Empty(t, channelMembers)
// Only includes if auto-add
syncable.AutoAdd = false
_, err = ss.Group().UpdateGroupSyncable(syncable)
require.NoError(t, err)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, channelMembers)
// reset state of syncable and verify
_, err = ss.Group().UpdateGroupSyncable(&pristineSyncable)
require.NoError(t, err)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
// No result if Group deleted
_, err = ss.Group().Delete(group.Id)
require.NoError(t, err)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, channelMembers)
// reset state of group and verify
group.DeleteAt = 0
_, err = ss.Group().Update(group)
require.NoError(t, err)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
// No result if Channel deleted
nErr = ss.Channel().Delete(channel.Id, model.GetMillis())
require.NoError(t, nErr)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, channelMembers)
// reset state of channel and verify
channel.DeleteAt = 0
_, nErr = ss.Channel().Update(channel)
require.NoError(t, nErr)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
// No result if GroupChannel deleted
_, err = ss.Group().DeleteGroupSyncable(group.Id, channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, channelMembers)
// reset GroupChannel and verify
_, err = ss.Group().UpdateGroupSyncable(&pristineSyncable)
require.NoError(t, err)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
// No result if GroupMember deleted
_, err = ss.Group().DeleteMember(group.Id, user.Id)
require.NoError(t, err)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, channelMembers)
// restore group member and verify
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
// Adding Channel (ChannelMemberHistory) should stop returning result
nErr = ss.ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis())
require.NoError(t, nErr)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, channelMembers)
// Leaving Channel (ChannelMemberHistory) should still not return result
nErr = ss.ChannelMemberHistory().LogLeaveEvent(user.Id, channel.Id, model.GetMillis())
require.NoError(t, nErr)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Empty(t, channelMembers)
// Purging ChannelMemberHistory re-returns the result
_, _, nErr = ss.ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies(
0, model.GetMillis()+1, 100, model.RetentionPolicyCursor{})
require.NoError(t, nErr)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
// If includeRemovedMembers is set to true, removed members should be added back in
nErr = ss.ChannelMemberHistory().LogLeaveEvent(user.Id, channel.Id, model.GetMillis())
require.NoError(t, nErr)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, true)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
}
func testChannelMembersToAddSingleChannel(t *testing.T, ss store.Store) {
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "TeamMembersToAdd Test Group",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
})
require.NoError(t, err)
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "TeamMembersToAdd Test Group",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
})
require.NoError(t, err)
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, nErr := ss.User().Save(user1)
require.NoError(t, nErr)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, nErr = ss.User().Save(user2)
require.NoError(t, nErr)
user3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, nErr = ss.User().Save(user3)
require.NoError(t, nErr)
for _, user := range []*model.User{user1, user2} {
_, err = ss.Group().UpsertMember(group1.Id, user.Id)
require.NoError(t, err)
}
_, err = ss.Group().UpsertMember(group2.Id, user3.Id)
require.NoError(t, err)
channel1 := &model.Channel{
DisplayName: "Name",
Name: "z-z-" + model.NewId() + "a",
Type: model.ChannelTypeOpen,
}
channel1, nErr = ss.Channel().Save(channel1, 999)
require.NoError(t, nErr)
channel2 := &model.Channel{
DisplayName: "Name",
Name: "z-z-" + model.NewId() + "a",
Type: model.ChannelTypeOpen,
}
channel2, nErr = ss.Channel().Save(channel2, 999)
require.NoError(t, nErr)
_, err = ss.Group().CreateGroupSyncable(model.NewGroupChannel(group1.Id, channel1.Id, true))
require.NoError(t, err)
_, err = ss.Group().CreateGroupSyncable(model.NewGroupChannel(group2.Id, channel2.Id, true))
require.NoError(t, err)
channelMembers, err := ss.Group().ChannelMembersToAdd(0, nil, false)
require.NoError(t, err)
require.GreaterOrEqual(t, len(channelMembers), 3)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, &channel1.Id, false)
require.NoError(t, err)
require.Len(t, channelMembers, 2)
channelMembers, err = ss.Group().ChannelMembersToAdd(0, &channel2.Id, false)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
}
func testTeamMembersToRemove(t *testing.T, ss store.Store) {
data := pendingMemberRemovalsDataSetup(t, ss)
// one result when both users are in the group (for user C)
teamMembers, err := ss.Group().TeamMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
require.Equal(t, data.UserC.Id, teamMembers[0].UserId)
_, err = ss.Group().DeleteMember(data.Group.Id, data.UserB.Id)
require.NoError(t, err)
// user b and c should now be returned
teamMembers, err = ss.Group().TeamMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, teamMembers, 2)
var userIDs []string
for _, item := range teamMembers {
userIDs = append(userIDs, item.UserId)
}
require.Contains(t, userIDs, data.UserB.Id)
require.Contains(t, userIDs, data.UserC.Id)
require.Equal(t, data.ConstrainedTeam.Id, teamMembers[0].TeamId)
require.Equal(t, data.ConstrainedTeam.Id, teamMembers[1].TeamId)
_, err = ss.Group().DeleteMember(data.Group.Id, data.UserA.Id)
require.NoError(t, err)
teamMembers, err = ss.Group().TeamMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, teamMembers, 3)
// Make one of them a bot
teamMembers, err = ss.Group().TeamMembersToRemove(nil)
require.NoError(t, err)
teamMember := teamMembers[0]
bot := &model.Bot{
UserId: teamMember.UserId,
Username: "un_" + model.NewId(),
DisplayName: "dn_" + model.NewId(),
OwnerId: teamMember.UserId,
}
bot, nErr := ss.Bot().Save(bot)
require.NoError(t, nErr)
// verify that bot is not returned in results
teamMembers, err = ss.Group().TeamMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, teamMembers, 2)
// delete the bot
nErr = ss.Bot().PermanentDelete(bot.UserId)
require.NoError(t, nErr)
// Should be back to 3 users
teamMembers, err = ss.Group().TeamMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, teamMembers, 3)
// add users back to groups
res := ss.Team().RemoveMember(data.ConstrainedTeam.Id, data.UserA.Id)
require.NoError(t, res)
res = ss.Team().RemoveMember(data.ConstrainedTeam.Id, data.UserB.Id)
require.NoError(t, res)
res = ss.Team().RemoveMember(data.ConstrainedTeam.Id, data.UserC.Id)
require.NoError(t, res)
nErr = ss.Channel().RemoveMember(data.ConstrainedChannel.Id, data.UserA.Id)
require.NoError(t, nErr)
nErr = ss.Channel().RemoveMember(data.ConstrainedChannel.Id, data.UserB.Id)
require.NoError(t, nErr)
nErr = ss.Channel().RemoveMember(data.ConstrainedChannel.Id, data.UserC.Id)
require.NoError(t, nErr)
}
func testTeamMembersToRemoveSingleTeam(t *testing.T, ss store.Store) {
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err := ss.User().Save(user1)
require.NoError(t, err)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
user3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, err = ss.User().Save(user3)
require.NoError(t, err)
team1 := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
GroupConstrained: model.NewBool(true),
}
team1, nErr := ss.Team().Save(team1)
require.NoError(t, nErr)
team2 := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
GroupConstrained: model.NewBool(true),
}
team2, nErr = ss.Team().Save(team2)
require.NoError(t, nErr)
for _, user := range []*model.User{user1, user2} {
_, nErr = ss.Team().SaveMember(&model.TeamMember{
TeamId: team1.Id,
UserId: user.Id,
}, 999)
require.NoError(t, nErr)
}
_, nErr = ss.Team().SaveMember(&model.TeamMember{
TeamId: team2.Id,
UserId: user3.Id,
}, 999)
require.NoError(t, nErr)
teamMembers, err := ss.Group().TeamMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, teamMembers, 3)
teamMembers, err = ss.Group().TeamMembersToRemove(&team1.Id)
require.NoError(t, err)
require.Len(t, teamMembers, 2)
teamMembers, err = ss.Group().TeamMembersToRemove(&team2.Id)
require.NoError(t, err)
require.Len(t, teamMembers, 1)
}
func testChannelMembersToRemove(t *testing.T, ss store.Store) {
data := pendingMemberRemovalsDataSetup(t, ss)
// one result when both users are in the group (for user C)
channelMembers, err := ss.Group().ChannelMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
require.Equal(t, data.UserC.Id, channelMembers[0].UserId)
_, err = ss.Group().DeleteMember(data.Group.Id, data.UserB.Id)
require.NoError(t, err)
// user b and c should now be returned
channelMembers, err = ss.Group().ChannelMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, channelMembers, 2)
var userIDs []string
for _, item := range channelMembers {
userIDs = append(userIDs, item.UserId)
}
require.Contains(t, userIDs, data.UserB.Id)
require.Contains(t, userIDs, data.UserC.Id)
require.Equal(t, data.ConstrainedChannel.Id, channelMembers[0].ChannelId)
require.Equal(t, data.ConstrainedChannel.Id, channelMembers[1].ChannelId)
_, err = ss.Group().DeleteMember(data.Group.Id, data.UserA.Id)
require.NoError(t, err)
channelMembers, err = ss.Group().ChannelMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, channelMembers, 3)
// Make one of them a bot
channelMembers, err = ss.Group().ChannelMembersToRemove(nil)
require.NoError(t, err)
channelMember := channelMembers[0]
bot := &model.Bot{
UserId: channelMember.UserId,
Username: "un_" + model.NewId(),
DisplayName: "dn_" + model.NewId(),
OwnerId: channelMember.UserId,
}
bot, nErr := ss.Bot().Save(bot)
require.NoError(t, nErr)
// verify that bot is not returned in results
channelMembers, err = ss.Group().ChannelMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, channelMembers, 2)
// delete the bot
nErr = ss.Bot().PermanentDelete(bot.UserId)
require.NoError(t, nErr)
// Should be back to 3 users
channelMembers, err = ss.Group().ChannelMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, channelMembers, 3)
// add users back to groups
res := ss.Team().RemoveMember(data.ConstrainedTeam.Id, data.UserA.Id)
require.NoError(t, res)
res = ss.Team().RemoveMember(data.ConstrainedTeam.Id, data.UserB.Id)
require.NoError(t, res)
res = ss.Team().RemoveMember(data.ConstrainedTeam.Id, data.UserC.Id)
require.NoError(t, res)
nErr = ss.Channel().RemoveMember(data.ConstrainedChannel.Id, data.UserA.Id)
require.NoError(t, nErr)
nErr = ss.Channel().RemoveMember(data.ConstrainedChannel.Id, data.UserB.Id)
require.NoError(t, nErr)
nErr = ss.Channel().RemoveMember(data.ConstrainedChannel.Id, data.UserC.Id)
require.NoError(t, nErr)
}
func testChannelMembersToRemoveSingleChannel(t *testing.T, ss store.Store) {
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err := ss.User().Save(user1)
require.NoError(t, err)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
user3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, err = ss.User().Save(user3)
require.NoError(t, err)
channel1 := &model.Channel{
DisplayName: "Name",
Name: "z-z-" + model.NewId() + "a",
Type: model.ChannelTypeOpen,
GroupConstrained: model.NewBool(true),
}
channel1, nErr := ss.Channel().Save(channel1, 999)
require.NoError(t, nErr)
channel2 := &model.Channel{
DisplayName: "Name",
Name: "z-z-" + model.NewId() + "a",
Type: model.ChannelTypeOpen,
GroupConstrained: model.NewBool(true),
}
channel2, nErr = ss.Channel().Save(channel2, 999)
require.NoError(t, nErr)
for _, user := range []*model.User{user1, user2} {
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel1.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
}
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel2.Id,
UserId: user3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
channelMembers, err := ss.Group().ChannelMembersToRemove(nil)
require.NoError(t, err)
require.Len(t, channelMembers, 3)
channelMembers, err = ss.Group().ChannelMembersToRemove(&channel1.Id)
require.NoError(t, err)
require.Len(t, channelMembers, 2)
channelMembers, err = ss.Group().ChannelMembersToRemove(&channel2.Id)
require.NoError(t, err)
require.Len(t, channelMembers, 1)
}
type removalsData struct {
UserA *model.User
UserB *model.User
UserC *model.User
ConstrainedChannel *model.Channel
UnconstrainedChannel *model.Channel
ConstrainedTeam *model.Team
UnconstrainedTeam *model.Team
Group *model.Group
}
func pendingMemberRemovalsDataSetup(t *testing.T, ss store.Store) *removalsData {
// create group
group, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "Pending[Channel|Team]MemberRemovals Test Group",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
})
require.NoError(t, err)
// create users
// userA will get removed from the group
userA := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
userA, nErr := ss.User().Save(userA)
require.NoError(t, nErr)
// userB will not get removed from the group
userB := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
userB, nErr = ss.User().Save(userB)
require.NoError(t, nErr)
// userC was never in the group
userC := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
userC, nErr = ss.User().Save(userC)
require.NoError(t, nErr)
// add users to group (but not userC)
_, err = ss.Group().UpsertMember(group.Id, userA.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group.Id, userB.Id)
require.NoError(t, err)
// create channels
channelConstrained := &model.Channel{
TeamId: model.NewId(),
DisplayName: "A Name",
Name: model.NewId(),
Type: model.ChannelTypePrivate,
GroupConstrained: model.NewBool(true),
}
channelConstrained, nErr = ss.Channel().Save(channelConstrained, 9999)
require.NoError(t, nErr)
channelUnconstrained := &model.Channel{
TeamId: model.NewId(),
DisplayName: "A Name",
Name: model.NewId(),
Type: model.ChannelTypePrivate,
}
channelUnconstrained, nErr = ss.Channel().Save(channelUnconstrained, 9999)
require.NoError(t, nErr)
// create teams
teamConstrained := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamInvite,
GroupConstrained: model.NewBool(true),
}
teamConstrained, nErr = ss.Team().Save(teamConstrained)
require.NoError(t, nErr)
teamUnconstrained := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid1",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamInvite,
}
teamUnconstrained, nErr = ss.Team().Save(teamUnconstrained)
require.NoError(t, nErr)
// create groupteams
_, err = ss.Group().CreateGroupSyncable(model.NewGroupTeam(group.Id, teamConstrained.Id, true))
require.NoError(t, err)
_, err = ss.Group().CreateGroupSyncable(model.NewGroupTeam(group.Id, teamUnconstrained.Id, true))
require.NoError(t, err)
// create groupchannels
_, err = ss.Group().CreateGroupSyncable(model.NewGroupChannel(group.Id, channelConstrained.Id, true))
require.NoError(t, err)
_, err = ss.Group().CreateGroupSyncable(model.NewGroupChannel(group.Id, channelUnconstrained.Id, true))
require.NoError(t, err)
// add users to teams
userIDTeamIDs := [][]string{
{userA.Id, teamConstrained.Id},
{userB.Id, teamConstrained.Id},
{userC.Id, teamConstrained.Id},
{userA.Id, teamUnconstrained.Id},
{userB.Id, teamUnconstrained.Id},
{userC.Id, teamUnconstrained.Id},
}
for _, item := range userIDTeamIDs {
_, nErr = ss.Team().SaveMember(&model.TeamMember{
UserId: item[0],
TeamId: item[1],
}, 99)
require.NoError(t, nErr)
}
// add users to channels
userIDChannelIDs := [][]string{
{userA.Id, channelConstrained.Id},
{userB.Id, channelConstrained.Id},
{userC.Id, channelConstrained.Id},
{userA.Id, channelUnconstrained.Id},
{userB.Id, channelUnconstrained.Id},
{userC.Id, channelUnconstrained.Id},
}
for _, item := range userIDChannelIDs {
_, err := ss.Channel().SaveMember(&model.ChannelMember{
UserId: item[0],
ChannelId: item[1],
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
}
return &removalsData{
UserA: userA,
UserB: userB,
UserC: userC,
ConstrainedChannel: channelConstrained,
UnconstrainedChannel: channelUnconstrained,
ConstrainedTeam: teamConstrained,
UnconstrainedTeam: teamUnconstrained,
Group: group,
}
}
func testGetGroupsByChannel(t *testing.T, ss store.Store) {
// Create Channel1
channel1 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Channel1",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
channel1, err := ss.Channel().Save(channel1, 9999)
require.NoError(t, err)
// Create Groups 1, 2 and a deleted group
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-1",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
})
require.NoError(t, err)
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-2",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: false,
})
require.NoError(t, err)
deletedGroup, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-deleted",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
DeleteAt: 1,
})
require.NoError(t, err)
// And associate them with Channel1
for _, g := range []*model.Group{group1, group2, deletedGroup} {
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: channel1.Id,
Type: model.GroupSyncableTypeChannel,
GroupId: g.Id,
})
require.NoError(t, err)
}
// Create Channel2
channel2 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Channel2",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
channel2, nErr := ss.Channel().Save(channel2, 9999)
require.NoError(t, nErr)
// Create Group3
group3, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-3",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
})
require.NoError(t, err)
// And associate it to Channel2
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: channel2.Id,
Type: model.GroupSyncableTypeChannel,
GroupId: group3.Id,
})
require.NoError(t, err)
// add members
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err := ss.User().Save(u1)
require.NoError(t, err)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err := ss.User().Save(u2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user2.Id)
require.NoError(t, err)
user2.DeleteAt = 1
_, err = ss.User().Update(user2, true)
require.NoError(t, err)
group1WithMemberCount := *group1
group1WithMemberCount.MemberCount = model.NewInt(1)
group2WithMemberCount := *group2
group2WithMemberCount.MemberCount = model.NewInt(0)
group1WSA := &model.GroupWithSchemeAdmin{Group: *group1, SchemeAdmin: model.NewBool(false)}
group2WSA := &model.GroupWithSchemeAdmin{Group: *group2, SchemeAdmin: model.NewBool(false)}
group3WSA := &model.GroupWithSchemeAdmin{Group: *group3, SchemeAdmin: model.NewBool(false)}
testCases := []struct {
Name string
ChannelId string
Page int
PerPage int
Result []*model.GroupWithSchemeAdmin
Opts model.GroupSearchOpts
TotalCount *int64
}{
{
Name: "Get the two Groups for Channel1",
ChannelId: channel1.Id,
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 60,
Result: []*model.GroupWithSchemeAdmin{group1WSA, group2WSA},
TotalCount: model.NewInt64(2),
},
{
Name: "Get first Group for Channel1 with page 0 with 1 element",
ChannelId: channel1.Id,
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 1,
Result: []*model.GroupWithSchemeAdmin{group1WSA},
},
{
Name: "Get second Group for Channel1 with page 1 with 1 element",
ChannelId: channel1.Id,
Opts: model.GroupSearchOpts{},
Page: 1,
PerPage: 1,
Result: []*model.GroupWithSchemeAdmin{group2WSA},
},
{
Name: "Get third Group for Channel2",
ChannelId: channel2.Id,
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 60,
Result: []*model.GroupWithSchemeAdmin{group3WSA},
},
{
Name: "Get empty Groups for a fake id",
ChannelId: model.NewId(),
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 60,
Result: []*model.GroupWithSchemeAdmin{},
TotalCount: model.NewInt64(0),
},
{
Name: "Get group matching name",
ChannelId: channel1.Id,
Opts: model.GroupSearchOpts{Q: string([]rune(*group1.Name)[2:10])}, // very low change of a name collision
Page: 0,
PerPage: 100,
Result: []*model.GroupWithSchemeAdmin{group1WSA},
TotalCount: model.NewInt64(1),
},
{
Name: "Get group matching display name",
ChannelId: channel1.Id,
Opts: model.GroupSearchOpts{Q: "rouP-1"},
Page: 0,
PerPage: 100,
Result: []*model.GroupWithSchemeAdmin{group1WSA},
TotalCount: model.NewInt64(1),
},
{
Name: "Get group matching multiple display names",
ChannelId: channel1.Id,
Opts: model.GroupSearchOpts{Q: "roUp-"},
Page: 0,
PerPage: 100,
Result: []*model.GroupWithSchemeAdmin{group1WSA, group2WSA},
TotalCount: model.NewInt64(2),
},
{
Name: "Include member counts",
ChannelId: channel1.Id,
Opts: model.GroupSearchOpts{IncludeMemberCount: true},
Page: 0,
PerPage: 2,
Result: []*model.GroupWithSchemeAdmin{
{Group: group1WithMemberCount, SchemeAdmin: model.NewBool(false)},
{Group: group2WithMemberCount, SchemeAdmin: model.NewBool(false)},
},
},
{
Name: "Include allow reference",
ChannelId: channel1.Id,
Opts: model.GroupSearchOpts{FilterAllowReference: true},
Page: 0,
PerPage: 100,
Result: []*model.GroupWithSchemeAdmin{group1WSA},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
if tc.Opts.PageOpts == nil {
tc.Opts.PageOpts = &model.PageOpts{}
}
tc.Opts.PageOpts.Page = tc.Page
tc.Opts.PageOpts.PerPage = tc.PerPage
groups, err := ss.Group().GetGroupsByChannel(tc.ChannelId, tc.Opts)
require.NoError(t, err)
require.ElementsMatch(t, tc.Result, groups)
if tc.TotalCount != nil {
var count int64
count, err = ss.Group().CountGroupsByChannel(tc.ChannelId, tc.Opts)
require.NoError(t, err)
require.Equal(t, *tc.TotalCount, count)
}
})
}
}
func testGetGroupsAssociatedToChannelsByTeam(t *testing.T, ss store.Store) {
// Create Team1
team1 := &model.Team{
DisplayName: "Team1",
Description: model.NewId(),
CompanyName: model.NewId(),
AllowOpenInvite: false,
InviteId: model.NewId(),
Name: NewTestId(),
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team1, errt := ss.Team().Save(team1)
require.NoError(t, errt)
// Create Channel1
channel1 := &model.Channel{
TeamId: team1.Id,
DisplayName: "Channel1",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
channel1, err := ss.Channel().Save(channel1, 9999)
require.NoError(t, err)
// Create Groups 1, 2 and a deleted group
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-1",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: false,
})
require.NoError(t, err)
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-2",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
})
require.NoError(t, err)
deletedGroup, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-deleted",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
DeleteAt: 1,
})
require.NoError(t, err)
// And associate them with Channel1
for _, g := range []*model.Group{group1, group2, deletedGroup} {
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: channel1.Id,
Type: model.GroupSyncableTypeChannel,
GroupId: g.Id,
})
require.NoError(t, err)
}
// Create Channel2
channel2 := &model.Channel{
TeamId: team1.Id,
DisplayName: "Channel2",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
channel2, err = ss.Channel().Save(channel2, 9999)
require.NoError(t, err)
// Create Group3
group3, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-3",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
})
require.NoError(t, err)
// And associate it to Channel2
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: channel2.Id,
Type: model.GroupSyncableTypeChannel,
GroupId: group3.Id,
})
require.NoError(t, err)
// add members
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err := ss.User().Save(u1)
require.NoError(t, err)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err := ss.User().Save(u2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user2.Id)
require.NoError(t, err)
user2.DeleteAt = 1
_, err = ss.User().Update(user2, true)
require.NoError(t, err)
group1WithMemberCount := *group1
group1WithMemberCount.MemberCount = model.NewInt(1)
group2WithMemberCount := *group2
group2WithMemberCount.MemberCount = model.NewInt(0)
group3WithMemberCount := *group3
group3WithMemberCount.MemberCount = model.NewInt(0)
group1WSA := &model.GroupWithSchemeAdmin{Group: *group1, SchemeAdmin: model.NewBool(false)}
group2WSA := &model.GroupWithSchemeAdmin{Group: *group2, SchemeAdmin: model.NewBool(false)}
group3WSA := &model.GroupWithSchemeAdmin{Group: *group3, SchemeAdmin: model.NewBool(false)}
testCases := []struct {
Name string
TeamId string
Page int
PerPage int
Result map[string][]*model.GroupWithSchemeAdmin
Opts model.GroupSearchOpts
}{
{
Name: "Get the groups for Channel1 and Channel2",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 60,
Result: map[string][]*model.GroupWithSchemeAdmin{channel1.Id: {group1WSA, group2WSA}, channel2.Id: {group3WSA}},
},
{
Name: "Get first Group for Channel1 with page 0 with 1 element",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 1,
Result: map[string][]*model.GroupWithSchemeAdmin{channel1.Id: {group1WSA}},
},
{
Name: "Get second Group for Channel1 with page 1 with 1 element",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{},
Page: 1,
PerPage: 1,
Result: map[string][]*model.GroupWithSchemeAdmin{channel1.Id: {group2WSA}},
},
{
Name: "Get empty Groups for a fake id",
TeamId: model.NewId(),
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 60,
Result: map[string][]*model.GroupWithSchemeAdmin{},
},
{
Name: "Get group matching name",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{Q: string([]rune(*group1.Name)[2:10])}, // very low chance of a name collision
Page: 0,
PerPage: 100,
Result: map[string][]*model.GroupWithSchemeAdmin{channel1.Id: {group1WSA}},
},
{
Name: "Get group matching display name",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{Q: "rouP-1"},
Page: 0,
PerPage: 100,
Result: map[string][]*model.GroupWithSchemeAdmin{channel1.Id: {group1WSA}},
},
{
Name: "Get group matching multiple display names",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{Q: "roUp-"},
Page: 0,
PerPage: 100,
Result: map[string][]*model.GroupWithSchemeAdmin{channel1.Id: {group1WSA, group2WSA}, channel2.Id: {group3WSA}},
},
{
Name: "Include member counts",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{IncludeMemberCount: true},
Page: 0,
PerPage: 10,
Result: map[string][]*model.GroupWithSchemeAdmin{
channel1.Id: {
{Group: group1WithMemberCount, SchemeAdmin: model.NewBool(false)},
{Group: group2WithMemberCount, SchemeAdmin: model.NewBool(false)},
},
channel2.Id: {
{Group: group3WithMemberCount, SchemeAdmin: model.NewBool(false)},
},
},
},
{
Name: "Include allow reference",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{FilterAllowReference: true},
Page: 0,
PerPage: 2,
Result: map[string][]*model.GroupWithSchemeAdmin{
channel1.Id: {
group2WSA,
},
channel2.Id: {
group3WSA,
},
},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
if tc.Opts.PageOpts == nil {
tc.Opts.PageOpts = &model.PageOpts{}
}
tc.Opts.PageOpts.Page = tc.Page
tc.Opts.PageOpts.PerPage = tc.PerPage
groups, err := ss.Group().GetGroupsAssociatedToChannelsByTeam(tc.TeamId, tc.Opts)
require.NoError(t, err)
assert.Equal(t, tc.Result, groups)
})
}
}
func testGetGroupsByTeam(t *testing.T, ss store.Store) {
// Create Team1
team1 := &model.Team{
DisplayName: "Team1",
Description: model.NewId(),
CompanyName: model.NewId(),
AllowOpenInvite: false,
InviteId: model.NewId(),
Name: NewTestId(),
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team1, err := ss.Team().Save(team1)
require.NoError(t, err)
// Create Groups 1, 2 and a deleted group
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-1",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: false,
})
require.NoError(t, err)
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-2",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
})
require.NoError(t, err)
deletedGroup, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-deleted",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
DeleteAt: 1,
})
require.NoError(t, err)
// And associate them with Team1
for _, g := range []*model.Group{group1, group2, deletedGroup} {
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: team1.Id,
Type: model.GroupSyncableTypeTeam,
GroupId: g.Id,
})
require.NoError(t, err)
}
// Create Team2
team2 := &model.Team{
DisplayName: "Team2",
Description: model.NewId(),
CompanyName: model.NewId(),
AllowOpenInvite: false,
InviteId: model.NewId(),
Name: NewTestId(),
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamInvite,
}
team2, err = ss.Team().Save(team2)
require.NoError(t, err)
// Create Group3
group3, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-3",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
})
require.NoError(t, err)
// And associate it to Team2
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: team2.Id,
Type: model.GroupSyncableTypeTeam,
GroupId: group3.Id,
})
require.NoError(t, err)
// add members
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err := ss.User().Save(u1)
require.NoError(t, err)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err := ss.User().Save(u2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user2.Id)
require.NoError(t, err)
user2.DeleteAt = 1
_, err = ss.User().Update(user2, true)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(deletedGroup.Id, user1.Id)
require.NoError(t, err)
group1WithMemberCount := *group1
group1WithMemberCount.MemberCount = model.NewInt(1)
group2WithMemberCount := *group2
group2WithMemberCount.MemberCount = model.NewInt(0)
group1WSA := &model.GroupWithSchemeAdmin{Group: *group1, SchemeAdmin: model.NewBool(false)}
group2WSA := &model.GroupWithSchemeAdmin{Group: *group2, SchemeAdmin: model.NewBool(false)}
group3WSA := &model.GroupWithSchemeAdmin{Group: *group3, SchemeAdmin: model.NewBool(false)}
testCases := []struct {
Name string
TeamId string
Page int
PerPage int
Opts model.GroupSearchOpts
Result []*model.GroupWithSchemeAdmin
TotalCount *int64
}{
{
Name: "Get the two Groups for Team1",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 60,
Result: []*model.GroupWithSchemeAdmin{group1WSA, group2WSA},
TotalCount: model.NewInt64(2),
},
{
Name: "Get first Group for Team1 with page 0 with 1 element",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 1,
Result: []*model.GroupWithSchemeAdmin{group1WSA},
},
{
Name: "Get second Group for Team1 with page 1 with 1 element",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{},
Page: 1,
PerPage: 1,
Result: []*model.GroupWithSchemeAdmin{group2WSA},
},
{
Name: "Get third Group for Team2",
TeamId: team2.Id,
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 60,
Result: []*model.GroupWithSchemeAdmin{group3WSA},
TotalCount: model.NewInt64(1),
},
{
Name: "Get empty Groups for a fake id",
TeamId: model.NewId(),
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 60,
Result: []*model.GroupWithSchemeAdmin{},
TotalCount: model.NewInt64(0),
},
{
Name: "Get group matching name",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{Q: string([]rune(*group1.Name)[2:10])}, // very low change of a name collision
Page: 0,
PerPage: 100,
Result: []*model.GroupWithSchemeAdmin{group1WSA},
TotalCount: model.NewInt64(1),
},
{
Name: "Get group matching display name",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{Q: "rouP-1"},
Page: 0,
PerPage: 100,
Result: []*model.GroupWithSchemeAdmin{group1WSA},
TotalCount: model.NewInt64(1),
},
{
Name: "Get group matching multiple display names",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{Q: "roUp-"},
Page: 0,
PerPage: 100,
Result: []*model.GroupWithSchemeAdmin{group1WSA, group2WSA},
TotalCount: model.NewInt64(2),
},
{
Name: "Include member counts",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{IncludeMemberCount: true},
Page: 0,
PerPage: 2,
Result: []*model.GroupWithSchemeAdmin{
{Group: group1WithMemberCount, SchemeAdmin: model.NewBool(false)},
{Group: group2WithMemberCount, SchemeAdmin: model.NewBool(false)},
},
},
{
Name: "Include allow reference",
TeamId: team1.Id,
Opts: model.GroupSearchOpts{FilterAllowReference: true},
Page: 0,
PerPage: 100,
Result: []*model.GroupWithSchemeAdmin{group2WSA},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
if tc.Opts.PageOpts == nil {
tc.Opts.PageOpts = &model.PageOpts{}
}
tc.Opts.PageOpts.Page = tc.Page
tc.Opts.PageOpts.PerPage = tc.PerPage
groups, err := ss.Group().GetGroupsByTeam(tc.TeamId, tc.Opts)
require.NoError(t, err)
require.ElementsMatch(t, tc.Result, groups)
if tc.TotalCount != nil {
var count int64
count, err = ss.Group().CountGroupsByTeam(tc.TeamId, tc.Opts)
require.NoError(t, err)
require.Equal(t, *tc.TotalCount, count)
}
})
}
}
func testGetGroups(t *testing.T, ss store.Store) {
// Create Team1
team1 := &model.Team{
DisplayName: "Team1",
Description: model.NewId(),
CompanyName: model.NewId(),
AllowOpenInvite: false,
InviteId: model.NewId(),
Name: NewTestId(),
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
GroupConstrained: model.NewBool(true),
}
team1, err := ss.Team().Save(team1)
require.NoError(t, err)
startCreateTime := team1.UpdateAt - 1
// Create Channel1
channel1 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Channel1",
Name: model.NewId(),
Type: model.ChannelTypePrivate,
}
channel1, nErr := ss.Channel().Save(channel1, 9999)
require.NoError(t, nErr)
// Create Groups 1 and 2
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: "group-1",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
})
require.NoError(t, err)
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId() + "-group-2"),
DisplayName: "group-2",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: false,
})
require.NoError(t, err)
deletedGroup, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId() + "-group-deleted"),
DisplayName: "group-deleted",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: false,
DeleteAt: 1,
})
require.NoError(t, err)
// And associate them with Team1
for _, g := range []*model.Group{group1, group2, deletedGroup} {
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: team1.Id,
Type: model.GroupSyncableTypeTeam,
GroupId: g.Id,
})
require.NoError(t, err)
}
// Create Team2
team2 := &model.Team{
DisplayName: "Team2",
Description: model.NewId(),
CompanyName: model.NewId(),
AllowOpenInvite: false,
InviteId: model.NewId(),
Name: NewTestId(),
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamInvite,
}
team2, err = ss.Team().Save(team2)
require.NoError(t, err)
// Create Channel2
channel2 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Channel2",
Name: model.NewId(),
Type: model.ChannelTypePrivate,
}
channel2, nErr = ss.Channel().Save(channel2, 9999)
require.NoError(t, nErr)
// Create Channel3
channel3 := &model.Channel{
TeamId: team1.Id,
DisplayName: "Channel3",
Name: model.NewId(),
Type: model.ChannelTypePrivate,
}
channel3, nErr = ss.Channel().Save(channel3, 9999)
require.NoError(t, nErr)
// Create Group3
group3, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId() + "-group-3"),
DisplayName: "group-3",
RemoteId: model.NewString(model.NewId()),
Source: model.GroupSourceLdap,
AllowReference: true,
})
require.NoError(t, err)
// And associate it to Team2
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: team2.Id,
Type: model.GroupSyncableTypeTeam,
GroupId: group3.Id,
})
require.NoError(t, err)
// And associate Group1 to Channel2
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: channel2.Id,
Type: model.GroupSyncableTypeChannel,
GroupId: group1.Id,
})
require.NoError(t, err)
// And associate Group2 and Group3 to Channel1
for _, g := range []*model.Group{group2, group3} {
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: channel1.Id,
Type: model.GroupSyncableTypeChannel,
GroupId: g.Id,
})
require.NoError(t, err)
}
// add members
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err := ss.User().Save(u1)
require.NoError(t, err)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err := ss.User().Save(u2)
require.NoError(t, err)
u3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, err := ss.User().Save(u3)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user2.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group2.Id, user2.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group2.Id, user3.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(deletedGroup.Id, user1.Id)
require.NoError(t, err)
m1 := model.ChannelMember{
ChannelId: channel1.Id,
UserId: user1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
_, err = ss.Channel().SaveMember(&m1)
require.NoError(t, err)
user2.DeleteAt = 1
u2Update, _ := ss.User().Update(user2, true)
group2NameSubstring := "group-2"
endCreateTime := u2Update.New.UpdateAt + 1
// Create Team3
team3 := &model.Team{
DisplayName: "Team3",
Description: model.NewId(),
CompanyName: model.NewId(),
AllowOpenInvite: false,
InviteId: model.NewId(),
Name: NewTestId(),
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamInvite,
}
team3, err = ss.Team().Save(team3)
require.NoError(t, err)
channel4 := &model.Channel{
TeamId: team3.Id,
DisplayName: "Channel4",
Name: model.NewId(),
Type: model.ChannelTypePrivate,
}
channel4, nErr = ss.Channel().Save(channel4, 9999)
require.NoError(t, nErr)
testCases := []struct {
Name string
Page int
PerPage int
Opts model.GroupSearchOpts
Resultf func([]*model.Group) bool
Restrictions *model.ViewUsersRestrictions
}{
{
Name: "Get all the Groups",
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 3,
Resultf: func(groups []*model.Group) bool { return len(groups) == 3 },
Restrictions: nil,
},
{
Name: "Get first Group with page 0 with 1 element",
Opts: model.GroupSearchOpts{},
Page: 0,
PerPage: 1,
Resultf: func(groups []*model.Group) bool { return len(groups) == 1 },
Restrictions: nil,
},
{
Name: "Get single result from page 1",
Opts: model.GroupSearchOpts{},
Page: 1,
PerPage: 1,
Resultf: func(groups []*model.Group) bool { return len(groups) == 1 },
Restrictions: nil,
},
{
Name: "Get multiple results from page 1",
Opts: model.GroupSearchOpts{},
Page: 1,
PerPage: 2,
Resultf: func(groups []*model.Group) bool { return len(groups) == 2 },
Restrictions: nil,
},
{
Name: "Get group matching name",
Opts: model.GroupSearchOpts{Q: group2NameSubstring},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
for _, g := range groups {
if !strings.Contains(*g.Name, group2NameSubstring) && !strings.Contains(g.DisplayName, group2NameSubstring) {
return false
}
}
return true
},
Restrictions: nil,
},
{
Name: "Get group matching display name",
Opts: model.GroupSearchOpts{Q: "rouP-3"},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
for _, g := range groups {
if !strings.Contains(strings.ToLower(g.DisplayName), "roup-3") {
return false
}
}
return true
},
Restrictions: nil,
},
{
Name: "Get group matching multiple display names",
Opts: model.GroupSearchOpts{Q: "groUp"},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
for _, g := range groups {
if !strings.Contains(strings.ToLower(g.DisplayName), "group") {
return false
}
}
return true
},
Restrictions: nil,
},
{
Name: "Include member counts",
Opts: model.GroupSearchOpts{IncludeMemberCount: true},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
for _, g := range groups {
if g.MemberCount == nil {
return false
}
if (g.Id == group1.Id || g.Id == group2.Id) && *g.MemberCount != 1 {
return false
}
if g.DeleteAt != 0 {
return false
}
}
return true
},
Restrictions: nil,
},
{
Name: "Include member counts with restrictions",
Opts: model.GroupSearchOpts{IncludeMemberCount: true},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
for _, g := range groups {
if g.MemberCount == nil {
return false
}
if g.Id == group1.Id && *g.MemberCount != 1 {
return false
}
if g.Id == group2.Id && *g.MemberCount != 0 {
return false
}
if g.DeleteAt != 0 {
return false
}
}
return true
},
Restrictions: &model.ViewUsersRestrictions{Channels: []string{channel1.Id}},
},
{
Name: "Not associated to team",
Opts: model.GroupSearchOpts{NotAssociatedToTeam: team2.Id},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
if len(groups) == 0 {
return false
}
for _, g := range groups {
if g.Id == group3.Id {
return false
}
if g.DeleteAt != 0 {
return false
}
}
return true
},
Restrictions: nil,
},
{
Name: "Not associated to other team",
Opts: model.GroupSearchOpts{NotAssociatedToTeam: team1.Id},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
if len(groups) == 0 {
return false
}
for _, g := range groups {
if g.Id == group1.Id || g.Id == group2.Id {
return false
}
if g.DeleteAt != 0 {
return false
}
}
return true
},
Restrictions: nil,
},
{
Name: "Include allow reference",
Opts: model.GroupSearchOpts{FilterAllowReference: true},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
if len(groups) == 0 {
return false
}
for _, g := range groups {
if !g.AllowReference {
return false
}
if g.DeleteAt != 0 {
return false
}
}
return true
},
Restrictions: nil,
},
{
Name: "Use Since return all",
Opts: model.GroupSearchOpts{FilterAllowReference: true, Since: startCreateTime},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
if len(groups) == 0 {
return false
}
for _, g := range groups {
if g.DeleteAt != 0 {
return false
}
}
return true
},
Restrictions: nil,
},
{
Name: "Use Since return none",
Opts: model.GroupSearchOpts{FilterAllowReference: true, Since: endCreateTime},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
return len(groups) == 0
},
Restrictions: nil,
},
{
Name: "Filter groups from group-constrained teams",
Opts: model.GroupSearchOpts{NotAssociatedToChannel: channel3.Id, FilterParentTeamPermitted: true},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
return len(groups) == 2 && groups[0].Id == group1.Id && groups[1].Id == group2.Id
},
Restrictions: nil,
},
{
Name: "Filter groups from group-constrained page 0",
Opts: model.GroupSearchOpts{NotAssociatedToChannel: channel3.Id, FilterParentTeamPermitted: true},
Page: 0,
PerPage: 1,
Resultf: func(groups []*model.Group) bool {
return groups[0].Id == group1.Id
},
Restrictions: nil,
},
{
Name: "Filter groups from group-constrained page 1",
Opts: model.GroupSearchOpts{NotAssociatedToChannel: channel3.Id, FilterParentTeamPermitted: true},
Page: 1,
PerPage: 1,
Resultf: func(groups []*model.Group) bool {
return groups[0].Id == group2.Id
},
Restrictions: nil,
},
{
Name: "Non-group constrained team with no associated groups still returns groups for the child channel",
Opts: model.GroupSearchOpts{NotAssociatedToChannel: channel4.Id, FilterParentTeamPermitted: true},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
return len(groups) > 0
},
Restrictions: nil,
},
{
Name: "Filter by group member",
Opts: model.GroupSearchOpts{FilterHasMember: user1.Id},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
return len(groups) == 1 && groups[0].Id == group1.Id
},
Restrictions: nil,
},
{
Name: "Filter by non-existent group member",
Opts: model.GroupSearchOpts{FilterHasMember: model.NewId()},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
return len(groups) == 0
},
Restrictions: nil,
},
{
Name: "Filter by non-member member",
Opts: model.GroupSearchOpts{FilterHasMember: user2.Id},
Page: 0,
PerPage: 100,
Resultf: func(groups []*model.Group) bool {
return len(groups) == 2
},
Restrictions: nil,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
groups, err := ss.Group().GetGroups(tc.Page, tc.PerPage, tc.Opts, tc.Restrictions)
require.NoError(t, err)
require.True(t, tc.Resultf(groups))
})
}
}
func testTeamMembersMinusGroupMembers(t *testing.T, ss store.Store) {
const numberOfGroups = 3
const numberOfUsers = 4
groups := []*model.Group{}
users := []*model.User{}
team := &model.Team{
DisplayName: model.NewId(),
Description: model.NewId(),
CompanyName: model.NewId(),
AllowOpenInvite: false,
InviteId: model.NewId(),
Name: NewTestId(),
Email: model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
GroupConstrained: model.NewBool(true),
}
team, err := ss.Team().Save(team)
require.NoError(t, err)
for i := 0; i < numberOfUsers; i++ {
user := &model.User{
Email: MakeEmail(),
Username: fmt.Sprintf("%d_%s", i, model.NewId()),
}
user, err = ss.User().Save(user)
require.NoError(t, err)
users = append(users, user)
trueOrFalse := int(math.Mod(float64(i), 2)) == 0
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: team.Id, UserId: user.Id, SchemeUser: trueOrFalse, SchemeAdmin: !trueOrFalse}, 999)
require.NoError(t, nErr)
}
// Extra user outside of the group member users.
user := &model.User{
Email: MakeEmail(),
Username: "99_" + model.NewId(),
}
user, err = ss.User().Save(user)
require.NoError(t, err)
users = append(users, user)
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: team.Id, UserId: user.Id, SchemeUser: true, SchemeAdmin: false}, 999)
require.NoError(t, nErr)
for i := 0; i < numberOfGroups; i++ {
group := &model.Group{
Name: model.NewString(fmt.Sprintf("n_%d_%s", i, model.NewId())),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(group)
require.NoError(t, err)
groups = append(groups, group)
}
sort.Slice(users, func(i, j int) bool {
return users[i].Username < users[j].Username
})
// Add even users to even group, and the inverse
for i := 0; i < numberOfUsers; i++ {
groupIndex := int(math.Mod(float64(i), 2))
_, err := ss.Group().UpsertMember(groups[groupIndex].Id, users[i].Id)
require.NoError(t, err)
// Add everyone to group 2
_, err = ss.Group().UpsertMember(groups[numberOfGroups-1].Id, users[i].Id)
require.NoError(t, err)
}
testCases := map[string]struct {
expectedUserIDs []string
expectedTotalCount int64
groupIDs []string
page int
perPage int
setup func()
teardown func()
}{
"No group IDs, all members": {
expectedUserIDs: []string{users[0].Id, users[1].Id, users[2].Id, users[3].Id, user.Id},
expectedTotalCount: numberOfUsers + 1,
groupIDs: []string{},
page: 0,
perPage: 100,
},
"All members, page 1": {
expectedUserIDs: []string{users[0].Id, users[1].Id, users[2].Id},
expectedTotalCount: numberOfUsers + 1,
groupIDs: []string{},
page: 0,
perPage: 3,
},
"All members, page 2": {
expectedUserIDs: []string{users[3].Id, users[4].Id},
expectedTotalCount: numberOfUsers + 1,
groupIDs: []string{},
page: 1,
perPage: 3,
},
"Group 1, even users would be removed": {
expectedUserIDs: []string{users[0].Id, users[2].Id, users[4].Id},
expectedTotalCount: 3,
groupIDs: []string{groups[1].Id},
page: 0,
perPage: 100,
},
"Group 0, odd users would be removed": {
expectedUserIDs: []string{users[1].Id, users[3].Id, users[4].Id},
expectedTotalCount: 3,
groupIDs: []string{groups[0].Id},
page: 0,
perPage: 100,
},
"All groups, no users would be removed": {
expectedUserIDs: []string{users[4].Id},
expectedTotalCount: 1,
groupIDs: []string{groups[0].Id, groups[1].Id},
page: 0,
perPage: 100,
},
}
mapUserIDs := func(users []*model.UserWithGroups) []string {
ids := []string{}
for _, user := range users {
ids = append(ids, user.Id)
}
return ids
}
for tcName, tc := range testCases {
t.Run(tcName, func(t *testing.T) {
if tc.setup != nil {
tc.setup()
}
if tc.teardown != nil {
defer tc.teardown()
}
actual, err := ss.Group().TeamMembersMinusGroupMembers(team.Id, tc.groupIDs, tc.page, tc.perPage)
require.NoError(t, err)
require.ElementsMatch(t, tc.expectedUserIDs, mapUserIDs(actual))
actualCount, err := ss.Group().CountTeamMembersMinusGroupMembers(team.Id, tc.groupIDs)
require.NoError(t, err)
require.Equal(t, tc.expectedTotalCount, actualCount)
})
}
}
func testChannelMembersMinusGroupMembers(t *testing.T, ss store.Store) {
const numberOfGroups = 3
const numberOfUsers = 4
groups := []*model.Group{}
users := []*model.User{}
channel := &model.Channel{
TeamId: model.NewId(),
DisplayName: "A Name",
Name: model.NewId(),
Type: model.ChannelTypePrivate,
GroupConstrained: model.NewBool(true),
}
channel, err := ss.Channel().Save(channel, 9999)
require.NoError(t, err)
for i := 0; i < numberOfUsers; i++ {
user := &model.User{
Email: MakeEmail(),
Username: fmt.Sprintf("%d_%s", i, model.NewId()),
}
user, err = ss.User().Save(user)
require.NoError(t, err)
users = append(users, user)
trueOrFalse := int(math.Mod(float64(i), 2)) == 0
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
SchemeUser: trueOrFalse,
SchemeAdmin: !trueOrFalse,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
}
// Extra user outside of the group member users.
user, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "99_" + model.NewId(),
})
require.NoError(t, err)
users = append(users, user)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
SchemeUser: true,
SchemeAdmin: false,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
for i := 0; i < numberOfGroups; i++ {
group := &model.Group{
Name: model.NewString(fmt.Sprintf("n_%d_%s", i, model.NewId())),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(group)
require.NoError(t, err)
groups = append(groups, group)
}
sort.Slice(users, func(i, j int) bool {
return users[i].Username < users[j].Username
})
// Add even users to even group, and the inverse
for i := 0; i < numberOfUsers; i++ {
groupIndex := int(math.Mod(float64(i), 2))
_, err := ss.Group().UpsertMember(groups[groupIndex].Id, users[i].Id)
require.NoError(t, err)
// Add everyone to group 2
_, err = ss.Group().UpsertMember(groups[numberOfGroups-1].Id, users[i].Id)
require.NoError(t, err)
}
testCases := map[string]struct {
expectedUserIDs []string
expectedTotalCount int64
groupIDs []string
page int
perPage int
setup func()
teardown func()
}{
"No group IDs, all members": {
expectedUserIDs: []string{users[0].Id, users[1].Id, users[2].Id, users[3].Id, users[4].Id},
expectedTotalCount: numberOfUsers + 1,
groupIDs: []string{},
page: 0,
perPage: 100,
},
"All members, page 1": {
expectedUserIDs: []string{users[0].Id, users[1].Id, users[2].Id},
expectedTotalCount: numberOfUsers + 1,
groupIDs: []string{},
page: 0,
perPage: 3,
},
"All members, page 2": {
expectedUserIDs: []string{users[3].Id, users[4].Id},
expectedTotalCount: numberOfUsers + 1,
groupIDs: []string{},
page: 1,
perPage: 3,
},
"Group 1, even users would be removed": {
expectedUserIDs: []string{users[0].Id, users[2].Id, users[4].Id},
expectedTotalCount: 3,
groupIDs: []string{groups[1].Id},
page: 0,
perPage: 100,
},
"Group 0, odd users would be removed": {
expectedUserIDs: []string{users[1].Id, users[3].Id, users[4].Id},
expectedTotalCount: 3,
groupIDs: []string{groups[0].Id},
page: 0,
perPage: 100,
},
"All groups, no users would be removed": {
expectedUserIDs: []string{users[4].Id},
expectedTotalCount: 1,
groupIDs: []string{groups[0].Id, groups[1].Id},
page: 0,
perPage: 100,
},
}
mapUserIDs := func(users []*model.UserWithGroups) []string {
ids := []string{}
for _, user := range users {
ids = append(ids, user.Id)
}
return ids
}
for tcName, tc := range testCases {
t.Run(tcName, func(t *testing.T) {
if tc.setup != nil {
tc.setup()
}
if tc.teardown != nil {
defer tc.teardown()
}
actual, err := ss.Group().ChannelMembersMinusGroupMembers(channel.Id, tc.groupIDs, tc.page, tc.perPage)
require.NoError(t, err)
require.ElementsMatch(t, tc.expectedUserIDs, mapUserIDs(actual))
actualCount, err := ss.Group().CountChannelMembersMinusGroupMembers(channel.Id, tc.groupIDs)
require.NoError(t, err)
require.Equal(t, tc.expectedTotalCount, actualCount)
})
}
}
func groupTestGetMemberCount(t *testing.T, ss store.Store) {
group := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(group)
require.NoError(t, err)
var user *model.User
var nErr error
for i := 0; i < 2; i++ {
user = &model.User{
Email: MakeEmail(),
Username: fmt.Sprintf("%d_%s", i, model.NewId()),
}
user, nErr = ss.User().Save(user)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
}
count, err := ss.Group().GetMemberCount(group.Id)
require.NoError(t, err)
require.Equal(t, int64(2), count)
user.DeleteAt = 1
_, nErr = ss.User().Update(user, true)
require.NoError(t, nErr)
count, err = ss.Group().GetMemberCount(group.Id)
require.NoError(t, err)
require.Equal(t, int64(1), count)
}
func groupTestAdminRoleGroupsForSyncableMemberChannel(t *testing.T, ss store.Store) {
user := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user, err := ss.User().Save(user)
require.NoError(t, err)
group1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group1, err = ss.Group().Create(group1)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user.Id)
require.NoError(t, err)
group2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group2, err = ss.Group().Create(group2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group2.Id, user.Id)
require.NoError(t, err)
channel := &model.Channel{
TeamId: model.NewId(),
DisplayName: "A Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
channel, nErr := ss.Channel().Save(channel, 9999)
require.NoError(t, nErr)
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: channel.Id,
Type: model.GroupSyncableTypeChannel,
GroupId: group1.Id,
SchemeAdmin: true,
})
require.NoError(t, err)
groupSyncable2, err := ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: channel.Id,
Type: model.GroupSyncableTypeChannel,
GroupId: group2.Id,
})
require.NoError(t, err)
// User is a member of both groups but only one is SchemeAdmin: true
actualGroupIDs, err := ss.Group().AdminRoleGroupsForSyncableMember(user.Id, channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
require.ElementsMatch(t, []string{group1.Id}, actualGroupIDs)
// Update the second group syncable to be SchemeAdmin: true and both groups should be returned
groupSyncable2.SchemeAdmin = true
_, err = ss.Group().UpdateGroupSyncable(groupSyncable2)
require.NoError(t, err)
actualGroupIDs, err = ss.Group().AdminRoleGroupsForSyncableMember(user.Id, channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
require.ElementsMatch(t, []string{group1.Id, group2.Id}, actualGroupIDs)
// Deleting membership from group should stop the group from being returned
_, err = ss.Group().DeleteMember(group1.Id, user.Id)
require.NoError(t, err)
actualGroupIDs, err = ss.Group().AdminRoleGroupsForSyncableMember(user.Id, channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
require.ElementsMatch(t, []string{group2.Id}, actualGroupIDs)
// Deleting group syncable should stop it being returned
_, err = ss.Group().DeleteGroupSyncable(group2.Id, channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
actualGroupIDs, err = ss.Group().AdminRoleGroupsForSyncableMember(user.Id, channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
require.ElementsMatch(t, []string{}, actualGroupIDs)
}
func groupTestAdminRoleGroupsForSyncableMemberTeam(t *testing.T, ss store.Store) {
user := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user, err := ss.User().Save(user)
require.NoError(t, err)
group1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group1, err = ss.Group().Create(group1)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user.Id)
require.NoError(t, err)
group2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group2, err = ss.Group().Create(group2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group2.Id, user.Id)
require.NoError(t, err)
team := &model.Team{
DisplayName: "A Name",
Name: NewTestId(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: team.Id,
Type: model.GroupSyncableTypeTeam,
GroupId: group1.Id,
SchemeAdmin: true,
})
require.NoError(t, err)
groupSyncable2, err := ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: team.Id,
Type: model.GroupSyncableTypeTeam,
GroupId: group2.Id,
})
require.NoError(t, err)
// User is a member of both groups but only one is SchemeAdmin: true
actualGroupIDs, err := ss.Group().AdminRoleGroupsForSyncableMember(user.Id, team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.ElementsMatch(t, []string{group1.Id}, actualGroupIDs)
// Update the second group syncable to be SchemeAdmin: true and both groups should be returned
groupSyncable2.SchemeAdmin = true
_, err = ss.Group().UpdateGroupSyncable(groupSyncable2)
require.NoError(t, err)
actualGroupIDs, err = ss.Group().AdminRoleGroupsForSyncableMember(user.Id, team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.ElementsMatch(t, []string{group1.Id, group2.Id}, actualGroupIDs)
// Deleting membership from group should stop the group from being returned
_, err = ss.Group().DeleteMember(group1.Id, user.Id)
require.NoError(t, err)
actualGroupIDs, err = ss.Group().AdminRoleGroupsForSyncableMember(user.Id, team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.ElementsMatch(t, []string{group2.Id}, actualGroupIDs)
// Deleting group syncable should stop it being returned
_, err = ss.Group().DeleteGroupSyncable(group2.Id, team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
actualGroupIDs, err = ss.Group().AdminRoleGroupsForSyncableMember(user.Id, team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.ElementsMatch(t, []string{}, actualGroupIDs)
}
func groupTestPermittedSyncableAdminsTeam(t *testing.T, ss store.Store) {
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err := ss.User().Save(user1)
require.NoError(t, err)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
user3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, err = ss.User().Save(user3)
require.NoError(t, err)
group1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group1, err = ss.Group().Create(group1)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user2.Id)
require.NoError(t, err)
group2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group2, err = ss.Group().Create(group2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group2.Id, user3.Id)
require.NoError(t, err)
team := &model.Team{
DisplayName: "A Name",
Name: NewTestId(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: team.Id,
Type: model.GroupSyncableTypeTeam,
GroupId: group1.Id,
SchemeAdmin: true,
})
require.NoError(t, err)
groupSyncable2, err := ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: team.Id,
Type: model.GroupSyncableTypeTeam,
GroupId: group2.Id,
SchemeAdmin: false,
})
require.NoError(t, err)
// group 1's users are returned because groupsyncable 2 has SchemeAdmin false.
actualUserIDs, err := ss.Group().PermittedSyncableAdmins(team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.ElementsMatch(t, []string{user1.Id, user2.Id}, actualUserIDs)
// update groupsyncable 2 to be SchemeAdmin true
groupSyncable2.SchemeAdmin = true
_, err = ss.Group().UpdateGroupSyncable(groupSyncable2)
require.NoError(t, err)
// group 2's users are now included in return value
actualUserIDs, err = ss.Group().PermittedSyncableAdmins(team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.ElementsMatch(t, []string{user1.Id, user2.Id, user3.Id}, actualUserIDs)
// deleted group member should not be included
ss.Group().DeleteMember(group1.Id, user2.Id)
require.NoError(t, err)
actualUserIDs, err = ss.Group().PermittedSyncableAdmins(team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.ElementsMatch(t, []string{user1.Id, user3.Id}, actualUserIDs)
// deleted group syncable no longer includes group members
_, err = ss.Group().DeleteGroupSyncable(group1.Id, team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
actualUserIDs, err = ss.Group().PermittedSyncableAdmins(team.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
require.ElementsMatch(t, []string{user3.Id}, actualUserIDs)
}
func groupTestPermittedSyncableAdminsChannel(t *testing.T, ss store.Store) {
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err := ss.User().Save(user1)
require.NoError(t, err)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
user3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, err = ss.User().Save(user3)
require.NoError(t, err)
group1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group1, err = ss.Group().Create(group1)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group1.Id, user2.Id)
require.NoError(t, err)
group2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
Description: model.NewId(),
RemoteId: model.NewString(model.NewId()),
}
group2, err = ss.Group().Create(group2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(group2.Id, user3.Id)
require.NoError(t, err)
channel := &model.Channel{
TeamId: model.NewId(),
DisplayName: "A Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
channel, nErr := ss.Channel().Save(channel, 9999)
require.NoError(t, nErr)
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: channel.Id,
Type: model.GroupSyncableTypeChannel,
GroupId: group1.Id,
SchemeAdmin: true,
})
require.NoError(t, err)
groupSyncable2, err := ss.Group().CreateGroupSyncable(&model.GroupSyncable{
AutoAdd: true,
SyncableId: channel.Id,
Type: model.GroupSyncableTypeChannel,
GroupId: group2.Id,
SchemeAdmin: false,
})
require.NoError(t, err)
// group 1's users are returned because groupsyncable 2 has SchemeAdmin false.
actualUserIDs, err := ss.Group().PermittedSyncableAdmins(channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
require.ElementsMatch(t, []string{user1.Id, user2.Id}, actualUserIDs)
// update groupsyncable 2 to be SchemeAdmin true
groupSyncable2.SchemeAdmin = true
_, err = ss.Group().UpdateGroupSyncable(groupSyncable2)
require.NoError(t, err)
// group 2's users are now included in return value
actualUserIDs, err = ss.Group().PermittedSyncableAdmins(channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
require.ElementsMatch(t, []string{user1.Id, user2.Id, user3.Id}, actualUserIDs)
// deleted group member should not be included
_, err = ss.Group().DeleteMember(group1.Id, user2.Id)
require.NoError(t, err)
actualUserIDs, err = ss.Group().PermittedSyncableAdmins(channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
require.ElementsMatch(t, []string{user1.Id, user3.Id}, actualUserIDs)
// deleted group syncable no longer includes group members
_, err = ss.Group().DeleteGroupSyncable(group1.Id, channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
actualUserIDs, err = ss.Group().PermittedSyncableAdmins(channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
require.ElementsMatch(t, []string{user3.Id}, actualUserIDs)
}
func groupTestpUpdateMembersRoleTeam(t *testing.T, ss store.Store) {
team := &model.Team{
DisplayName: "Name",
Description: "Some description",
CompanyName: "Some company name",
AllowOpenInvite: false,
InviteId: "inviteid0",
Name: "z-z-" + model.NewId() + "a",
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
team, err := ss.Team().Save(team)
require.NoError(t, err)
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err = ss.User().Save(user1)
require.NoError(t, err)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
user3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, err = ss.User().Save(user3)
require.NoError(t, err)
user4 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user4, err = ss.User().Save(user4)
require.NoError(t, err)
for _, user := range []*model.User{user1, user2, user3} {
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: team.Id, UserId: user.Id}, 9999)
require.NoError(t, nErr)
}
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: team.Id, UserId: user4.Id, SchemeGuest: true}, 9999)
require.NoError(t, nErr)
tests := []struct {
testName string
inUserIDs []string
targetSchemeAdminValue bool
}{
{
"Given users are admins",
[]string{user1.Id, user2.Id},
true,
},
{
"Given users are members",
[]string{user2.Id},
false,
},
{
"Non-given users are admins",
[]string{user2.Id},
false,
},
{
"Non-given users are members",
[]string{user2.Id},
false,
},
}
for _, tt := range tests {
t.Run(tt.testName, func(t *testing.T) {
err = ss.Team().UpdateMembersRole(team.Id, tt.inUserIDs)
require.NoError(t, err)
members, err := ss.Team().GetMembers(team.Id, 0, 100, nil)
require.NoError(t, err)
require.GreaterOrEqual(t, len(members), 4) // sanity check for team membership
for _, member := range members {
if utils.StringInSlice(member.UserId, tt.inUserIDs) {
require.True(t, member.SchemeAdmin)
} else {
require.False(t, member.SchemeAdmin)
}
// Ensure guest account never changes.
if member.UserId == user4.Id {
require.False(t, member.SchemeUser)
require.False(t, member.SchemeAdmin)
require.True(t, member.SchemeGuest)
}
}
})
}
}
func groupTestpUpdateMembersRoleChannel(t *testing.T, ss store.Store) {
channel := &model.Channel{
TeamId: model.NewId(),
DisplayName: "A Name",
Name: model.NewId(),
Type: model.ChannelTypeOpen, // Query does not look at type so this shouldn't matter.
}
channel, err := ss.Channel().Save(channel, 9999)
require.NoError(t, err)
user1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, err = ss.User().Save(user1)
require.NoError(t, err)
user2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
user3 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user3, err = ss.User().Save(user3)
require.NoError(t, err)
user4 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user4, err = ss.User().Save(user4)
require.NoError(t, err)
for _, user := range []*model.User{user1, user2, user3} {
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
}
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel.Id,
UserId: user4.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: true,
})
require.NoError(t, err)
tests := []struct {
testName string
inUserIDs []string
targetSchemeAdminValue bool
}{
{
"Given users are admins",
[]string{user1.Id, user2.Id},
true,
},
{
"Given users are members",
[]string{user2.Id},
false,
},
{
"Non-given users are admins",
[]string{user2.Id},
false,
},
{
"Non-given users are members",
[]string{user2.Id},
false,
},
}
for _, tt := range tests {
t.Run(tt.testName, func(t *testing.T) {
err = ss.Channel().UpdateMembersRole(channel.Id, tt.inUserIDs)
require.NoError(t, err)
members, err := ss.Channel().GetMembers(channel.Id, 0, 100)
require.NoError(t, err)
require.GreaterOrEqual(t, len(members), 4) // sanity check for channel membership
for _, member := range members {
if utils.StringInSlice(member.UserId, tt.inUserIDs) {
require.True(t, member.SchemeAdmin)
} else {
require.False(t, member.SchemeAdmin)
}
// Ensure guest account never changes.
if member.UserId == user4.Id {
require.False(t, member.SchemeUser)
require.False(t, member.SchemeAdmin)
require.True(t, member.SchemeGuest)
}
}
})
}
}
func groupTestGroupCount(t *testing.T, ss store.Store) {
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.NoError(t, err)
defer ss.Group().Delete(group1.Id)
count, err := ss.Group().GroupCount()
require.NoError(t, err)
require.GreaterOrEqual(t, count, int64(1))
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.NoError(t, err)
defer ss.Group().Delete(group2.Id)
countAfter, err := ss.Group().GroupCount()
require.NoError(t, err)
require.GreaterOrEqual(t, countAfter, count+1)
}
func groupTestGroupTeamCount(t *testing.T, ss store.Store) {
team, err := ss.Team().Save(&model.Team{
DisplayName: model.NewId(),
Description: model.NewId(),
AllowOpenInvite: false,
InviteId: model.NewId(),
Name: NewTestId(),
Email: model.NewId() + "@simulator.amazonses.com",
Type: model.TeamOpen,
})
require.NoError(t, err)
defer ss.Team().PermanentDelete(team.Id)
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.NoError(t, err)
defer ss.Group().Delete(group1.Id)
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.NoError(t, err)
defer ss.Group().Delete(group2.Id)
groupSyncable1, err := ss.Group().CreateGroupSyncable(model.NewGroupTeam(group1.Id, team.Id, false))
require.NoError(t, err)
defer ss.Group().DeleteGroupSyncable(groupSyncable1.GroupId, groupSyncable1.SyncableId, groupSyncable1.Type)
count, err := ss.Group().GroupTeamCount()
require.NoError(t, err)
require.GreaterOrEqual(t, count, int64(1))
groupSyncable2, err := ss.Group().CreateGroupSyncable(model.NewGroupTeam(group2.Id, team.Id, false))
require.NoError(t, err)
defer ss.Group().DeleteGroupSyncable(groupSyncable2.GroupId, groupSyncable2.SyncableId, groupSyncable2.Type)
countAfter, err := ss.Group().GroupTeamCount()
require.NoError(t, err)
require.GreaterOrEqual(t, countAfter, count+1)
}
func groupTestGroupChannelCount(t *testing.T, ss store.Store) {
channel, err := ss.Channel().Save(&model.Channel{
TeamId: model.NewId(),
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}, 9999)
require.NoError(t, err)
defer ss.Channel().Delete(channel.Id, 0)
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.NoError(t, err)
defer ss.Group().Delete(group1.Id)
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.NoError(t, err)
defer ss.Group().Delete(group2.Id)
groupSyncable1, err := ss.Group().CreateGroupSyncable(model.NewGroupChannel(group1.Id, channel.Id, false))
require.NoError(t, err)
defer ss.Group().DeleteGroupSyncable(groupSyncable1.GroupId, groupSyncable1.SyncableId, groupSyncable1.Type)
count, err := ss.Group().GroupChannelCount()
require.NoError(t, err)
require.GreaterOrEqual(t, count, int64(1))
groupSyncable2, err := ss.Group().CreateGroupSyncable(model.NewGroupChannel(group2.Id, channel.Id, false))
require.NoError(t, err)
defer ss.Group().DeleteGroupSyncable(groupSyncable2.GroupId, groupSyncable2.SyncableId, groupSyncable2.Type)
countAfter, err := ss.Group().GroupChannelCount()
require.NoError(t, err)
require.GreaterOrEqual(t, countAfter, count+1)
}
func groupTestGroupMemberCount(t *testing.T, ss store.Store) {
user := &model.User{
Email: fmt.Sprintf("test.%s@localhost", model.NewId()),
Username: model.NewId(),
}
user, err := ss.User().Save(user)
require.NoError(t, err)
user2 := &model.User{
Email: fmt.Sprintf("test.%s@localhost", model.NewId()),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
group, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.NoError(t, err)
defer ss.Group().Delete(group.Id)
member1, err := ss.Group().UpsertMember(group.Id, user.Id)
require.NoError(t, err)
defer ss.Group().DeleteMember(group.Id, member1.UserId)
count, err := ss.Group().GroupMemberCount()
require.NoError(t, err)
require.GreaterOrEqual(t, count, int64(1))
member2, err := ss.Group().UpsertMember(group.Id, user2.Id)
require.NoError(t, err)
defer ss.Group().DeleteMember(group.Id, member2.UserId)
countAfter, err := ss.Group().GroupMemberCount()
require.NoError(t, err)
require.GreaterOrEqual(t, countAfter, count+1)
}
func groupTestDistinctGroupMemberCount(t *testing.T, ss store.Store) {
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.NoError(t, err)
defer ss.Group().Delete(group1.Id)
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.NoError(t, err)
defer ss.Group().Delete(group2.Id)
user := &model.User{
Email: fmt.Sprintf("test.%s@localhost", model.NewId()),
Username: model.NewId(),
}
user, err = ss.User().Save(user)
require.NoError(t, err)
user2 := &model.User{
Email: fmt.Sprintf("test.%s@localhost", model.NewId()),
Username: model.NewId(),
}
user2, err = ss.User().Save(user2)
require.NoError(t, err)
member1, err := ss.Group().UpsertMember(group1.Id, user.Id)
require.NoError(t, err)
defer ss.Group().DeleteMember(group1.Id, member1.UserId)
count, err := ss.Group().GroupMemberCount()
require.NoError(t, err)
require.GreaterOrEqual(t, count, int64(1))
member2, err := ss.Group().UpsertMember(group1.Id, user2.Id)
require.NoError(t, err)
defer ss.Group().DeleteMember(group1.Id, member2.UserId)
countAfter1, err := ss.Group().GroupMemberCount()
require.NoError(t, err)
require.GreaterOrEqual(t, countAfter1, count+1)
member3, err := ss.Group().UpsertMember(group1.Id, member1.UserId)
require.NoError(t, err)
defer ss.Group().DeleteMember(group1.Id, member3.UserId)
countAfter2, err := ss.Group().GroupMemberCount()
require.NoError(t, err)
require.GreaterOrEqual(t, countAfter2, countAfter1)
}
func groupTestGroupCountWithAllowReference(t *testing.T, ss store.Store) {
initialCount, err := ss.Group().GroupCountWithAllowReference()
require.NoError(t, err)
group1, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
})
require.NoError(t, err)
defer ss.Group().Delete(group1.Id)
count, err := ss.Group().GroupCountWithAllowReference()
require.NoError(t, err)
require.Equal(t, count, initialCount)
group2, err := ss.Group().Create(&model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
AllowReference: true,
})
require.NoError(t, err)
defer ss.Group().Delete(group2.Id)
countAfter, err := ss.Group().GroupCountWithAllowReference()
require.NoError(t, err)
require.Greater(t, countAfter, count)
}
func groupTestGetMember(t *testing.T, ss store.Store) {
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, nErr := ss.User().Save(u2)
require.NoError(t, nErr)
_, err = ss.Group().UpsertMember(group.Id, user1.Id)
require.NoError(t, err)
member, err := ss.Group().GetMember(g1.Id, u1.Id)
require.NoError(t, err)
require.NotNil(t, member)
member, err = ss.Group().GetMember(g1.Id, user2.Id)
require.Error(t, err)
require.Nil(t, member)
}
func groupTestGetNonMemberUsersPage(t *testing.T, ss store.Store) {
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
group, err := ss.Group().Create(g1)
require.NoError(t, err)
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
_, nErr = ss.User().Save(u2)
require.NoError(t, nErr)
users, err := ss.Group().GetNonMemberUsersPage(group.Id, 0, 1000, nil)
require.NoError(t, err)
originalLen := len(users)
_, err = ss.Group().UpsertMember(group.Id, user1.Id)
require.NoError(t, err)
users, err = ss.Group().GetNonMemberUsersPage(group.Id, 0, 1000, nil)
require.NoError(t, err)
require.Len(t, users, originalLen-1)
users, err = ss.Group().GetNonMemberUsersPage(model.NewId(), 0, 1000, nil)
require.Error(t, err)
require.Nil(t, users)
}
func groupTestDistinctGroupMemberCountForSource(t *testing.T, ss store.Store) {
// get the before counts
customGroupCountBefore, err := ss.Group().DistinctGroupMemberCountForSource(model.GroupSourceCustom)
require.NoError(t, err)
ldapGroupCountBefore, err := ss.Group().DistinctGroupMemberCountForSource(model.GroupSourceLdap)
require.NoError(t, err)
// create 2 groups, 1 custom and 1 ldap
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceCustom,
RemoteId: model.NewString(model.NewId()),
}
customGroup, err := ss.Group().Create(g1)
require.NoError(t, err)
g2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
ldapGroup, err := ss.Group().Create(g2)
require.NoError(t, err)
// create a couple of users
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user1, nErr := ss.User().Save(u1)
require.NoError(t, nErr)
u2 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
user2, nErr := ss.User().Save(u2)
require.NoError(t, nErr)
// add both new users to both new groups
_, err = ss.Group().UpsertMember(customGroup.Id, user1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(ldapGroup.Id, user1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(customGroup.Id, user2.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(ldapGroup.Id, user2.Id)
require.NoError(t, err)
// remove one user from a group to ensure the 'where deleteat = 0' clause is working
_, err = ss.Group().DeleteMember(ldapGroup.Id, user1.Id)
require.NoError(t, err)
defer func() {
ss.Group().DeleteMember(ldapGroup.Id, user2.Id)
ss.Group().DeleteMember(customGroup.Id, user1.Id)
ss.Group().DeleteMember(customGroup.Id, user2.Id)
ss.Group().Delete(customGroup.Id)
ss.Group().Delete(ldapGroup.Id)
ss.User().PermanentDelete(user1.Id)
ss.User().PermanentDelete(user2.Id)
}()
customGroupCount, err := ss.Group().DistinctGroupMemberCountForSource(model.GroupSourceCustom)
require.NoError(t, err)
require.Equal(t, customGroupCountBefore+2, customGroupCount)
ldapGroupCount, err := ss.Group().DistinctGroupMemberCountForSource(model.GroupSourceLdap)
require.NoError(t, err)
require.Equal(t, ldapGroupCountBefore+1, ldapGroupCount)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestJobStore(t *testing.T, ss store.Store) {
t.Run("JobSaveGet", func(t *testing.T) { testJobSaveGet(t, ss) })
t.Run("JobGetAllByType", func(t *testing.T) { testJobGetAllByType(t, ss) })
t.Run("JobGetAllByTypeAndStatus", func(t *testing.T) { testJobGetAllByTypeAndStatus(t, ss) })
t.Run("JobGetAllByTypePage", func(t *testing.T) { testJobGetAllByTypePage(t, ss) })
t.Run("JobGetAllByTypesPage", func(t *testing.T) { testJobGetAllByTypesPage(t, ss) })
t.Run("JobGetAllPage", func(t *testing.T) { testJobGetAllPage(t, ss) })
t.Run("JobGetAllByStatus", func(t *testing.T) { testJobGetAllByStatus(t, ss) })
t.Run("GetNewestJobByStatusAndType", func(t *testing.T) { testJobStoreGetNewestJobByStatusAndType(t, ss) })
t.Run("GetNewestJobByStatusesAndType", func(t *testing.T) { testJobStoreGetNewestJobByStatusesAndType(t, ss) })
t.Run("GetCountByStatusAndType", func(t *testing.T) { testJobStoreGetCountByStatusAndType(t, ss) })
t.Run("JobUpdateOptimistically", func(t *testing.T) { testJobUpdateOptimistically(t, ss) })
t.Run("JobUpdateStatusUpdateStatusOptimistically", func(t *testing.T) { testJobUpdateStatusUpdateStatusOptimistically(t, ss) })
t.Run("JobDelete", func(t *testing.T) { testJobDelete(t, ss) })
t.Run("JobCleanup", func(t *testing.T) { testJobCleanup(t, ss) })
}
func testJobSaveGet(t *testing.T, ss store.Store) {
job := &model.Job{
Id: model.NewId(),
Type: model.NewId(),
Status: model.NewId(),
Data: map[string]string{
"Processed": "0",
"Total": "12345",
"LastProcessed": "abcd",
},
}
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
received, err := ss.Job().Get(job.Id)
require.NoError(t, err)
require.Equal(t, job.Id, received.Id, "received incorrect job after save")
require.Equal(t, "12345", received.Data["Total"])
}
func testJobGetAllByType(t *testing.T, ss store.Store) {
jobType := model.NewId()
jobs := []*model.Job{
{
Id: model.NewId(),
Type: jobType,
},
{
Id: model.NewId(),
Type: jobType,
},
{
Id: model.NewId(),
Type: model.NewId(),
},
}
for _, job := range jobs {
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
}
received, err := ss.Job().GetAllByType(jobType)
require.NoError(t, err)
require.Len(t, received, 2)
require.ElementsMatch(t, []string{jobs[0].Id, jobs[1].Id}, []string{received[0].Id, received[1].Id})
}
func testJobGetAllByTypeAndStatus(t *testing.T, ss store.Store) {
jobType := model.NewId()
jobs := []*model.Job{
{
Id: model.NewId(),
Type: jobType,
Status: model.JobStatusPending,
},
{
Id: model.NewId(),
Type: jobType,
Status: model.JobStatusPending,
},
}
for _, job := range jobs {
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
}
received, err := ss.Job().GetAllByTypeAndStatus(jobType, model.JobStatusPending)
require.NoError(t, err)
require.Len(t, received, 2)
require.ElementsMatch(t, []string{jobs[0].Id, jobs[1].Id}, []string{received[0].Id, received[1].Id})
}
func testJobGetAllByTypePage(t *testing.T, ss store.Store) {
jobType := model.NewId()
jobs := []*model.Job{
{
Id: model.NewId(),
Type: jobType,
CreateAt: 1000,
},
{
Id: model.NewId(),
Type: jobType,
CreateAt: 999,
},
{
Id: model.NewId(),
Type: jobType,
CreateAt: 1001,
},
{
Id: model.NewId(),
Type: model.NewId(),
CreateAt: 1002,
},
}
for _, job := range jobs {
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
}
received, err := ss.Job().GetAllByTypePage(jobType, 0, 2)
require.NoError(t, err)
require.Len(t, received, 2)
require.Equal(t, received[0].Id, jobs[2].Id, "should've received newest job first")
require.Equal(t, received[1].Id, jobs[0].Id, "should've received second newest job second")
received, err = ss.Job().GetAllByTypePage(jobType, 2, 2)
require.NoError(t, err)
require.Len(t, received, 1)
require.Equal(t, received[0].Id, jobs[1].Id, "should've received oldest job last")
}
func testJobGetAllByTypesPage(t *testing.T, ss store.Store) {
jobType := model.NewId()
jobType2 := model.NewId()
jobs := []*model.Job{
{
Id: model.NewId(),
Type: jobType,
CreateAt: 1000,
},
{
Id: model.NewId(),
Type: jobType,
CreateAt: 999,
},
{
Id: model.NewId(),
Type: jobType2,
CreateAt: 1001,
},
{
Id: model.NewId(),
Type: model.NewId(),
CreateAt: 1002,
},
}
for _, job := range jobs {
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
}
// test return all
jobTypes := []string{jobType, jobType2}
received, err := ss.Job().GetAllByTypesPage(jobTypes, 0, 4)
require.NoError(t, err)
require.Len(t, received, 3)
require.Equal(t, received[0].Id, jobs[2].Id, "should've received newest job first")
require.Equal(t, received[1].Id, jobs[0].Id, "should've received second newest job second")
// test paging
jobTypes = []string{jobType, jobType2}
received, err = ss.Job().GetAllByTypesPage(jobTypes, 0, 2)
require.NoError(t, err)
require.Len(t, received, 2)
require.Equal(t, received[0].Id, jobs[2].Id, "should've received newest job first")
require.Equal(t, received[1].Id, jobs[0].Id, "should've received second newest job second")
received, err = ss.Job().GetAllByTypesPage(jobTypes, 2, 2)
require.NoError(t, err)
require.Len(t, received, 1)
require.Equal(t, received[0].Id, jobs[1].Id, "should've received oldest job last")
}
func testJobGetAllPage(t *testing.T, ss store.Store) {
jobType := model.NewId()
createAtTime := model.GetMillis()
jobs := []*model.Job{
{
Id: model.NewId(),
Type: jobType,
CreateAt: createAtTime + 1,
},
{
Id: model.NewId(),
Type: jobType,
CreateAt: createAtTime,
},
{
Id: model.NewId(),
Type: jobType,
CreateAt: createAtTime + 2,
},
}
for _, job := range jobs {
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
}
received, err := ss.Job().GetAllPage(0, 2)
require.NoError(t, err)
require.Len(t, received, 2)
require.Equal(t, received[0].Id, jobs[2].Id, "should've received newest job first")
require.Equal(t, received[1].Id, jobs[0].Id, "should've received second newest job second")
received, err = ss.Job().GetAllPage(2, 2)
require.NoError(t, err)
require.NotEmpty(t, received)
require.Equal(t, received[0].Id, jobs[1].Id, "should've received oldest job last")
}
func testJobGetAllByStatus(t *testing.T, ss store.Store) {
jobType := model.NewId()
status := model.NewId()
jobs := []*model.Job{
{
Id: model.NewId(),
Type: jobType,
CreateAt: 1000,
Status: status,
Data: map[string]string{
"test": "data",
},
},
{
Id: model.NewId(),
Type: jobType,
CreateAt: 999,
Status: status,
},
{
Id: model.NewId(),
Type: jobType,
CreateAt: 1001,
Status: status,
},
{
Id: model.NewId(),
Type: jobType,
CreateAt: 1002,
Status: model.NewId(),
},
}
for _, job := range jobs {
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
}
received, err := ss.Job().GetAllByStatus(status)
require.NoError(t, err)
require.Len(t, received, 3)
require.Equal(t, received[0].Id, jobs[1].Id)
require.Equal(t, received[1].Id, jobs[0].Id)
require.Equal(t, received[2].Id, jobs[2].Id)
require.Equal(t, "data", received[1].Data["test"], "should've received job data field back as saved")
}
func testJobStoreGetNewestJobByStatusAndType(t *testing.T, ss store.Store) {
jobType1 := model.NewId()
jobType2 := model.NewId()
status1 := model.NewId()
status2 := model.NewId()
jobs := []*model.Job{
{
Id: model.NewId(),
Type: jobType1,
CreateAt: 1001,
Status: status1,
},
{
Id: model.NewId(),
Type: jobType1,
CreateAt: 1000,
Status: status1,
},
{
Id: model.NewId(),
Type: jobType2,
CreateAt: 1003,
Status: status1,
},
{
Id: model.NewId(),
Type: jobType1,
CreateAt: 1004,
Status: status2,
},
}
for _, job := range jobs {
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
}
received, err := ss.Job().GetNewestJobByStatusAndType(status1, jobType1)
assert.NoError(t, err)
assert.EqualValues(t, jobs[0].Id, received.Id)
received, err = ss.Job().GetNewestJobByStatusAndType(model.NewId(), model.NewId())
assert.Error(t, err)
var nfErr *store.ErrNotFound
assert.True(t, errors.As(err, &nfErr))
assert.Nil(t, received)
}
func testJobStoreGetNewestJobByStatusesAndType(t *testing.T, ss store.Store) {
jobType1 := model.NewId()
jobType2 := model.NewId()
status1 := model.NewId()
status2 := model.NewId()
jobs := []*model.Job{
{
Id: model.NewId(),
Type: jobType1,
CreateAt: 1001,
Status: status1,
},
{
Id: model.NewId(),
Type: jobType1,
CreateAt: 1000,
Status: status1,
},
{
Id: model.NewId(),
Type: jobType2,
CreateAt: 1003,
Status: status1,
},
{
Id: model.NewId(),
Type: jobType1,
CreateAt: 1004,
Status: status2,
},
}
for _, job := range jobs {
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
}
received, err := ss.Job().GetNewestJobByStatusesAndType([]string{status1, status2}, jobType1)
assert.NoError(t, err)
assert.EqualValues(t, jobs[3].Id, received.Id)
received, err = ss.Job().GetNewestJobByStatusesAndType([]string{model.NewId(), model.NewId()}, model.NewId())
assert.Error(t, err)
var nfErr *store.ErrNotFound
assert.True(t, errors.As(err, &nfErr))
assert.Nil(t, received)
received, err = ss.Job().GetNewestJobByStatusesAndType([]string{status2}, jobType2)
assert.Error(t, err)
assert.True(t, errors.As(err, &nfErr))
assert.Nil(t, received)
received, err = ss.Job().GetNewestJobByStatusesAndType([]string{status1}, jobType2)
assert.NoError(t, err)
assert.EqualValues(t, jobs[2].Id, received.Id)
received, err = ss.Job().GetNewestJobByStatusesAndType([]string{}, jobType1)
assert.Error(t, err)
assert.True(t, errors.As(err, &nfErr))
assert.Nil(t, received)
}
func testJobStoreGetCountByStatusAndType(t *testing.T, ss store.Store) {
jobType1 := model.NewId()
jobType2 := model.NewId()
status1 := model.NewId()
status2 := model.NewId()
jobs := []*model.Job{
{
Id: model.NewId(),
Type: jobType1,
CreateAt: 1000,
Status: status1,
},
{
Id: model.NewId(),
Type: jobType1,
CreateAt: 999,
Status: status1,
},
{
Id: model.NewId(),
Type: jobType2,
CreateAt: 1001,
Status: status1,
},
{
Id: model.NewId(),
Type: jobType1,
CreateAt: 1002,
Status: status2,
},
}
for _, job := range jobs {
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
}
count, err := ss.Job().GetCountByStatusAndType(status1, jobType1)
assert.NoError(t, err)
assert.EqualValues(t, 2, count)
count, err = ss.Job().GetCountByStatusAndType(status2, jobType2)
assert.NoError(t, err)
assert.EqualValues(t, 0, count)
count, err = ss.Job().GetCountByStatusAndType(status1, jobType2)
assert.NoError(t, err)
assert.EqualValues(t, 1, count)
count, err = ss.Job().GetCountByStatusAndType(status2, jobType1)
assert.NoError(t, err)
assert.EqualValues(t, 1, count)
}
func testJobUpdateOptimistically(t *testing.T, ss store.Store) {
job := &model.Job{
Id: model.NewId(),
Type: model.JobTypeDataRetention,
CreateAt: model.GetMillis(),
Status: model.JobStatusPending,
}
_, err := ss.Job().Save(job)
require.NoError(t, err)
defer ss.Job().Delete(job.Id)
job.LastActivityAt = model.GetMillis()
job.Status = model.JobStatusInProgress
job.Progress = 50
job.Data = map[string]string{
"Foo": "Bar",
}
updated, err := ss.Job().UpdateOptimistically(job, model.JobStatusSuccess)
require.False(t, err != nil && updated)
time.Sleep(2 * time.Millisecond)
updated, err = ss.Job().UpdateOptimistically(job, model.JobStatusPending)
require.NoError(t, err)
require.True(t, updated)
updatedJob, err := ss.Job().Get(job.Id)
require.NoError(t, err)
require.Equal(t, updatedJob.Type, job.Type)
require.Equal(t, updatedJob.CreateAt, job.CreateAt)
require.Equal(t, updatedJob.Status, job.Status)
require.Greater(t, updatedJob.LastActivityAt, job.LastActivityAt)
require.Equal(t, updatedJob.Progress, job.Progress)
require.Equal(t, updatedJob.Data["Foo"], job.Data["Foo"])
}
func testJobUpdateStatusUpdateStatusOptimistically(t *testing.T, ss store.Store) {
job := &model.Job{
Id: model.NewId(),
Type: model.JobTypeDataRetention,
CreateAt: model.GetMillis(),
Status: model.JobStatusSuccess,
}
var lastUpdateAt int64
received, err := ss.Job().Save(job)
require.NoError(t, err)
lastUpdateAt = received.LastActivityAt
defer ss.Job().Delete(job.Id)
time.Sleep(2 * time.Millisecond)
received, err = ss.Job().UpdateStatus(job.Id, model.JobStatusPending)
require.NoError(t, err)
require.Equal(t, model.JobStatusPending, received.Status)
require.Greater(t, received.LastActivityAt, lastUpdateAt)
lastUpdateAt = received.LastActivityAt
time.Sleep(2 * time.Millisecond)
updated, err := ss.Job().UpdateStatusOptimistically(job.Id, model.JobStatusInProgress, model.JobStatusSuccess)
require.NoError(t, err)
require.False(t, updated)
received, err = ss.Job().Get(job.Id)
require.NoError(t, err)
require.Equal(t, model.JobStatusPending, received.Status)
require.Equal(t, received.LastActivityAt, lastUpdateAt)
time.Sleep(2 * time.Millisecond)
updated, err = ss.Job().UpdateStatusOptimistically(job.Id, model.JobStatusPending, model.JobStatusInProgress)
require.NoError(t, err)
require.True(t, updated, "should have succeeded")
var startAtSet int64
received, err = ss.Job().Get(job.Id)
require.NoError(t, err)
require.Equal(t, model.JobStatusInProgress, received.Status)
require.NotEqual(t, 0, received.StartAt)
require.Greater(t, received.LastActivityAt, lastUpdateAt)
lastUpdateAt = received.LastActivityAt
startAtSet = received.StartAt
time.Sleep(2 * time.Millisecond)
updated, err = ss.Job().UpdateStatusOptimistically(job.Id, model.JobStatusInProgress, model.JobStatusSuccess)
require.NoError(t, err)
require.True(t, updated, "should have succeeded")
received, err = ss.Job().Get(job.Id)
require.NoError(t, err)
require.Equal(t, model.JobStatusSuccess, received.Status)
require.Equal(t, startAtSet, received.StartAt)
require.Greater(t, received.LastActivityAt, lastUpdateAt)
}
func testJobDelete(t *testing.T, ss store.Store) {
job, err := ss.Job().Save(&model.Job{Id: model.NewId()})
require.NoError(t, err)
_, err = ss.Job().Delete(job.Id)
assert.NoError(t, err)
}
func testJobCleanup(t *testing.T, ss store.Store) {
now := model.GetMillis()
ids := make([]string, 0, 10)
for i := 0; i < 10; i++ {
job, err := ss.Job().Save(&model.Job{
Id: model.NewId(),
CreateAt: now - int64(i),
Status: model.JobStatusPending,
})
require.NoError(t, err)
ids = append(ids, job.Id)
defer ss.Job().Delete(job.Id)
}
jobs, err := ss.Job().GetAllByStatus(model.JobStatusPending)
require.NoError(t, err)
assert.Len(t, jobs, 10)
err = ss.Job().Cleanup(now+1, 5)
require.NoError(t, err)
// Should not clean up pending jobs
jobs, err = ss.Job().GetAllByStatus(model.JobStatusPending)
require.NoError(t, err)
assert.Len(t, jobs, 10)
for _, id := range ids {
_, err = ss.Job().UpdateStatus(id, model.JobStatusSuccess)
require.NoError(t, err)
}
err = ss.Job().Cleanup(now+1, 5)
require.NoError(t, err)
// Should clean up now
jobs, err = ss.Job().GetAllByStatus(model.JobStatusSuccess)
require.NoError(t, err)
assert.Len(t, jobs, 0)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestLicenseStore(t *testing.T, ss store.Store) {
t.Run("Save", func(t *testing.T) { testLicenseStoreSave(t, ss) })
t.Run("Get", func(t *testing.T) { testLicenseStoreGet(t, ss) })
}
func testLicenseStoreSave(t *testing.T, ss store.Store) {
l1 := model.LicenseRecord{}
l1.Id = model.NewId()
l1.Bytes = "junk"
_, err := ss.License().Save(&l1)
require.NoError(t, err, "couldn't save license record")
_, err = ss.License().Save(&l1)
require.NoError(t, err, "shouldn't fail on trying to save existing license record")
l1.Id = ""
_, err = ss.License().Save(&l1)
require.Error(t, err, "should fail on invalid license")
}
func testLicenseStoreGet(t *testing.T, ss store.Store) {
l1 := model.LicenseRecord{}
l1.Id = model.NewId()
l1.Bytes = "junk"
_, err := ss.License().Save(&l1)
require.NoError(t, err)
record, err := ss.License().Get(l1.Id)
require.NoError(t, err, "couldn't get license")
require.Equal(t, record.Bytes, l1.Bytes, "license bytes didn't match")
_, err = ss.License().Get("missing")
require.Error(t, err, "should fail on get license")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"errors"
"testing"
"time"
"github.com/dyatlov/go-opengraph/opengraph"
"github.com/dyatlov/go-opengraph/opengraph/types/image"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
// These tests are ran on the same store instance, so this provides easier unique, valid timestamps
var linkMetadataTimestamp int64 = 1546300800000
func getNextLinkMetadataTimestamp() int64 {
linkMetadataTimestamp += int64(time.Hour) / (1000 * 1000)
return linkMetadataTimestamp
}
func TestLinkMetadataStore(t *testing.T, ss store.Store) {
t.Run("Save", func(t *testing.T) { testLinkMetadataStoreSave(t, ss) })
t.Run("Get", func(t *testing.T) { testLinkMetadataStoreGet(t, ss) })
t.Run("Types", func(t *testing.T) { testLinkMetadataStoreTypes(t, ss) })
}
func testLinkMetadataStoreSave(t *testing.T, ss store.Store) {
t.Run("should save item", func(t *testing.T) {
metadata := &model.LinkMetadata{
URL: "http://example.com",
Timestamp: getNextLinkMetadataTimestamp(),
Type: model.LinkMetadataTypeImage,
Data: &model.PostImage{},
}
linkMetadata, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
assert.Equal(t, *metadata, *linkMetadata)
})
t.Run("should fail to save invalid item", func(t *testing.T) {
metadata := &model.LinkMetadata{
URL: "",
Timestamp: 0,
Type: "garbage",
Data: nil,
}
_, err := ss.LinkMetadata().Save(metadata)
assert.Error(t, err)
})
t.Run("should save with duplicate URL and different timestamp", func(t *testing.T) {
metadata := &model.LinkMetadata{
URL: "http://example.com",
Timestamp: getNextLinkMetadataTimestamp(),
Type: model.LinkMetadataTypeImage,
Data: &model.PostImage{},
}
_, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
metadata.Timestamp = getNextLinkMetadataTimestamp()
linkMetadata, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
assert.Equal(t, *metadata, *linkMetadata)
})
t.Run("should save with duplicate timestamp and different URL", func(t *testing.T) {
metadata := &model.LinkMetadata{
URL: "http://example.com",
Timestamp: getNextLinkMetadataTimestamp(),
Type: model.LinkMetadataTypeImage,
Data: &model.PostImage{},
}
_, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
metadata.URL = "http://example.com/another/page"
linkMetadata, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
assert.Equal(t, *metadata, *linkMetadata)
})
t.Run("should save data with duplicate URL and timestamp", func(t *testing.T) {
metadata := &model.LinkMetadata{
URL: "http://example.com",
Timestamp: getNextLinkMetadataTimestamp(),
Type: model.LinkMetadataTypeImage,
Data: &model.PostImage{},
}
linkMetadata, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
assert.Equal(t, &model.PostImage{}, linkMetadata.Data)
newData := &model.PostImage{Height: 10, Width: 20}
metadata.Data = newData
linkMetadata, err = ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
assert.Equal(t, newData, linkMetadata.Data)
// Should return the original result, not the duplicate one
linkMetadata, err = ss.LinkMetadata().Get(metadata.URL, metadata.Timestamp)
require.NoError(t, err)
assert.Equal(t, newData, linkMetadata.Data)
})
}
func testLinkMetadataStoreGet(t *testing.T, ss store.Store) {
t.Run("should get value", func(t *testing.T) {
metadata := &model.LinkMetadata{
URL: "http://example.com",
Timestamp: getNextLinkMetadataTimestamp(),
Type: model.LinkMetadataTypeImage,
Data: &model.PostImage{},
}
_, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
linkMetadata, err := ss.LinkMetadata().Get(metadata.URL, metadata.Timestamp)
require.NoError(t, err)
require.IsType(t, metadata, linkMetadata)
assert.Equal(t, *metadata, *linkMetadata)
})
t.Run("should return not found with incorrect URL", func(t *testing.T) {
metadata := &model.LinkMetadata{
URL: "http://example.com",
Timestamp: getNextLinkMetadataTimestamp(),
Type: model.LinkMetadataTypeImage,
Data: &model.PostImage{},
}
_, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
_, err = ss.LinkMetadata().Get("http://example.com/another_page", metadata.Timestamp)
require.Error(t, err)
var nfErr *store.ErrNotFound
assert.True(t, errors.As(err, &nfErr))
})
t.Run("should return not found with incorrect timestamp", func(t *testing.T) {
metadata := &model.LinkMetadata{
URL: "http://example.com",
Timestamp: getNextLinkMetadataTimestamp(),
Type: model.LinkMetadataTypeImage,
Data: &model.PostImage{},
}
_, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
_, err = ss.LinkMetadata().Get(metadata.URL, getNextLinkMetadataTimestamp())
require.Error(t, err)
var nfErr *store.ErrNotFound
assert.True(t, errors.As(err, &nfErr))
})
}
func testLinkMetadataStoreTypes(t *testing.T, ss store.Store) {
t.Run("should save and get image metadata", func(t *testing.T) {
metadata := &model.LinkMetadata{
URL: "http://example.com",
Timestamp: getNextLinkMetadataTimestamp(),
Type: model.LinkMetadataTypeImage,
Data: &model.PostImage{
Width: 123,
Height: 456,
},
}
received, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
require.IsType(t, &model.PostImage{}, received.Data)
assert.Equal(t, *(metadata.Data.(*model.PostImage)), *(received.Data.(*model.PostImage)))
received, err = ss.LinkMetadata().Get(metadata.URL, metadata.Timestamp)
require.NoError(t, err)
require.IsType(t, &model.PostImage{}, received.Data)
assert.Equal(t, *(metadata.Data.(*model.PostImage)), *(received.Data.(*model.PostImage)))
})
t.Run("should save and get opengraph data", func(t *testing.T) {
og := &opengraph.OpenGraph{
URL: "http://example.com",
Images: []*image.Image{
{
URL: "http://example.com/image.png",
},
},
}
metadata := &model.LinkMetadata{
URL: "http://example.com",
Timestamp: getNextLinkMetadataTimestamp(),
Type: model.LinkMetadataTypeOpengraph,
Data: og,
}
received, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
require.IsType(t, &opengraph.OpenGraph{}, received.Data)
assert.Equal(t, *(metadata.Data.(*opengraph.OpenGraph)), *(received.Data.(*opengraph.OpenGraph)))
received, err = ss.LinkMetadata().Get(metadata.URL, metadata.Timestamp)
require.NoError(t, err)
require.IsType(t, &opengraph.OpenGraph{}, received.Data)
assert.Equal(t, *(metadata.Data.(*opengraph.OpenGraph)), *(received.Data.(*opengraph.OpenGraph)))
})
t.Run("should save and get nil", func(t *testing.T) {
metadata := &model.LinkMetadata{
URL: "http://example.com",
Timestamp: getNextLinkMetadataTimestamp(),
Type: model.LinkMetadataTypeNone,
Data: nil,
}
received, err := ss.LinkMetadata().Save(metadata)
require.NoError(t, err)
assert.Nil(t, received.Data)
received, err = ss.LinkMetadata().Get(metadata.URL, metadata.Timestamp)
require.NoError(t, err)
require.Nil(t, received.Data)
})
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// AuditStore is an autogenerated mock type for the AuditStore type
type AuditStore struct {
mock.Mock
}
// Get provides a mock function with given fields: user_id, offset, limit
func (_m *AuditStore) Get(user_id string, offset int, limit int) (model.Audits, error) {
ret := _m.Called(user_id, offset, limit)
var r0 model.Audits
if rf, ok := ret.Get(0).(func(string, int, int) model.Audits); ok {
r0 = rf(user_id, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Audits)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(user_id, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteByUser provides a mock function with given fields: userID
func (_m *AuditStore) PermanentDeleteByUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: audit
func (_m *AuditStore) Save(audit *model.Audit) error {
ret := _m.Called(audit)
var r0 error
if rf, ok := ret.Get(0).(func(*model.Audit) error); ok {
r0 = rf(audit)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// BotStore is an autogenerated mock type for the BotStore type
type BotStore struct {
mock.Mock
}
// Get provides a mock function with given fields: userID, includeDeleted
func (_m *BotStore) Get(userID string, includeDeleted bool) (*model.Bot, error) {
ret := _m.Called(userID, includeDeleted)
var r0 *model.Bot
if rf, ok := ret.Get(0).(func(string, bool) *model.Bot); ok {
r0 = rf(userID, includeDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Bot)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(userID, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields: options
func (_m *BotStore) GetAll(options *model.BotGetOptions) ([]*model.Bot, error) {
ret := _m.Called(options)
var r0 []*model.Bot
if rf, ok := ret.Get(0).(func(*model.BotGetOptions) []*model.Bot); ok {
r0 = rf(options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Bot)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.BotGetOptions) error); ok {
r1 = rf(options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDelete provides a mock function with given fields: userID
func (_m *BotStore) PermanentDelete(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: bot
func (_m *BotStore) Save(bot *model.Bot) (*model.Bot, error) {
ret := _m.Called(bot)
var r0 *model.Bot
if rf, ok := ret.Get(0).(func(*model.Bot) *model.Bot); ok {
r0 = rf(bot)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Bot)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Bot) error); ok {
r1 = rf(bot)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: bot
func (_m *BotStore) Update(bot *model.Bot) (*model.Bot, error) {
ret := _m.Called(bot)
var r0 *model.Bot
if rf, ok := ret.Get(0).(func(*model.Bot) *model.Bot); ok {
r0 = rf(bot)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Bot)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Bot) error); ok {
r1 = rf(bot)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// ChannelMemberHistoryStore is an autogenerated mock type for the ChannelMemberHistoryStore type
type ChannelMemberHistoryStore struct {
mock.Mock
}
// DeleteOrphanedRows provides a mock function with given fields: limit
func (_m *ChannelMemberHistoryStore) DeleteOrphanedRows(limit int) (int64, error) {
ret := _m.Called(limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int) int64); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelsLeftSince provides a mock function with given fields: userID, since
func (_m *ChannelMemberHistoryStore) GetChannelsLeftSince(userID string, since int64) ([]string, error) {
ret := _m.Called(userID, since)
var r0 []string
if rf, ok := ret.Get(0).(func(string, int64) []string); ok {
r0 = rf(userID, since)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64) error); ok {
r1 = rf(userID, since)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUsersInChannelDuring provides a mock function with given fields: startTime, endTime, channelID
func (_m *ChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelID string) ([]*model.ChannelMemberHistoryResult, error) {
ret := _m.Called(startTime, endTime, channelID)
var r0 []*model.ChannelMemberHistoryResult
if rf, ok := ret.Get(0).(func(int64, int64, string) []*model.ChannelMemberHistoryResult); ok {
r0 = rf(startTime, endTime, channelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelMemberHistoryResult)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, int64, string) error); ok {
r1 = rf(startTime, endTime, channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// LogJoinEvent provides a mock function with given fields: userID, channelID, joinTime
func (_m *ChannelMemberHistoryStore) LogJoinEvent(userID string, channelID string, joinTime int64) error {
ret := _m.Called(userID, channelID, joinTime)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, int64) error); ok {
r0 = rf(userID, channelID, joinTime)
} else {
r0 = ret.Error(0)
}
return r0
}
// LogLeaveEvent provides a mock function with given fields: userID, channelID, leaveTime
func (_m *ChannelMemberHistoryStore) LogLeaveEvent(userID string, channelID string, leaveTime int64) error {
ret := _m.Called(userID, channelID, leaveTime)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, int64) error); ok {
r0 = rf(userID, channelID, leaveTime)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteBatch provides a mock function with given fields: endTime, limit
func (_m *ChannelMemberHistoryStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
ret := _m.Called(endTime, limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64) int64); ok {
r0 = rf(endTime, limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, int64) error); ok {
r1 = rf(endTime, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteBatchForRetentionPolicies provides a mock function with given fields: now, globalPolicyEndTime, limit, cursor
func (_m *ChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
ret := _m.Called(now, globalPolicyEndTime, limit, cursor)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64, int64, model.RetentionPolicyCursor) int64); ok {
r0 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r0 = ret.Get(0).(int64)
}
var r1 model.RetentionPolicyCursor
if rf, ok := ret.Get(1).(func(int64, int64, int64, model.RetentionPolicyCursor) model.RetentionPolicyCursor); ok {
r1 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r1 = ret.Get(1).(model.RetentionPolicyCursor)
}
var r2 error
if rf, ok := ret.Get(2).(func(int64, int64, int64, model.RetentionPolicyCursor) error); ok {
r2 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
context "context"
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
store "github.com/mattermost/mattermost-server/v6/server/channels/store"
time "time"
)
// ChannelStore is an autogenerated mock type for the ChannelStore type
type ChannelStore struct {
mock.Mock
}
// AnalyticsDeletedTypeCount provides a mock function with given fields: teamID, channelType
func (_m *ChannelStore) AnalyticsDeletedTypeCount(teamID string, channelType model.ChannelType) (int64, error) {
ret := _m.Called(teamID, channelType)
var r0 int64
if rf, ok := ret.Get(0).(func(string, model.ChannelType) int64); ok {
r0 = rf(teamID, channelType)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.ChannelType) error); ok {
r1 = rf(teamID, channelType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AnalyticsTypeCount provides a mock function with given fields: teamID, channelType
func (_m *ChannelStore) AnalyticsTypeCount(teamID string, channelType model.ChannelType) (int64, error) {
ret := _m.Called(teamID, channelType)
var r0 int64
if rf, ok := ret.Get(0).(func(string, model.ChannelType) int64); ok {
r0 = rf(teamID, channelType)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.ChannelType) error); ok {
r1 = rf(teamID, channelType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Autocomplete provides a mock function with given fields: userID, term, includeDeleted, isGuest
func (_m *ChannelStore) Autocomplete(userID string, term string, includeDeleted bool, isGuest bool) (model.ChannelListWithTeamData, error) {
ret := _m.Called(userID, term, includeDeleted, isGuest)
var r0 model.ChannelListWithTeamData
if rf, ok := ret.Get(0).(func(string, string, bool, bool) model.ChannelListWithTeamData); ok {
r0 = rf(userID, term, includeDeleted, isGuest)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelListWithTeamData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, bool, bool) error); ok {
r1 = rf(userID, term, includeDeleted, isGuest)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AutocompleteInTeam provides a mock function with given fields: teamID, userID, term, includeDeleted, isGuest
func (_m *ChannelStore) AutocompleteInTeam(teamID string, userID string, term string, includeDeleted bool, isGuest bool) (model.ChannelList, error) {
ret := _m.Called(teamID, userID, term, includeDeleted, isGuest)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, string, string, bool, bool) model.ChannelList); ok {
r0 = rf(teamID, userID, term, includeDeleted, isGuest)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string, bool, bool) error); ok {
r1 = rf(teamID, userID, term, includeDeleted, isGuest)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AutocompleteInTeamForSearch provides a mock function with given fields: teamID, userID, term, includeDeleted
func (_m *ChannelStore) AutocompleteInTeamForSearch(teamID string, userID string, term string, includeDeleted bool) (model.ChannelList, error) {
ret := _m.Called(teamID, userID, term, includeDeleted)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, string, string, bool) model.ChannelList); ok {
r0 = rf(teamID, userID, term, includeDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string, bool) error); ok {
r1 = rf(teamID, userID, term, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ClearAllCustomRoleAssignments provides a mock function with given fields:
func (_m *ChannelStore) ClearAllCustomRoleAssignments() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// ClearCaches provides a mock function with given fields:
func (_m *ChannelStore) ClearCaches() {
_m.Called()
}
// ClearMembersForUserCache provides a mock function with given fields:
func (_m *ChannelStore) ClearMembersForUserCache() {
_m.Called()
}
// ClearSidebarOnTeamLeave provides a mock function with given fields: userID, teamID
func (_m *ChannelStore) ClearSidebarOnTeamLeave(userID string, teamID string) error {
ret := _m.Called(userID, teamID)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(userID, teamID)
} else {
r0 = ret.Error(0)
}
return r0
}
// CountPostsAfter provides a mock function with given fields: channelID, timestamp, userID
func (_m *ChannelStore) CountPostsAfter(channelID string, timestamp int64, userID string) (int, int, error) {
ret := _m.Called(channelID, timestamp, userID)
var r0 int
if rf, ok := ret.Get(0).(func(string, int64, string) int); ok {
r0 = rf(channelID, timestamp, userID)
} else {
r0 = ret.Get(0).(int)
}
var r1 int
if rf, ok := ret.Get(1).(func(string, int64, string) int); ok {
r1 = rf(channelID, timestamp, userID)
} else {
r1 = ret.Get(1).(int)
}
var r2 error
if rf, ok := ret.Get(2).(func(string, int64, string) error); ok {
r2 = rf(channelID, timestamp, userID)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// CountUrgentPostsAfter provides a mock function with given fields: channelID, timestamp, userID
func (_m *ChannelStore) CountUrgentPostsAfter(channelID string, timestamp int64, userID string) (int, error) {
ret := _m.Called(channelID, timestamp, userID)
var r0 int
if rf, ok := ret.Get(0).(func(string, int64, string) int); ok {
r0 = rf(channelID, timestamp, userID)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64, string) error); ok {
r1 = rf(channelID, timestamp, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateDirectChannel provides a mock function with given fields: userID, otherUserID, channelOptions
func (_m *ChannelStore) CreateDirectChannel(userID *model.User, otherUserID *model.User, channelOptions ...model.ChannelOption) (*model.Channel, error) {
_va := make([]interface{}, len(channelOptions))
for _i := range channelOptions {
_va[_i] = channelOptions[_i]
}
var _ca []interface{}
_ca = append(_ca, userID, otherUserID)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 *model.Channel
if rf, ok := ret.Get(0).(func(*model.User, *model.User, ...model.ChannelOption) *model.Channel); ok {
r0 = rf(userID, otherUserID, channelOptions...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.User, *model.User, ...model.ChannelOption) error); ok {
r1 = rf(userID, otherUserID, channelOptions...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateInitialSidebarCategories provides a mock function with given fields: userID, opts
func (_m *ChannelStore) CreateInitialSidebarCategories(userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
ret := _m.Called(userID, opts)
var r0 *model.OrderedSidebarCategories
if rf, ok := ret.Get(0).(func(string, *store.SidebarCategorySearchOpts) *model.OrderedSidebarCategories); ok {
r0 = rf(userID, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OrderedSidebarCategories)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, *store.SidebarCategorySearchOpts) error); ok {
r1 = rf(userID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateSidebarCategory provides a mock function with given fields: userID, teamID, newCategory
func (_m *ChannelStore) CreateSidebarCategory(userID string, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) {
ret := _m.Called(userID, teamID, newCategory)
var r0 *model.SidebarCategoryWithChannels
if rf, ok := ret.Get(0).(func(string, string, *model.SidebarCategoryWithChannels) *model.SidebarCategoryWithChannels); ok {
r0 = rf(userID, teamID, newCategory)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SidebarCategoryWithChannels)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *model.SidebarCategoryWithChannels) error); ok {
r1 = rf(userID, teamID, newCategory)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: channelID, timestamp
func (_m *ChannelStore) Delete(channelID string, timestamp int64) error {
ret := _m.Called(channelID, timestamp)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64) error); ok {
r0 = rf(channelID, timestamp)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteSidebarCategory provides a mock function with given fields: categoryID
func (_m *ChannelStore) DeleteSidebarCategory(categoryID string) error {
ret := _m.Called(categoryID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(categoryID)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteSidebarChannelsByPreferences provides a mock function with given fields: preferences
func (_m *ChannelStore) DeleteSidebarChannelsByPreferences(preferences model.Preferences) error {
ret := _m.Called(preferences)
var r0 error
if rf, ok := ret.Get(0).(func(model.Preferences) error); ok {
r0 = rf(preferences)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: id, allowFromCache
func (_m *ChannelStore) Get(id string, allowFromCache bool) (*model.Channel, error) {
ret := _m.Called(id, allowFromCache)
var r0 *model.Channel
if rf, ok := ret.Get(0).(func(string, bool) *model.Channel); ok {
r0 = rf(id, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(id, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields: teamID
func (_m *ChannelStore) GetAll(teamID string) ([]*model.Channel, error) {
ret := _m.Called(teamID)
var r0 []*model.Channel
if rf, ok := ret.Get(0).(func(string) []*model.Channel); ok {
r0 = rf(teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllChannelMembersById provides a mock function with given fields: id
func (_m *ChannelStore) GetAllChannelMembersById(id string) ([]string, error) {
ret := _m.Called(id)
var r0 []string
if rf, ok := ret.Get(0).(func(string) []string); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllChannelMembersForUser provides a mock function with given fields: userID, allowFromCache, includeDeleted
func (_m *ChannelStore) GetAllChannelMembersForUser(userID string, allowFromCache bool, includeDeleted bool) (map[string]string, error) {
ret := _m.Called(userID, allowFromCache, includeDeleted)
var r0 map[string]string
if rf, ok := ret.Get(0).(func(string, bool, bool) map[string]string); ok {
r0 = rf(userID, allowFromCache, includeDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool, bool) error); ok {
r1 = rf(userID, allowFromCache, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllChannelMembersNotifyPropsForChannel provides a mock function with given fields: channelID, allowFromCache
func (_m *ChannelStore) GetAllChannelMembersNotifyPropsForChannel(channelID string, allowFromCache bool) (map[string]model.StringMap, error) {
ret := _m.Called(channelID, allowFromCache)
var r0 map[string]model.StringMap
if rf, ok := ret.Get(0).(func(string, bool) map[string]model.StringMap); ok {
r0 = rf(channelID, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]model.StringMap)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(channelID, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllChannels provides a mock function with given fields: page, perPage, opts
func (_m *ChannelStore) GetAllChannels(page int, perPage int, opts store.ChannelSearchOpts) (model.ChannelListWithTeamData, error) {
ret := _m.Called(page, perPage, opts)
var r0 model.ChannelListWithTeamData
if rf, ok := ret.Get(0).(func(int, int, store.ChannelSearchOpts) model.ChannelListWithTeamData); ok {
r0 = rf(page, perPage, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelListWithTeamData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int, store.ChannelSearchOpts) error); ok {
r1 = rf(page, perPage, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllChannelsCount provides a mock function with given fields: opts
func (_m *ChannelStore) GetAllChannelsCount(opts store.ChannelSearchOpts) (int64, error) {
ret := _m.Called(opts)
var r0 int64
if rf, ok := ret.Get(0).(func(store.ChannelSearchOpts) int64); ok {
r0 = rf(opts)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(store.ChannelSearchOpts) error); ok {
r1 = rf(opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllChannelsForExportAfter provides a mock function with given fields: limit, afterID
func (_m *ChannelStore) GetAllChannelsForExportAfter(limit int, afterID string) ([]*model.ChannelForExport, error) {
ret := _m.Called(limit, afterID)
var r0 []*model.ChannelForExport
if rf, ok := ret.Get(0).(func(int, string) []*model.ChannelForExport); ok {
r0 = rf(limit, afterID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelForExport)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, string) error); ok {
r1 = rf(limit, afterID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllDirectChannelsForExportAfter provides a mock function with given fields: limit, afterID
func (_m *ChannelStore) GetAllDirectChannelsForExportAfter(limit int, afterID string) ([]*model.DirectChannelForExport, error) {
ret := _m.Called(limit, afterID)
var r0 []*model.DirectChannelForExport
if rf, ok := ret.Get(0).(func(int, string) []*model.DirectChannelForExport); ok {
r0 = rf(limit, afterID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.DirectChannelForExport)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, string) error); ok {
r1 = rf(limit, afterID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByName provides a mock function with given fields: team_id, name, allowFromCache
func (_m *ChannelStore) GetByName(team_id string, name string, allowFromCache bool) (*model.Channel, error) {
ret := _m.Called(team_id, name, allowFromCache)
var r0 *model.Channel
if rf, ok := ret.Get(0).(func(string, string, bool) *model.Channel); ok {
r0 = rf(team_id, name, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, bool) error); ok {
r1 = rf(team_id, name, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByNameIncludeDeleted provides a mock function with given fields: team_id, name, allowFromCache
func (_m *ChannelStore) GetByNameIncludeDeleted(team_id string, name string, allowFromCache bool) (*model.Channel, error) {
ret := _m.Called(team_id, name, allowFromCache)
var r0 *model.Channel
if rf, ok := ret.Get(0).(func(string, string, bool) *model.Channel); ok {
r0 = rf(team_id, name, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, bool) error); ok {
r1 = rf(team_id, name, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByNames provides a mock function with given fields: team_id, names, allowFromCache
func (_m *ChannelStore) GetByNames(team_id string, names []string, allowFromCache bool) ([]*model.Channel, error) {
ret := _m.Called(team_id, names, allowFromCache)
var r0 []*model.Channel
if rf, ok := ret.Get(0).(func(string, []string, bool) []*model.Channel); ok {
r0 = rf(team_id, names, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string, bool) error); ok {
r1 = rf(team_id, names, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelCounts provides a mock function with given fields: teamID, userID
func (_m *ChannelStore) GetChannelCounts(teamID string, userID string) (*model.ChannelCounts, error) {
ret := _m.Called(teamID, userID)
var r0 *model.ChannelCounts
if rf, ok := ret.Get(0).(func(string, string) *model.ChannelCounts); ok {
r0 = rf(teamID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ChannelCounts)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(teamID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelMembersForExport provides a mock function with given fields: userID, teamID
func (_m *ChannelStore) GetChannelMembersForExport(userID string, teamID string) ([]*model.ChannelMemberForExport, error) {
ret := _m.Called(userID, teamID)
var r0 []*model.ChannelMemberForExport
if rf, ok := ret.Get(0).(func(string, string) []*model.ChannelMemberForExport); ok {
r0 = rf(userID, teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelMemberForExport)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelMembersTimezones provides a mock function with given fields: channelID
func (_m *ChannelStore) GetChannelMembersTimezones(channelID string) ([]model.StringMap, error) {
ret := _m.Called(channelID)
var r0 []model.StringMap
if rf, ok := ret.Get(0).(func(string) []model.StringMap); ok {
r0 = rf(channelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]model.StringMap)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelUnread provides a mock function with given fields: channelID, userID
func (_m *ChannelStore) GetChannelUnread(channelID string, userID string) (*model.ChannelUnread, error) {
ret := _m.Called(channelID, userID)
var r0 *model.ChannelUnread
if rf, ok := ret.Get(0).(func(string, string) *model.ChannelUnread); ok {
r0 = rf(channelID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ChannelUnread)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(channelID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannels provides a mock function with given fields: teamID, userID, opts
func (_m *ChannelStore) GetChannels(teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, error) {
ret := _m.Called(teamID, userID, opts)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, string, *model.ChannelSearchOpts) model.ChannelList); ok {
r0 = rf(teamID, userID, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *model.ChannelSearchOpts) error); ok {
r1 = rf(teamID, userID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelsBatchForIndexing provides a mock function with given fields: startTime, startChannelID, limit
func (_m *ChannelStore) GetChannelsBatchForIndexing(startTime int64, startChannelID string, limit int) ([]*model.Channel, error) {
ret := _m.Called(startTime, startChannelID, limit)
var r0 []*model.Channel
if rf, ok := ret.Get(0).(func(int64, string, int) []*model.Channel); ok {
r0 = rf(startTime, startChannelID, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, string, int) error); ok {
r1 = rf(startTime, startChannelID, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelsByIds provides a mock function with given fields: channelIds, includeDeleted
func (_m *ChannelStore) GetChannelsByIds(channelIds []string, includeDeleted bool) ([]*model.Channel, error) {
ret := _m.Called(channelIds, includeDeleted)
var r0 []*model.Channel
if rf, ok := ret.Get(0).(func([]string, bool) []*model.Channel); ok {
r0 = rf(channelIds, includeDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string, bool) error); ok {
r1 = rf(channelIds, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelsByScheme provides a mock function with given fields: schemeID, offset, limit
func (_m *ChannelStore) GetChannelsByScheme(schemeID string, offset int, limit int) (model.ChannelList, error) {
ret := _m.Called(schemeID, offset, limit)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, int, int) model.ChannelList); ok {
r0 = rf(schemeID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(schemeID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelsByUser provides a mock function with given fields: userID, includeDeleted, lastDeleteAt, pageSize, fromChannelID
func (_m *ChannelStore) GetChannelsByUser(userID string, includeDeleted bool, lastDeleteAt int, pageSize int, fromChannelID string) (model.ChannelList, error) {
ret := _m.Called(userID, includeDeleted, lastDeleteAt, pageSize, fromChannelID)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, bool, int, int, string) model.ChannelList); ok {
r0 = rf(userID, includeDeleted, lastDeleteAt, pageSize, fromChannelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool, int, int, string) error); ok {
r1 = rf(userID, includeDeleted, lastDeleteAt, pageSize, fromChannelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelsWithCursor provides a mock function with given fields: teamId, userId, opts, afterChannelID
func (_m *ChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
ret := _m.Called(teamId, userId, opts, afterChannelID)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, string, *model.ChannelSearchOpts, string) model.ChannelList); ok {
r0 = rf(teamId, userId, opts, afterChannelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *model.ChannelSearchOpts, string) error); ok {
r1 = rf(teamId, userId, opts, afterChannelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelsWithTeamDataByIds provides a mock function with given fields: channelIds, includeDeleted
func (_m *ChannelStore) GetChannelsWithTeamDataByIds(channelIds []string, includeDeleted bool) ([]*model.ChannelWithTeamData, error) {
ret := _m.Called(channelIds, includeDeleted)
var r0 []*model.ChannelWithTeamData
if rf, ok := ret.Get(0).(func([]string, bool) []*model.ChannelWithTeamData); ok {
r0 = rf(channelIds, includeDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelWithTeamData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string, bool) error); ok {
r1 = rf(channelIds, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetDeleted provides a mock function with given fields: team_id, offset, limit, userID
func (_m *ChannelStore) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) {
ret := _m.Called(team_id, offset, limit, userID)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, int, int, string) model.ChannelList); ok {
r0 = rf(team_id, offset, limit, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int, string) error); ok {
r1 = rf(team_id, offset, limit, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetDeletedByName provides a mock function with given fields: team_id, name
func (_m *ChannelStore) GetDeletedByName(team_id string, name string) (*model.Channel, error) {
ret := _m.Called(team_id, name)
var r0 *model.Channel
if rf, ok := ret.Get(0).(func(string, string) *model.Channel); ok {
r0 = rf(team_id, name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(team_id, name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetFileCount provides a mock function with given fields: channelID
func (_m *ChannelStore) GetFileCount(channelID string) (int64, error) {
ret := _m.Called(channelID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(channelID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForPost provides a mock function with given fields: postID
func (_m *ChannelStore) GetForPost(postID string) (*model.Channel, error) {
ret := _m.Called(postID)
var r0 *model.Channel
if rf, ok := ret.Get(0).(func(string) *model.Channel); ok {
r0 = rf(postID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(postID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetGuestCount provides a mock function with given fields: channelID, allowFromCache
func (_m *ChannelStore) GetGuestCount(channelID string, allowFromCache bool) (int64, error) {
ret := _m.Called(channelID, allowFromCache)
var r0 int64
if rf, ok := ret.Get(0).(func(string, bool) int64); ok {
r0 = rf(channelID, allowFromCache)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(channelID, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMany provides a mock function with given fields: ids, allowFromCache
func (_m *ChannelStore) GetMany(ids []string, allowFromCache bool) (model.ChannelList, error) {
ret := _m.Called(ids, allowFromCache)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func([]string, bool) model.ChannelList); ok {
r0 = rf(ids, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string, bool) error); ok {
r1 = rf(ids, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMember provides a mock function with given fields: ctx, channelID, userID
func (_m *ChannelStore) GetMember(ctx context.Context, channelID string, userID string) (*model.ChannelMember, error) {
ret := _m.Called(ctx, channelID, userID)
var r0 *model.ChannelMember
if rf, ok := ret.Get(0).(func(context.Context, string, string) *model.ChannelMember); ok {
r0 = rf(ctx, channelID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ChannelMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = rf(ctx, channelID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMemberCount provides a mock function with given fields: channelID, allowFromCache
func (_m *ChannelStore) GetMemberCount(channelID string, allowFromCache bool) (int64, error) {
ret := _m.Called(channelID, allowFromCache)
var r0 int64
if rf, ok := ret.Get(0).(func(string, bool) int64); ok {
r0 = rf(channelID, allowFromCache)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(channelID, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMemberCountFromCache provides a mock function with given fields: channelID
func (_m *ChannelStore) GetMemberCountFromCache(channelID string) int64 {
ret := _m.Called(channelID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(channelID)
} else {
r0 = ret.Get(0).(int64)
}
return r0
}
// GetMemberCountsByGroup provides a mock function with given fields: ctx, channelID, includeTimezones
func (_m *ChannelStore) GetMemberCountsByGroup(ctx context.Context, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, error) {
ret := _m.Called(ctx, channelID, includeTimezones)
var r0 []*model.ChannelMemberCountByGroup
if rf, ok := ret.Get(0).(func(context.Context, string, bool) []*model.ChannelMemberCountByGroup); ok {
r0 = rf(ctx, channelID, includeTimezones)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelMemberCountByGroup)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, bool) error); ok {
r1 = rf(ctx, channelID, includeTimezones)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMemberForPost provides a mock function with given fields: postID, userID
func (_m *ChannelStore) GetMemberForPost(postID string, userID string) (*model.ChannelMember, error) {
ret := _m.Called(postID, userID)
var r0 *model.ChannelMember
if rf, ok := ret.Get(0).(func(string, string) *model.ChannelMember); ok {
r0 = rf(postID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ChannelMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(postID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembers provides a mock function with given fields: channelID, offset, limit
func (_m *ChannelStore) GetMembers(channelID string, offset int, limit int) (model.ChannelMembers, error) {
ret := _m.Called(channelID, offset, limit)
var r0 model.ChannelMembers
if rf, ok := ret.Get(0).(func(string, int, int) model.ChannelMembers); ok {
r0 = rf(channelID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelMembers)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(channelID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembersByChannelIds provides a mock function with given fields: channelIds, userID
func (_m *ChannelStore) GetMembersByChannelIds(channelIds []string, userID string) (model.ChannelMembers, error) {
ret := _m.Called(channelIds, userID)
var r0 model.ChannelMembers
if rf, ok := ret.Get(0).(func([]string, string) model.ChannelMembers); ok {
r0 = rf(channelIds, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelMembers)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string, string) error); ok {
r1 = rf(channelIds, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembersByIds provides a mock function with given fields: channelID, userIds
func (_m *ChannelStore) GetMembersByIds(channelID string, userIds []string) (model.ChannelMembers, error) {
ret := _m.Called(channelID, userIds)
var r0 model.ChannelMembers
if rf, ok := ret.Get(0).(func(string, []string) model.ChannelMembers); ok {
r0 = rf(channelID, userIds)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelMembers)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string) error); ok {
r1 = rf(channelID, userIds)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembersForUser provides a mock function with given fields: teamID, userID
func (_m *ChannelStore) GetMembersForUser(teamID string, userID string) (model.ChannelMembers, error) {
ret := _m.Called(teamID, userID)
var r0 model.ChannelMembers
if rf, ok := ret.Get(0).(func(string, string) model.ChannelMembers); ok {
r0 = rf(teamID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelMembers)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(teamID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembersForUserWithCursor provides a mock function with given fields: userID, teamID, opts
func (_m *ChannelStore) GetMembersForUserWithCursor(userID string, teamID string, opts *store.ChannelMemberGraphQLSearchOpts) (model.ChannelMembers, error) {
ret := _m.Called(userID, teamID, opts)
var r0 model.ChannelMembers
if rf, ok := ret.Get(0).(func(string, string, *store.ChannelMemberGraphQLSearchOpts) model.ChannelMembers); ok {
r0 = rf(userID, teamID, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelMembers)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *store.ChannelMemberGraphQLSearchOpts) error); ok {
r1 = rf(userID, teamID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembersForUserWithPagination provides a mock function with given fields: userID, page, perPage
func (_m *ChannelStore) GetMembersForUserWithPagination(userID string, page int, perPage int) (model.ChannelMembersWithTeamData, error) {
ret := _m.Called(userID, page, perPage)
var r0 model.ChannelMembersWithTeamData
if rf, ok := ret.Get(0).(func(string, int, int) model.ChannelMembersWithTeamData); ok {
r0 = rf(userID, page, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelMembersWithTeamData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(userID, page, perPage)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembersInfoByChannelIds provides a mock function with given fields: channelIDs
func (_m *ChannelStore) GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error) {
ret := _m.Called(channelIDs)
var r0 map[string][]*model.User
if rf, ok := ret.Get(0).(func([]string) map[string][]*model.User); ok {
r0 = rf(channelIDs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string][]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(channelIDs)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMoreChannels provides a mock function with given fields: teamID, userID, offset, limit
func (_m *ChannelStore) GetMoreChannels(teamID string, userID string, offset int, limit int) (model.ChannelList, error) {
ret := _m.Called(teamID, userID, offset, limit)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, string, int, int) model.ChannelList); ok {
r0 = rf(teamID, userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int, int) error); ok {
r1 = rf(teamID, userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPinnedPostCount provides a mock function with given fields: channelID, allowFromCache
func (_m *ChannelStore) GetPinnedPostCount(channelID string, allowFromCache bool) (int64, error) {
ret := _m.Called(channelID, allowFromCache)
var r0 int64
if rf, ok := ret.Get(0).(func(string, bool) int64); ok {
r0 = rf(channelID, allowFromCache)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(channelID, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPinnedPosts provides a mock function with given fields: channelID
func (_m *ChannelStore) GetPinnedPosts(channelID string) (*model.PostList, error) {
ret := _m.Called(channelID)
var r0 *model.PostList
if rf, ok := ret.Get(0).(func(string) *model.PostList); ok {
r0 = rf(channelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPrivateChannelsForTeam provides a mock function with given fields: teamID, offset, limit
func (_m *ChannelStore) GetPrivateChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, error) {
ret := _m.Called(teamID, offset, limit)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, int, int) model.ChannelList); ok {
r0 = rf(teamID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(teamID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPublicChannelsByIdsForTeam provides a mock function with given fields: teamID, channelIds
func (_m *ChannelStore) GetPublicChannelsByIdsForTeam(teamID string, channelIds []string) (model.ChannelList, error) {
ret := _m.Called(teamID, channelIds)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, []string) model.ChannelList); ok {
r0 = rf(teamID, channelIds)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string) error); ok {
r1 = rf(teamID, channelIds)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPublicChannelsForTeam provides a mock function with given fields: teamID, offset, limit
func (_m *ChannelStore) GetPublicChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, error) {
ret := _m.Called(teamID, offset, limit)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, int, int) model.ChannelList); ok {
r0 = rf(teamID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(teamID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSidebarCategories provides a mock function with given fields: userID, opts
func (_m *ChannelStore) GetSidebarCategories(userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
ret := _m.Called(userID, opts)
var r0 *model.OrderedSidebarCategories
if rf, ok := ret.Get(0).(func(string, *store.SidebarCategorySearchOpts) *model.OrderedSidebarCategories); ok {
r0 = rf(userID, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OrderedSidebarCategories)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, *store.SidebarCategorySearchOpts) error); ok {
r1 = rf(userID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSidebarCategoriesForTeamForUser provides a mock function with given fields: userID, teamID
func (_m *ChannelStore) GetSidebarCategoriesForTeamForUser(userID string, teamID string) (*model.OrderedSidebarCategories, error) {
ret := _m.Called(userID, teamID)
var r0 *model.OrderedSidebarCategories
if rf, ok := ret.Get(0).(func(string, string) *model.OrderedSidebarCategories); ok {
r0 = rf(userID, teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OrderedSidebarCategories)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSidebarCategory provides a mock function with given fields: categoryID
func (_m *ChannelStore) GetSidebarCategory(categoryID string) (*model.SidebarCategoryWithChannels, error) {
ret := _m.Called(categoryID)
var r0 *model.SidebarCategoryWithChannels
if rf, ok := ret.Get(0).(func(string) *model.SidebarCategoryWithChannels); ok {
r0 = rf(categoryID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SidebarCategoryWithChannels)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(categoryID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSidebarCategoryOrder provides a mock function with given fields: userID, teamID
func (_m *ChannelStore) GetSidebarCategoryOrder(userID string, teamID string) ([]string, error) {
ret := _m.Called(userID, teamID)
var r0 []string
if rf, ok := ret.Get(0).(func(string, string) []string); ok {
r0 = rf(userID, teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamChannels provides a mock function with given fields: teamID
func (_m *ChannelStore) GetTeamChannels(teamID string) (model.ChannelList, error) {
ret := _m.Called(teamID)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string) model.ChannelList); ok {
r0 = rf(teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamForChannel provides a mock function with given fields: channelID
func (_m *ChannelStore) GetTeamForChannel(channelID string) (*model.Team, error) {
ret := _m.Called(channelID)
var r0 *model.Team
if rf, ok := ret.Get(0).(func(string) *model.Team); ok {
r0 = rf(channelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamMembersForChannel provides a mock function with given fields: channelID
func (_m *ChannelStore) GetTeamMembersForChannel(channelID string) ([]string, error) {
ret := _m.Called(channelID)
var r0 []string
if rf, ok := ret.Get(0).(func(string) []string); ok {
r0 = rf(channelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopChannelsForTeamSince provides a mock function with given fields: teamID, userID, since, offset, limit
func (_m *ChannelStore) GetTopChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
ret := _m.Called(teamID, userID, since, offset, limit)
var r0 *model.TopChannelList
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopChannelList); ok {
r0 = rf(teamID, userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(teamID, userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopChannelsForUserSince provides a mock function with given fields: userID, teamID, since, offset, limit
func (_m *ChannelStore) GetTopChannelsForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
ret := _m.Called(userID, teamID, since, offset, limit)
var r0 *model.TopChannelList
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopChannelList); ok {
r0 = rf(userID, teamID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(userID, teamID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopInactiveChannelsForTeamSince provides a mock function with given fields: teamID, userID, since, offset, limit
func (_m *ChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
ret := _m.Called(teamID, userID, since, offset, limit)
var r0 *model.TopInactiveChannelList
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopInactiveChannelList); ok {
r0 = rf(teamID, userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopInactiveChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(teamID, userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopInactiveChannelsForUserSince provides a mock function with given fields: teamID, userID, since, offset, limit
func (_m *ChannelStore) GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
ret := _m.Called(teamID, userID, since, offset, limit)
var r0 *model.TopInactiveChannelList
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopInactiveChannelList); ok {
r0 = rf(teamID, userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopInactiveChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(teamID, userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GroupSyncedChannelCount provides a mock function with given fields:
func (_m *ChannelStore) GroupSyncedChannelCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// IncrementMentionCount provides a mock function with given fields: channelID, userIDs, isRoot, isUrgent
func (_m *ChannelStore) IncrementMentionCount(channelID string, userIDs []string, isRoot bool, isUrgent bool) error {
ret := _m.Called(channelID, userIDs, isRoot, isUrgent)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string, bool, bool) error); ok {
r0 = rf(channelID, userIDs, isRoot, isUrgent)
} else {
r0 = ret.Error(0)
}
return r0
}
// InvalidateAllChannelMembersForUser provides a mock function with given fields: userID
func (_m *ChannelStore) InvalidateAllChannelMembersForUser(userID string) {
_m.Called(userID)
}
// InvalidateCacheForChannelMembersNotifyProps provides a mock function with given fields: channelID
func (_m *ChannelStore) InvalidateCacheForChannelMembersNotifyProps(channelID string) {
_m.Called(channelID)
}
// InvalidateChannel provides a mock function with given fields: id
func (_m *ChannelStore) InvalidateChannel(id string) {
_m.Called(id)
}
// InvalidateChannelByName provides a mock function with given fields: teamID, name
func (_m *ChannelStore) InvalidateChannelByName(teamID string, name string) {
_m.Called(teamID, name)
}
// InvalidateGuestCount provides a mock function with given fields: channelID
func (_m *ChannelStore) InvalidateGuestCount(channelID string) {
_m.Called(channelID)
}
// InvalidateMemberCount provides a mock function with given fields: channelID
func (_m *ChannelStore) InvalidateMemberCount(channelID string) {
_m.Called(channelID)
}
// InvalidatePinnedPostCount provides a mock function with given fields: channelID
func (_m *ChannelStore) InvalidatePinnedPostCount(channelID string) {
_m.Called(channelID)
}
// IsUserInChannelUseCache provides a mock function with given fields: userID, channelID
func (_m *ChannelStore) IsUserInChannelUseCache(userID string, channelID string) bool {
ret := _m.Called(userID, channelID)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string) bool); ok {
r0 = rf(userID, channelID)
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// MigrateChannelMembers provides a mock function with given fields: fromChannelID, fromUserID
func (_m *ChannelStore) MigrateChannelMembers(fromChannelID string, fromUserID string) (map[string]string, error) {
ret := _m.Called(fromChannelID, fromUserID)
var r0 map[string]string
if rf, ok := ret.Get(0).(func(string, string) map[string]string); ok {
r0 = rf(fromChannelID, fromUserID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(fromChannelID, fromUserID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDelete provides a mock function with given fields: channelID
func (_m *ChannelStore) PermanentDelete(channelID string) error {
ret := _m.Called(channelID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(channelID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteByTeam provides a mock function with given fields: teamID
func (_m *ChannelStore) PermanentDeleteByTeam(teamID string) error {
ret := _m.Called(teamID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(teamID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteMembersByChannel provides a mock function with given fields: channelID
func (_m *ChannelStore) PermanentDeleteMembersByChannel(channelID string) error {
ret := _m.Called(channelID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(channelID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteMembersByUser provides a mock function with given fields: userID
func (_m *ChannelStore) PermanentDeleteMembersByUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PostCountsByDuration provides a mock function with given fields: channelIDs, sinceUnixMillis, userID, duration, groupingLocation
func (_m *ChannelStore) PostCountsByDuration(channelIDs []string, sinceUnixMillis int64, userID *string, duration model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, error) {
ret := _m.Called(channelIDs, sinceUnixMillis, userID, duration, groupingLocation)
var r0 []*model.DurationPostCount
if rf, ok := ret.Get(0).(func([]string, int64, *string, model.PostCountGrouping, *time.Location) []*model.DurationPostCount); ok {
r0 = rf(channelIDs, sinceUnixMillis, userID, duration, groupingLocation)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.DurationPostCount)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string, int64, *string, model.PostCountGrouping, *time.Location) error); ok {
r1 = rf(channelIDs, sinceUnixMillis, userID, duration, groupingLocation)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RemoveAllDeactivatedMembers provides a mock function with given fields: channelID
func (_m *ChannelStore) RemoveAllDeactivatedMembers(channelID string) error {
ret := _m.Called(channelID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(channelID)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveMember provides a mock function with given fields: channelID, userID
func (_m *ChannelStore) RemoveMember(channelID string, userID string) error {
ret := _m.Called(channelID, userID)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(channelID, userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveMembers provides a mock function with given fields: channelID, userIds
func (_m *ChannelStore) RemoveMembers(channelID string, userIds []string) error {
ret := _m.Called(channelID, userIds)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(channelID, userIds)
} else {
r0 = ret.Error(0)
}
return r0
}
// ResetAllChannelSchemes provides a mock function with given fields:
func (_m *ChannelStore) ResetAllChannelSchemes() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Restore provides a mock function with given fields: channelID, timestamp
func (_m *ChannelStore) Restore(channelID string, timestamp int64) error {
ret := _m.Called(channelID, timestamp)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64) error); ok {
r0 = rf(channelID, timestamp)
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: channel, maxChannelsPerTeam
func (_m *ChannelStore) Save(channel *model.Channel, maxChannelsPerTeam int64) (*model.Channel, error) {
ret := _m.Called(channel, maxChannelsPerTeam)
var r0 *model.Channel
if rf, ok := ret.Get(0).(func(*model.Channel, int64) *model.Channel); ok {
r0 = rf(channel, maxChannelsPerTeam)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Channel, int64) error); ok {
r1 = rf(channel, maxChannelsPerTeam)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveDirectChannel provides a mock function with given fields: channel, member1, member2
func (_m *ChannelStore) SaveDirectChannel(channel *model.Channel, member1 *model.ChannelMember, member2 *model.ChannelMember) (*model.Channel, error) {
ret := _m.Called(channel, member1, member2)
var r0 *model.Channel
if rf, ok := ret.Get(0).(func(*model.Channel, *model.ChannelMember, *model.ChannelMember) *model.Channel); ok {
r0 = rf(channel, member1, member2)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Channel, *model.ChannelMember, *model.ChannelMember) error); ok {
r1 = rf(channel, member1, member2)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveMember provides a mock function with given fields: member
func (_m *ChannelStore) SaveMember(member *model.ChannelMember) (*model.ChannelMember, error) {
ret := _m.Called(member)
var r0 *model.ChannelMember
if rf, ok := ret.Get(0).(func(*model.ChannelMember) *model.ChannelMember); ok {
r0 = rf(member)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ChannelMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.ChannelMember) error); ok {
r1 = rf(member)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveMultipleMembers provides a mock function with given fields: members
func (_m *ChannelStore) SaveMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
ret := _m.Called(members)
var r0 []*model.ChannelMember
if rf, ok := ret.Get(0).(func([]*model.ChannelMember) []*model.ChannelMember); ok {
r0 = rf(members)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]*model.ChannelMember) error); ok {
r1 = rf(members)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchAllChannels provides a mock function with given fields: term, opts
func (_m *ChannelStore) SearchAllChannels(term string, opts store.ChannelSearchOpts) (model.ChannelListWithTeamData, int64, error) {
ret := _m.Called(term, opts)
var r0 model.ChannelListWithTeamData
if rf, ok := ret.Get(0).(func(string, store.ChannelSearchOpts) model.ChannelListWithTeamData); ok {
r0 = rf(term, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelListWithTeamData)
}
}
var r1 int64
if rf, ok := ret.Get(1).(func(string, store.ChannelSearchOpts) int64); ok {
r1 = rf(term, opts)
} else {
r1 = ret.Get(1).(int64)
}
var r2 error
if rf, ok := ret.Get(2).(func(string, store.ChannelSearchOpts) error); ok {
r2 = rf(term, opts)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// SearchArchivedInTeam provides a mock function with given fields: teamID, term, userID
func (_m *ChannelStore) SearchArchivedInTeam(teamID string, term string, userID string) (model.ChannelList, error) {
ret := _m.Called(teamID, term, userID)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, string, string) model.ChannelList); ok {
r0 = rf(teamID, term, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string) error); ok {
r1 = rf(teamID, term, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchForUserInTeam provides a mock function with given fields: userID, teamID, term, includeDeleted
func (_m *ChannelStore) SearchForUserInTeam(userID string, teamID string, term string, includeDeleted bool) (model.ChannelList, error) {
ret := _m.Called(userID, teamID, term, includeDeleted)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, string, string, bool) model.ChannelList); ok {
r0 = rf(userID, teamID, term, includeDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string, bool) error); ok {
r1 = rf(userID, teamID, term, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchGroupChannels provides a mock function with given fields: userID, term
func (_m *ChannelStore) SearchGroupChannels(userID string, term string) (model.ChannelList, error) {
ret := _m.Called(userID, term)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, string) model.ChannelList); ok {
r0 = rf(userID, term)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, term)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchInTeam provides a mock function with given fields: teamID, term, includeDeleted
func (_m *ChannelStore) SearchInTeam(teamID string, term string, includeDeleted bool) (model.ChannelList, error) {
ret := _m.Called(teamID, term, includeDeleted)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, string, bool) model.ChannelList); ok {
r0 = rf(teamID, term, includeDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, bool) error); ok {
r1 = rf(teamID, term, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchMore provides a mock function with given fields: userID, teamID, term
func (_m *ChannelStore) SearchMore(userID string, teamID string, term string) (model.ChannelList, error) {
ret := _m.Called(userID, teamID, term)
var r0 model.ChannelList
if rf, ok := ret.Get(0).(func(string, string, string) model.ChannelList); ok {
r0 = rf(userID, teamID, term)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string) error); ok {
r1 = rf(userID, teamID, term)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SetDeleteAt provides a mock function with given fields: channelID, deleteAt, updateAt
func (_m *ChannelStore) SetDeleteAt(channelID string, deleteAt int64, updateAt int64) error {
ret := _m.Called(channelID, deleteAt, updateAt)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64, int64) error); ok {
r0 = rf(channelID, deleteAt, updateAt)
} else {
r0 = ret.Error(0)
}
return r0
}
// SetShared provides a mock function with given fields: channelId, shared
func (_m *ChannelStore) SetShared(channelId string, shared bool) error {
ret := _m.Called(channelId, shared)
var r0 error
if rf, ok := ret.Get(0).(func(string, bool) error); ok {
r0 = rf(channelId, shared)
} else {
r0 = ret.Error(0)
}
return r0
}
// Update provides a mock function with given fields: channel
func (_m *ChannelStore) Update(channel *model.Channel) (*model.Channel, error) {
ret := _m.Called(channel)
var r0 *model.Channel
if rf, ok := ret.Get(0).(func(*model.Channel) *model.Channel); ok {
r0 = rf(channel)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Channel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Channel) error); ok {
r1 = rf(channel)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateLastViewedAt provides a mock function with given fields: channelIds, userID
func (_m *ChannelStore) UpdateLastViewedAt(channelIds []string, userID string) (map[string]int64, error) {
ret := _m.Called(channelIds, userID)
var r0 map[string]int64
if rf, ok := ret.Get(0).(func([]string, string) map[string]int64); ok {
r0 = rf(channelIds, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]int64)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string, string) error); ok {
r1 = rf(channelIds, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateLastViewedAtPost provides a mock function with given fields: unreadPost, userID, mentionCount, mentionCountRoot, urgentMentionCount, setUnreadCountRoot
func (_m *ChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, urgentMentionCount int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) {
ret := _m.Called(unreadPost, userID, mentionCount, mentionCountRoot, urgentMentionCount, setUnreadCountRoot)
var r0 *model.ChannelUnreadAt
if rf, ok := ret.Get(0).(func(*model.Post, string, int, int, int, bool) *model.ChannelUnreadAt); ok {
r0 = rf(unreadPost, userID, mentionCount, mentionCountRoot, urgentMentionCount, setUnreadCountRoot)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ChannelUnreadAt)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Post, string, int, int, int, bool) error); ok {
r1 = rf(unreadPost, userID, mentionCount, mentionCountRoot, urgentMentionCount, setUnreadCountRoot)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateMember provides a mock function with given fields: member
func (_m *ChannelStore) UpdateMember(member *model.ChannelMember) (*model.ChannelMember, error) {
ret := _m.Called(member)
var r0 *model.ChannelMember
if rf, ok := ret.Get(0).(func(*model.ChannelMember) *model.ChannelMember); ok {
r0 = rf(member)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ChannelMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.ChannelMember) error); ok {
r1 = rf(member)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateMemberNotifyProps provides a mock function with given fields: channelID, userID, props
func (_m *ChannelStore) UpdateMemberNotifyProps(channelID string, userID string, props map[string]string) (*model.ChannelMember, error) {
ret := _m.Called(channelID, userID, props)
var r0 *model.ChannelMember
if rf, ok := ret.Get(0).(func(string, string, map[string]string) *model.ChannelMember); ok {
r0 = rf(channelID, userID, props)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ChannelMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, map[string]string) error); ok {
r1 = rf(channelID, userID, props)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateMembersRole provides a mock function with given fields: channelID, userIDs
func (_m *ChannelStore) UpdateMembersRole(channelID string, userIDs []string) error {
ret := _m.Called(channelID, userIDs)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(channelID, userIDs)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateMultipleMembers provides a mock function with given fields: members
func (_m *ChannelStore) UpdateMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
ret := _m.Called(members)
var r0 []*model.ChannelMember
if rf, ok := ret.Get(0).(func([]*model.ChannelMember) []*model.ChannelMember); ok {
r0 = rf(members)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]*model.ChannelMember) error); ok {
r1 = rf(members)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateSidebarCategories provides a mock function with given fields: userID, teamID, categories
func (_m *ChannelStore) UpdateSidebarCategories(userID string, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) {
ret := _m.Called(userID, teamID, categories)
var r0 []*model.SidebarCategoryWithChannels
if rf, ok := ret.Get(0).(func(string, string, []*model.SidebarCategoryWithChannels) []*model.SidebarCategoryWithChannels); ok {
r0 = rf(userID, teamID, categories)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.SidebarCategoryWithChannels)
}
}
var r1 []*model.SidebarCategoryWithChannels
if rf, ok := ret.Get(1).(func(string, string, []*model.SidebarCategoryWithChannels) []*model.SidebarCategoryWithChannels); ok {
r1 = rf(userID, teamID, categories)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).([]*model.SidebarCategoryWithChannels)
}
}
var r2 error
if rf, ok := ret.Get(2).(func(string, string, []*model.SidebarCategoryWithChannels) error); ok {
r2 = rf(userID, teamID, categories)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// UpdateSidebarCategoryOrder provides a mock function with given fields: userID, teamID, categoryOrder
func (_m *ChannelStore) UpdateSidebarCategoryOrder(userID string, teamID string, categoryOrder []string) error {
ret := _m.Called(userID, teamID, categoryOrder)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, []string) error); ok {
r0 = rf(userID, teamID, categoryOrder)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateSidebarChannelCategoryOnMove provides a mock function with given fields: channel, newTeamID
func (_m *ChannelStore) UpdateSidebarChannelCategoryOnMove(channel *model.Channel, newTeamID string) error {
ret := _m.Called(channel, newTeamID)
var r0 error
if rf, ok := ret.Get(0).(func(*model.Channel, string) error); ok {
r0 = rf(channel, newTeamID)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateSidebarChannelsByPreferences provides a mock function with given fields: preferences
func (_m *ChannelStore) UpdateSidebarChannelsByPreferences(preferences model.Preferences) error {
ret := _m.Called(preferences)
var r0 error
if rf, ok := ret.Get(0).(func(model.Preferences) error); ok {
r0 = rf(preferences)
} else {
r0 = ret.Error(0)
}
return r0
}
// UserBelongsToChannels provides a mock function with given fields: userID, channelIds
func (_m *ChannelStore) UserBelongsToChannels(userID string, channelIds []string) (bool, error) {
ret := _m.Called(userID, channelIds)
var r0 bool
if rf, ok := ret.Get(0).(func(string, []string) bool); ok {
r0 = rf(userID, channelIds)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string) error); ok {
r1 = rf(userID, channelIds)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// ClusterDiscoveryStore is an autogenerated mock type for the ClusterDiscoveryStore type
type ClusterDiscoveryStore struct {
mock.Mock
}
// Cleanup provides a mock function with given fields:
func (_m *ClusterDiscoveryStore) Cleanup() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Delete provides a mock function with given fields: discovery
func (_m *ClusterDiscoveryStore) Delete(discovery *model.ClusterDiscovery) (bool, error) {
ret := _m.Called(discovery)
var r0 bool
if rf, ok := ret.Get(0).(func(*model.ClusterDiscovery) bool); ok {
r0 = rf(discovery)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.ClusterDiscovery) error); ok {
r1 = rf(discovery)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Exists provides a mock function with given fields: discovery
func (_m *ClusterDiscoveryStore) Exists(discovery *model.ClusterDiscovery) (bool, error) {
ret := _m.Called(discovery)
var r0 bool
if rf, ok := ret.Get(0).(func(*model.ClusterDiscovery) bool); ok {
r0 = rf(discovery)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.ClusterDiscovery) error); ok {
r1 = rf(discovery)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields: discoveryType, clusterName
func (_m *ClusterDiscoveryStore) GetAll(discoveryType string, clusterName string) ([]*model.ClusterDiscovery, error) {
ret := _m.Called(discoveryType, clusterName)
var r0 []*model.ClusterDiscovery
if rf, ok := ret.Get(0).(func(string, string) []*model.ClusterDiscovery); ok {
r0 = rf(discoveryType, clusterName)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ClusterDiscovery)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(discoveryType, clusterName)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: discovery
func (_m *ClusterDiscoveryStore) Save(discovery *model.ClusterDiscovery) error {
ret := _m.Called(discovery)
var r0 error
if rf, ok := ret.Get(0).(func(*model.ClusterDiscovery) error); ok {
r0 = rf(discovery)
} else {
r0 = ret.Error(0)
}
return r0
}
// SetLastPingAt provides a mock function with given fields: discovery
func (_m *ClusterDiscoveryStore) SetLastPingAt(discovery *model.ClusterDiscovery) error {
ret := _m.Called(discovery)
var r0 error
if rf, ok := ret.Get(0).(func(*model.ClusterDiscovery) error); ok {
r0 = rf(discovery)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// CommandStore is an autogenerated mock type for the CommandStore type
type CommandStore struct {
mock.Mock
}
// AnalyticsCommandCount provides a mock function with given fields: teamID
func (_m *CommandStore) AnalyticsCommandCount(teamID string) (int64, error) {
ret := _m.Called(teamID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(teamID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: commandID, timestamp
func (_m *CommandStore) Delete(commandID string, timestamp int64) error {
ret := _m.Called(commandID, timestamp)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64) error); ok {
r0 = rf(commandID, timestamp)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: id
func (_m *CommandStore) Get(id string) (*model.Command, error) {
ret := _m.Called(id)
var r0 *model.Command
if rf, ok := ret.Get(0).(func(string) *model.Command); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Command)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByTeam provides a mock function with given fields: teamID
func (_m *CommandStore) GetByTeam(teamID string) ([]*model.Command, error) {
ret := _m.Called(teamID)
var r0 []*model.Command
if rf, ok := ret.Get(0).(func(string) []*model.Command); ok {
r0 = rf(teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Command)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByTrigger provides a mock function with given fields: teamID, trigger
func (_m *CommandStore) GetByTrigger(teamID string, trigger string) (*model.Command, error) {
ret := _m.Called(teamID, trigger)
var r0 *model.Command
if rf, ok := ret.Get(0).(func(string, string) *model.Command); ok {
r0 = rf(teamID, trigger)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Command)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(teamID, trigger)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteByTeam provides a mock function with given fields: teamID
func (_m *CommandStore) PermanentDeleteByTeam(teamID string) error {
ret := _m.Called(teamID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(teamID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteByUser provides a mock function with given fields: userID
func (_m *CommandStore) PermanentDeleteByUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: webhook
func (_m *CommandStore) Save(webhook *model.Command) (*model.Command, error) {
ret := _m.Called(webhook)
var r0 *model.Command
if rf, ok := ret.Get(0).(func(*model.Command) *model.Command); ok {
r0 = rf(webhook)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Command)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Command) error); ok {
r1 = rf(webhook)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: hook
func (_m *CommandStore) Update(hook *model.Command) (*model.Command, error) {
ret := _m.Called(hook)
var r0 *model.Command
if rf, ok := ret.Get(0).(func(*model.Command) *model.Command); ok {
r0 = rf(hook)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Command)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Command) error); ok {
r1 = rf(hook)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// CommandWebhookStore is an autogenerated mock type for the CommandWebhookStore type
type CommandWebhookStore struct {
mock.Mock
}
// Cleanup provides a mock function with given fields:
func (_m *CommandWebhookStore) Cleanup() {
_m.Called()
}
// Get provides a mock function with given fields: id
func (_m *CommandWebhookStore) Get(id string) (*model.CommandWebhook, error) {
ret := _m.Called(id)
var r0 *model.CommandWebhook
if rf, ok := ret.Get(0).(func(string) *model.CommandWebhook); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.CommandWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: webhook
func (_m *CommandWebhookStore) Save(webhook *model.CommandWebhook) (*model.CommandWebhook, error) {
ret := _m.Called(webhook)
var r0 *model.CommandWebhook
if rf, ok := ret.Get(0).(func(*model.CommandWebhook) *model.CommandWebhook); ok {
r0 = rf(webhook)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.CommandWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.CommandWebhook) error); ok {
r1 = rf(webhook)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// TryUse provides a mock function with given fields: id, limit
func (_m *CommandWebhookStore) TryUse(id string, limit int) error {
ret := _m.Called(id, limit)
var r0 error
if rf, ok := ret.Get(0).(func(string, int) error); ok {
r0 = rf(id, limit)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
context "context"
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// ComplianceStore is an autogenerated mock type for the ComplianceStore type
type ComplianceStore struct {
mock.Mock
}
// ComplianceExport provides a mock function with given fields: compliance, cursor, limit
func (_m *ComplianceStore) ComplianceExport(compliance *model.Compliance, cursor model.ComplianceExportCursor, limit int) ([]*model.CompliancePost, model.ComplianceExportCursor, error) {
ret := _m.Called(compliance, cursor, limit)
var r0 []*model.CompliancePost
if rf, ok := ret.Get(0).(func(*model.Compliance, model.ComplianceExportCursor, int) []*model.CompliancePost); ok {
r0 = rf(compliance, cursor, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.CompliancePost)
}
}
var r1 model.ComplianceExportCursor
if rf, ok := ret.Get(1).(func(*model.Compliance, model.ComplianceExportCursor, int) model.ComplianceExportCursor); ok {
r1 = rf(compliance, cursor, limit)
} else {
r1 = ret.Get(1).(model.ComplianceExportCursor)
}
var r2 error
if rf, ok := ret.Get(2).(func(*model.Compliance, model.ComplianceExportCursor, int) error); ok {
r2 = rf(compliance, cursor, limit)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// Get provides a mock function with given fields: id
func (_m *ComplianceStore) Get(id string) (*model.Compliance, error) {
ret := _m.Called(id)
var r0 *model.Compliance
if rf, ok := ret.Get(0).(func(string) *model.Compliance); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Compliance)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields: offset, limit
func (_m *ComplianceStore) GetAll(offset int, limit int) (model.Compliances, error) {
ret := _m.Called(offset, limit)
var r0 model.Compliances
if rf, ok := ret.Get(0).(func(int, int) model.Compliances); ok {
r0 = rf(offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Compliances)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int) error); ok {
r1 = rf(offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MessageExport provides a mock function with given fields: ctx, cursor, limit
func (_m *ComplianceStore) MessageExport(ctx context.Context, cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) {
ret := _m.Called(ctx, cursor, limit)
var r0 []*model.MessageExport
if rf, ok := ret.Get(0).(func(context.Context, model.MessageExportCursor, int) []*model.MessageExport); ok {
r0 = rf(ctx, cursor, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.MessageExport)
}
}
var r1 model.MessageExportCursor
if rf, ok := ret.Get(1).(func(context.Context, model.MessageExportCursor, int) model.MessageExportCursor); ok {
r1 = rf(ctx, cursor, limit)
} else {
r1 = ret.Get(1).(model.MessageExportCursor)
}
var r2 error
if rf, ok := ret.Get(2).(func(context.Context, model.MessageExportCursor, int) error); ok {
r2 = rf(ctx, cursor, limit)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// Save provides a mock function with given fields: compliance
func (_m *ComplianceStore) Save(compliance *model.Compliance) (*model.Compliance, error) {
ret := _m.Called(compliance)
var r0 *model.Compliance
if rf, ok := ret.Get(0).(func(*model.Compliance) *model.Compliance); ok {
r0 = rf(compliance)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Compliance)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Compliance) error); ok {
r1 = rf(compliance)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: compliance
func (_m *ComplianceStore) Update(compliance *model.Compliance) (*model.Compliance, error) {
ret := _m.Called(compliance)
var r0 *model.Compliance
if rf, ok := ret.Get(0).(func(*model.Compliance) *model.Compliance); ok {
r0 = rf(compliance)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Compliance)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Compliance) error); ok {
r1 = rf(compliance)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// DraftStore is an autogenerated mock type for the DraftStore type
type DraftStore struct {
mock.Mock
}
// Delete provides a mock function with given fields: userID, channelID, rootID
func (_m *DraftStore) Delete(userID string, channelID string, rootID string) error {
ret := _m.Called(userID, channelID, rootID)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = rf(userID, channelID, rootID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: userID, channelID, rootID, includeDeleted
func (_m *DraftStore) Get(userID string, channelID string, rootID string, includeDeleted bool) (*model.Draft, error) {
ret := _m.Called(userID, channelID, rootID, includeDeleted)
var r0 *model.Draft
if rf, ok := ret.Get(0).(func(string, string, string, bool) *model.Draft); ok {
r0 = rf(userID, channelID, rootID, includeDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Draft)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string, bool) error); ok {
r1 = rf(userID, channelID, rootID, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetDraftsForUser provides a mock function with given fields: userID, teamID
func (_m *DraftStore) GetDraftsForUser(userID string, teamID string) ([]*model.Draft, error) {
ret := _m.Called(userID, teamID)
var r0 []*model.Draft
if rf, ok := ret.Get(0).(func(string, string) []*model.Draft); ok {
r0 = rf(userID, teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Draft)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: d
func (_m *DraftStore) Save(d *model.Draft) (*model.Draft, error) {
ret := _m.Called(d)
var r0 *model.Draft
if rf, ok := ret.Get(0).(func(*model.Draft) *model.Draft); ok {
r0 = rf(d)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Draft)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Draft) error); ok {
r1 = rf(d)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: d
func (_m *DraftStore) Update(d *model.Draft) (*model.Draft, error) {
ret := _m.Called(d)
var r0 *model.Draft
if rf, ok := ret.Get(0).(func(*model.Draft) *model.Draft); ok {
r0 = rf(d)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Draft)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Draft) error); ok {
r1 = rf(d)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
context "context"
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// EmojiStore is an autogenerated mock type for the EmojiStore type
type EmojiStore struct {
mock.Mock
}
// Delete provides a mock function with given fields: emoji, timestamp
func (_m *EmojiStore) Delete(emoji *model.Emoji, timestamp int64) error {
ret := _m.Called(emoji, timestamp)
var r0 error
if rf, ok := ret.Get(0).(func(*model.Emoji, int64) error); ok {
r0 = rf(emoji, timestamp)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: ctx, id, allowFromCache
func (_m *EmojiStore) Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error) {
ret := _m.Called(ctx, id, allowFromCache)
var r0 *model.Emoji
if rf, ok := ret.Get(0).(func(context.Context, string, bool) *model.Emoji); ok {
r0 = rf(ctx, id, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Emoji)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, bool) error); ok {
r1 = rf(ctx, id, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByName provides a mock function with given fields: ctx, name, allowFromCache
func (_m *EmojiStore) GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error) {
ret := _m.Called(ctx, name, allowFromCache)
var r0 *model.Emoji
if rf, ok := ret.Get(0).(func(context.Context, string, bool) *model.Emoji); ok {
r0 = rf(ctx, name, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Emoji)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, bool) error); ok {
r1 = rf(ctx, name, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetList provides a mock function with given fields: offset, limit, sort
func (_m *EmojiStore) GetList(offset int, limit int, sort string) ([]*model.Emoji, error) {
ret := _m.Called(offset, limit, sort)
var r0 []*model.Emoji
if rf, ok := ret.Get(0).(func(int, int, string) []*model.Emoji); ok {
r0 = rf(offset, limit, sort)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Emoji)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int, string) error); ok {
r1 = rf(offset, limit, sort)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMultipleByName provides a mock function with given fields: names
func (_m *EmojiStore) GetMultipleByName(names []string) ([]*model.Emoji, error) {
ret := _m.Called(names)
var r0 []*model.Emoji
if rf, ok := ret.Get(0).(func([]string) []*model.Emoji); ok {
r0 = rf(names)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Emoji)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(names)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: emoji
func (_m *EmojiStore) Save(emoji *model.Emoji) (*model.Emoji, error) {
ret := _m.Called(emoji)
var r0 *model.Emoji
if rf, ok := ret.Get(0).(func(*model.Emoji) *model.Emoji); ok {
r0 = rf(emoji)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Emoji)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Emoji) error); ok {
r1 = rf(emoji)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Search provides a mock function with given fields: name, prefixOnly, limit
func (_m *EmojiStore) Search(name string, prefixOnly bool, limit int) ([]*model.Emoji, error) {
ret := _m.Called(name, prefixOnly, limit)
var r0 []*model.Emoji
if rf, ok := ret.Get(0).(func(string, bool, int) []*model.Emoji); ok {
r0 = rf(name, prefixOnly, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Emoji)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool, int) error); ok {
r1 = rf(name, prefixOnly, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// FileInfoStore is an autogenerated mock type for the FileInfoStore type
type FileInfoStore struct {
mock.Mock
}
// AttachToPost provides a mock function with given fields: fileID, postID, channelID, creatorID
func (_m *FileInfoStore) AttachToPost(fileID string, postID string, channelID string, creatorID string) error {
ret := _m.Called(fileID, postID, channelID, creatorID)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
r0 = rf(fileID, postID, channelID, creatorID)
} else {
r0 = ret.Error(0)
}
return r0
}
// ClearCaches provides a mock function with given fields:
func (_m *FileInfoStore) ClearCaches() {
_m.Called()
}
// CountAll provides a mock function with given fields:
func (_m *FileInfoStore) CountAll() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteForPost provides a mock function with given fields: postID
func (_m *FileInfoStore) DeleteForPost(postID string) (string, error) {
ret := _m.Called(postID)
var r0 string
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(postID)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(postID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: id
func (_m *FileInfoStore) Get(id string) (*model.FileInfo, error) {
ret := _m.Called(id)
var r0 *model.FileInfo
if rf, ok := ret.Get(0).(func(string) *model.FileInfo); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.FileInfo)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByIds provides a mock function with given fields: ids
func (_m *FileInfoStore) GetByIds(ids []string) ([]*model.FileInfo, error) {
ret := _m.Called(ids)
var r0 []*model.FileInfo
if rf, ok := ret.Get(0).(func([]string) []*model.FileInfo); ok {
r0 = rf(ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.FileInfo)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByPath provides a mock function with given fields: path
func (_m *FileInfoStore) GetByPath(path string) (*model.FileInfo, error) {
ret := _m.Called(path)
var r0 *model.FileInfo
if rf, ok := ret.Get(0).(func(string) *model.FileInfo); ok {
r0 = rf(path)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.FileInfo)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(path)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetFilesBatchForIndexing provides a mock function with given fields: startTime, startFileID, limit
func (_m *FileInfoStore) GetFilesBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.FileForIndexing, error) {
ret := _m.Called(startTime, startFileID, limit)
var r0 []*model.FileForIndexing
if rf, ok := ret.Get(0).(func(int64, string, int) []*model.FileForIndexing); ok {
r0 = rf(startTime, startFileID, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.FileForIndexing)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, string, int) error); ok {
r1 = rf(startTime, startFileID, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForPost provides a mock function with given fields: postID, readFromMaster, includeDeleted, allowFromCache
func (_m *FileInfoStore) GetForPost(postID string, readFromMaster bool, includeDeleted bool, allowFromCache bool) ([]*model.FileInfo, error) {
ret := _m.Called(postID, readFromMaster, includeDeleted, allowFromCache)
var r0 []*model.FileInfo
if rf, ok := ret.Get(0).(func(string, bool, bool, bool) []*model.FileInfo); ok {
r0 = rf(postID, readFromMaster, includeDeleted, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.FileInfo)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool, bool, bool) error); ok {
r1 = rf(postID, readFromMaster, includeDeleted, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForUser provides a mock function with given fields: userID
func (_m *FileInfoStore) GetForUser(userID string) ([]*model.FileInfo, error) {
ret := _m.Called(userID)
var r0 []*model.FileInfo
if rf, ok := ret.Get(0).(func(string) []*model.FileInfo); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.FileInfo)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetFromMaster provides a mock function with given fields: id
func (_m *FileInfoStore) GetFromMaster(id string) (*model.FileInfo, error) {
ret := _m.Called(id)
var r0 *model.FileInfo
if rf, ok := ret.Get(0).(func(string) *model.FileInfo); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.FileInfo)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetStorageUsage provides a mock function with given fields: allowFromCache, includeDeleted
func (_m *FileInfoStore) GetStorageUsage(allowFromCache bool, includeDeleted bool) (int64, error) {
ret := _m.Called(allowFromCache, includeDeleted)
var r0 int64
if rf, ok := ret.Get(0).(func(bool, bool) int64); ok {
r0 = rf(allowFromCache, includeDeleted)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(bool, bool) error); ok {
r1 = rf(allowFromCache, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUptoNSizeFileTime provides a mock function with given fields: n
func (_m *FileInfoStore) GetUptoNSizeFileTime(n int64) (int64, error) {
ret := _m.Called(n)
var r0 int64
if rf, ok := ret.Get(0).(func(int64) int64); ok {
r0 = rf(n)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int64) error); ok {
r1 = rf(n)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetWithOptions provides a mock function with given fields: page, perPage, opt
func (_m *FileInfoStore) GetWithOptions(page int, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, error) {
ret := _m.Called(page, perPage, opt)
var r0 []*model.FileInfo
if rf, ok := ret.Get(0).(func(int, int, *model.GetFileInfosOptions) []*model.FileInfo); ok {
r0 = rf(page, perPage, opt)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.FileInfo)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int, *model.GetFileInfosOptions) error); ok {
r1 = rf(page, perPage, opt)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// InvalidateFileInfosForPostCache provides a mock function with given fields: postID, deleted
func (_m *FileInfoStore) InvalidateFileInfosForPostCache(postID string, deleted bool) {
_m.Called(postID, deleted)
}
// PermanentDelete provides a mock function with given fields: fileID
func (_m *FileInfoStore) PermanentDelete(fileID string) error {
ret := _m.Called(fileID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(fileID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteBatch provides a mock function with given fields: endTime, limit
func (_m *FileInfoStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
ret := _m.Called(endTime, limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64) int64); ok {
r0 = rf(endTime, limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, int64) error); ok {
r1 = rf(endTime, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteByUser provides a mock function with given fields: userID
func (_m *FileInfoStore) PermanentDeleteByUser(userID string) (int64, error) {
ret := _m.Called(userID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(userID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: info
func (_m *FileInfoStore) Save(info *model.FileInfo) (*model.FileInfo, error) {
ret := _m.Called(info)
var r0 *model.FileInfo
if rf, ok := ret.Get(0).(func(*model.FileInfo) *model.FileInfo); ok {
r0 = rf(info)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.FileInfo)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.FileInfo) error); ok {
r1 = rf(info)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Search provides a mock function with given fields: paramsList, userID, teamID, page, perPage
func (_m *FileInfoStore) Search(paramsList []*model.SearchParams, userID string, teamID string, page int, perPage int) (*model.FileInfoList, error) {
ret := _m.Called(paramsList, userID, teamID, page, perPage)
var r0 *model.FileInfoList
if rf, ok := ret.Get(0).(func([]*model.SearchParams, string, string, int, int) *model.FileInfoList); ok {
r0 = rf(paramsList, userID, teamID, page, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.FileInfoList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]*model.SearchParams, string, string, int, int) error); ok {
r1 = rf(paramsList, userID, teamID, page, perPage)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SetContent provides a mock function with given fields: fileID, content
func (_m *FileInfoStore) SetContent(fileID string, content string) error {
ret := _m.Called(fileID, content)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(fileID, content)
} else {
r0 = ret.Error(0)
}
return r0
}
// Upsert provides a mock function with given fields: info
func (_m *FileInfoStore) Upsert(info *model.FileInfo) (*model.FileInfo, error) {
ret := _m.Called(info)
var r0 *model.FileInfo
if rf, ok := ret.Get(0).(func(*model.FileInfo) *model.FileInfo); ok {
r0 = rf(info)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.FileInfo)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.FileInfo) error); ok {
r1 = rf(info)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// GroupStore is an autogenerated mock type for the GroupStore type
type GroupStore struct {
mock.Mock
}
// AdminRoleGroupsForSyncableMember provides a mock function with given fields: userID, syncableID, syncableType
func (_m *GroupStore) AdminRoleGroupsForSyncableMember(userID string, syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
ret := _m.Called(userID, syncableID, syncableType)
var r0 []string
if rf, ok := ret.Get(0).(func(string, string, model.GroupSyncableType) []string); ok {
r0 = rf(userID, syncableID, syncableType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, model.GroupSyncableType) error); ok {
r1 = rf(userID, syncableID, syncableType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ChannelMembersMinusGroupMembers provides a mock function with given fields: channelID, groupIDs, page, perPage
func (_m *GroupStore) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, error) {
ret := _m.Called(channelID, groupIDs, page, perPage)
var r0 []*model.UserWithGroups
if rf, ok := ret.Get(0).(func(string, []string, int, int) []*model.UserWithGroups); ok {
r0 = rf(channelID, groupIDs, page, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.UserWithGroups)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string, int, int) error); ok {
r1 = rf(channelID, groupIDs, page, perPage)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ChannelMembersToAdd provides a mock function with given fields: since, channelID, includeRemovedMembers
func (_m *GroupStore) ChannelMembersToAdd(since int64, channelID *string, includeRemovedMembers bool) ([]*model.UserChannelIDPair, error) {
ret := _m.Called(since, channelID, includeRemovedMembers)
var r0 []*model.UserChannelIDPair
if rf, ok := ret.Get(0).(func(int64, *string, bool) []*model.UserChannelIDPair); ok {
r0 = rf(since, channelID, includeRemovedMembers)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.UserChannelIDPair)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, *string, bool) error); ok {
r1 = rf(since, channelID, includeRemovedMembers)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ChannelMembersToRemove provides a mock function with given fields: channelID
func (_m *GroupStore) ChannelMembersToRemove(channelID *string) ([]*model.ChannelMember, error) {
ret := _m.Called(channelID)
var r0 []*model.ChannelMember
if rf, ok := ret.Get(0).(func(*string) []*model.ChannelMember); ok {
r0 = rf(channelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*string) error); ok {
r1 = rf(channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountChannelMembersMinusGroupMembers provides a mock function with given fields: channelID, groupIDs
func (_m *GroupStore) CountChannelMembersMinusGroupMembers(channelID string, groupIDs []string) (int64, error) {
ret := _m.Called(channelID, groupIDs)
var r0 int64
if rf, ok := ret.Get(0).(func(string, []string) int64); ok {
r0 = rf(channelID, groupIDs)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string) error); ok {
r1 = rf(channelID, groupIDs)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountGroupsByChannel provides a mock function with given fields: channelID, opts
func (_m *GroupStore) CountGroupsByChannel(channelID string, opts model.GroupSearchOpts) (int64, error) {
ret := _m.Called(channelID, opts)
var r0 int64
if rf, ok := ret.Get(0).(func(string, model.GroupSearchOpts) int64); ok {
r0 = rf(channelID, opts)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.GroupSearchOpts) error); ok {
r1 = rf(channelID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountGroupsByTeam provides a mock function with given fields: teamID, opts
func (_m *GroupStore) CountGroupsByTeam(teamID string, opts model.GroupSearchOpts) (int64, error) {
ret := _m.Called(teamID, opts)
var r0 int64
if rf, ok := ret.Get(0).(func(string, model.GroupSearchOpts) int64); ok {
r0 = rf(teamID, opts)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.GroupSearchOpts) error); ok {
r1 = rf(teamID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountTeamMembersMinusGroupMembers provides a mock function with given fields: teamID, groupIDs
func (_m *GroupStore) CountTeamMembersMinusGroupMembers(teamID string, groupIDs []string) (int64, error) {
ret := _m.Called(teamID, groupIDs)
var r0 int64
if rf, ok := ret.Get(0).(func(string, []string) int64); ok {
r0 = rf(teamID, groupIDs)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string) error); ok {
r1 = rf(teamID, groupIDs)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Create provides a mock function with given fields: group
func (_m *GroupStore) Create(group *model.Group) (*model.Group, error) {
ret := _m.Called(group)
var r0 *model.Group
if rf, ok := ret.Get(0).(func(*model.Group) *model.Group); ok {
r0 = rf(group)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Group) error); ok {
r1 = rf(group)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateGroupSyncable provides a mock function with given fields: groupSyncable
func (_m *GroupStore) CreateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
ret := _m.Called(groupSyncable)
var r0 *model.GroupSyncable
if rf, ok := ret.Get(0).(func(*model.GroupSyncable) *model.GroupSyncable); ok {
r0 = rf(groupSyncable)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.GroupSyncable)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.GroupSyncable) error); ok {
r1 = rf(groupSyncable)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateWithUserIds provides a mock function with given fields: group
func (_m *GroupStore) CreateWithUserIds(group *model.GroupWithUserIds) (*model.Group, error) {
ret := _m.Called(group)
var r0 *model.Group
if rf, ok := ret.Get(0).(func(*model.GroupWithUserIds) *model.Group); ok {
r0 = rf(group)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.GroupWithUserIds) error); ok {
r1 = rf(group)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: groupID
func (_m *GroupStore) Delete(groupID string) (*model.Group, error) {
ret := _m.Called(groupID)
var r0 *model.Group
if rf, ok := ret.Get(0).(func(string) *model.Group); ok {
r0 = rf(groupID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(groupID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteGroupSyncable provides a mock function with given fields: groupID, syncableID, syncableType
func (_m *GroupStore) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
ret := _m.Called(groupID, syncableID, syncableType)
var r0 *model.GroupSyncable
if rf, ok := ret.Get(0).(func(string, string, model.GroupSyncableType) *model.GroupSyncable); ok {
r0 = rf(groupID, syncableID, syncableType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.GroupSyncable)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, model.GroupSyncableType) error); ok {
r1 = rf(groupID, syncableID, syncableType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteMember provides a mock function with given fields: groupID, userID
func (_m *GroupStore) DeleteMember(groupID string, userID string) (*model.GroupMember, error) {
ret := _m.Called(groupID, userID)
var r0 *model.GroupMember
if rf, ok := ret.Get(0).(func(string, string) *model.GroupMember); ok {
r0 = rf(groupID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.GroupMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(groupID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteMembers provides a mock function with given fields: groupID, userIDs
func (_m *GroupStore) DeleteMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
ret := _m.Called(groupID, userIDs)
var r0 []*model.GroupMember
if rf, ok := ret.Get(0).(func(string, []string) []*model.GroupMember); ok {
r0 = rf(groupID, userIDs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.GroupMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string) error); ok {
r1 = rf(groupID, userIDs)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DistinctGroupMemberCount provides a mock function with given fields:
func (_m *GroupStore) DistinctGroupMemberCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DistinctGroupMemberCountForSource provides a mock function with given fields: source
func (_m *GroupStore) DistinctGroupMemberCountForSource(source model.GroupSource) (int64, error) {
ret := _m.Called(source)
var r0 int64
if rf, ok := ret.Get(0).(func(model.GroupSource) int64); ok {
r0 = rf(source)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(model.GroupSource) error); ok {
r1 = rf(source)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: groupID
func (_m *GroupStore) Get(groupID string) (*model.Group, error) {
ret := _m.Called(groupID)
var r0 *model.Group
if rf, ok := ret.Get(0).(func(string) *model.Group); ok {
r0 = rf(groupID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(groupID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllBySource provides a mock function with given fields: groupSource
func (_m *GroupStore) GetAllBySource(groupSource model.GroupSource) ([]*model.Group, error) {
ret := _m.Called(groupSource)
var r0 []*model.Group
if rf, ok := ret.Get(0).(func(model.GroupSource) []*model.Group); ok {
r0 = rf(groupSource)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(model.GroupSource) error); ok {
r1 = rf(groupSource)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllGroupSyncablesByGroupId provides a mock function with given fields: groupID, syncableType
func (_m *GroupStore) GetAllGroupSyncablesByGroupId(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, error) {
ret := _m.Called(groupID, syncableType)
var r0 []*model.GroupSyncable
if rf, ok := ret.Get(0).(func(string, model.GroupSyncableType) []*model.GroupSyncable); ok {
r0 = rf(groupID, syncableType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.GroupSyncable)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.GroupSyncableType) error); ok {
r1 = rf(groupID, syncableType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByIDs provides a mock function with given fields: groupIDs
func (_m *GroupStore) GetByIDs(groupIDs []string) ([]*model.Group, error) {
ret := _m.Called(groupIDs)
var r0 []*model.Group
if rf, ok := ret.Get(0).(func([]string) []*model.Group); ok {
r0 = rf(groupIDs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(groupIDs)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByName provides a mock function with given fields: name, opts
func (_m *GroupStore) GetByName(name string, opts model.GroupSearchOpts) (*model.Group, error) {
ret := _m.Called(name, opts)
var r0 *model.Group
if rf, ok := ret.Get(0).(func(string, model.GroupSearchOpts) *model.Group); ok {
r0 = rf(name, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.GroupSearchOpts) error); ok {
r1 = rf(name, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByRemoteID provides a mock function with given fields: remoteID, groupSource
func (_m *GroupStore) GetByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, error) {
ret := _m.Called(remoteID, groupSource)
var r0 *model.Group
if rf, ok := ret.Get(0).(func(string, model.GroupSource) *model.Group); ok {
r0 = rf(remoteID, groupSource)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.GroupSource) error); ok {
r1 = rf(remoteID, groupSource)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByUser provides a mock function with given fields: userID
func (_m *GroupStore) GetByUser(userID string) ([]*model.Group, error) {
ret := _m.Called(userID)
var r0 []*model.Group
if rf, ok := ret.Get(0).(func(string) []*model.Group); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetGroupSyncable provides a mock function with given fields: groupID, syncableID, syncableType
func (_m *GroupStore) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
ret := _m.Called(groupID, syncableID, syncableType)
var r0 *model.GroupSyncable
if rf, ok := ret.Get(0).(func(string, string, model.GroupSyncableType) *model.GroupSyncable); ok {
r0 = rf(groupID, syncableID, syncableType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.GroupSyncable)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, model.GroupSyncableType) error); ok {
r1 = rf(groupID, syncableID, syncableType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetGroups provides a mock function with given fields: page, perPage, opts, viewRestrictions
func (_m *GroupStore) GetGroups(page int, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error) {
ret := _m.Called(page, perPage, opts, viewRestrictions)
var r0 []*model.Group
if rf, ok := ret.Get(0).(func(int, int, model.GroupSearchOpts, *model.ViewUsersRestrictions) []*model.Group); ok {
r0 = rf(page, perPage, opts, viewRestrictions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int, model.GroupSearchOpts, *model.ViewUsersRestrictions) error); ok {
r1 = rf(page, perPage, opts, viewRestrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetGroupsAssociatedToChannelsByTeam provides a mock function with given fields: teamID, opts
func (_m *GroupStore) GetGroupsAssociatedToChannelsByTeam(teamID string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, error) {
ret := _m.Called(teamID, opts)
var r0 map[string][]*model.GroupWithSchemeAdmin
if rf, ok := ret.Get(0).(func(string, model.GroupSearchOpts) map[string][]*model.GroupWithSchemeAdmin); ok {
r0 = rf(teamID, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string][]*model.GroupWithSchemeAdmin)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.GroupSearchOpts) error); ok {
r1 = rf(teamID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetGroupsByChannel provides a mock function with given fields: channelID, opts
func (_m *GroupStore) GetGroupsByChannel(channelID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
ret := _m.Called(channelID, opts)
var r0 []*model.GroupWithSchemeAdmin
if rf, ok := ret.Get(0).(func(string, model.GroupSearchOpts) []*model.GroupWithSchemeAdmin); ok {
r0 = rf(channelID, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.GroupWithSchemeAdmin)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.GroupSearchOpts) error); ok {
r1 = rf(channelID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetGroupsByTeam provides a mock function with given fields: teamID, opts
func (_m *GroupStore) GetGroupsByTeam(teamID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
ret := _m.Called(teamID, opts)
var r0 []*model.GroupWithSchemeAdmin
if rf, ok := ret.Get(0).(func(string, model.GroupSearchOpts) []*model.GroupWithSchemeAdmin); ok {
r0 = rf(teamID, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.GroupWithSchemeAdmin)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.GroupSearchOpts) error); ok {
r1 = rf(teamID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMember provides a mock function with given fields: groupID, userID
func (_m *GroupStore) GetMember(groupID string, userID string) (*model.GroupMember, error) {
ret := _m.Called(groupID, userID)
var r0 *model.GroupMember
if rf, ok := ret.Get(0).(func(string, string) *model.GroupMember); ok {
r0 = rf(groupID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.GroupMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(groupID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMemberCount provides a mock function with given fields: groupID
func (_m *GroupStore) GetMemberCount(groupID string) (int64, error) {
ret := _m.Called(groupID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(groupID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(groupID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMemberCountWithRestrictions provides a mock function with given fields: groupID, viewRestrictions
func (_m *GroupStore) GetMemberCountWithRestrictions(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, error) {
ret := _m.Called(groupID, viewRestrictions)
var r0 int64
if rf, ok := ret.Get(0).(func(string, *model.ViewUsersRestrictions) int64); ok {
r0 = rf(groupID, viewRestrictions)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, *model.ViewUsersRestrictions) error); ok {
r1 = rf(groupID, viewRestrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMemberUsers provides a mock function with given fields: groupID
func (_m *GroupStore) GetMemberUsers(groupID string) ([]*model.User, error) {
ret := _m.Called(groupID)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string) []*model.User); ok {
r0 = rf(groupID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(groupID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMemberUsersInTeam provides a mock function with given fields: groupID, teamID
func (_m *GroupStore) GetMemberUsersInTeam(groupID string, teamID string) ([]*model.User, error) {
ret := _m.Called(groupID, teamID)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, string) []*model.User); ok {
r0 = rf(groupID, teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(groupID, teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMemberUsersNotInChannel provides a mock function with given fields: groupID, channelID
func (_m *GroupStore) GetMemberUsersNotInChannel(groupID string, channelID string) ([]*model.User, error) {
ret := _m.Called(groupID, channelID)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, string) []*model.User); ok {
r0 = rf(groupID, channelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(groupID, channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMemberUsersPage provides a mock function with given fields: groupID, page, perPage, viewRestrictions
func (_m *GroupStore) GetMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
ret := _m.Called(groupID, page, perPage, viewRestrictions)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, int, int, *model.ViewUsersRestrictions) []*model.User); ok {
r0 = rf(groupID, page, perPage, viewRestrictions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int, *model.ViewUsersRestrictions) error); ok {
r1 = rf(groupID, page, perPage, viewRestrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMemberUsersSortedPage provides a mock function with given fields: groupID, page, perPage, viewRestrictions, teammateNameDisplay
func (_m *GroupStore) GetMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, error) {
ret := _m.Called(groupID, page, perPage, viewRestrictions, teammateNameDisplay)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, int, int, *model.ViewUsersRestrictions, string) []*model.User); ok {
r0 = rf(groupID, page, perPage, viewRestrictions, teammateNameDisplay)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int, *model.ViewUsersRestrictions, string) error); ok {
r1 = rf(groupID, page, perPage, viewRestrictions, teammateNameDisplay)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetNonMemberUsersPage provides a mock function with given fields: groupID, page, perPage, viewRestrictions
func (_m *GroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
ret := _m.Called(groupID, page, perPage, viewRestrictions)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, int, int, *model.ViewUsersRestrictions) []*model.User); ok {
r0 = rf(groupID, page, perPage, viewRestrictions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int, *model.ViewUsersRestrictions) error); ok {
r1 = rf(groupID, page, perPage, viewRestrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GroupChannelCount provides a mock function with given fields:
func (_m *GroupStore) GroupChannelCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GroupCount provides a mock function with given fields:
func (_m *GroupStore) GroupCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GroupCountBySource provides a mock function with given fields: source
func (_m *GroupStore) GroupCountBySource(source model.GroupSource) (int64, error) {
ret := _m.Called(source)
var r0 int64
if rf, ok := ret.Get(0).(func(model.GroupSource) int64); ok {
r0 = rf(source)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(model.GroupSource) error); ok {
r1 = rf(source)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GroupCountWithAllowReference provides a mock function with given fields:
func (_m *GroupStore) GroupCountWithAllowReference() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GroupMemberCount provides a mock function with given fields:
func (_m *GroupStore) GroupMemberCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GroupTeamCount provides a mock function with given fields:
func (_m *GroupStore) GroupTeamCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteMembersByUser provides a mock function with given fields: userID
func (_m *GroupStore) PermanentDeleteMembersByUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermittedSyncableAdmins provides a mock function with given fields: syncableID, syncableType
func (_m *GroupStore) PermittedSyncableAdmins(syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
ret := _m.Called(syncableID, syncableType)
var r0 []string
if rf, ok := ret.Get(0).(func(string, model.GroupSyncableType) []string); ok {
r0 = rf(syncableID, syncableType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.GroupSyncableType) error); ok {
r1 = rf(syncableID, syncableType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Restore provides a mock function with given fields: groupID
func (_m *GroupStore) Restore(groupID string) (*model.Group, error) {
ret := _m.Called(groupID)
var r0 *model.Group
if rf, ok := ret.Get(0).(func(string) *model.Group); ok {
r0 = rf(groupID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(groupID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// TeamMembersMinusGroupMembers provides a mock function with given fields: teamID, groupIDs, page, perPage
func (_m *GroupStore) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, error) {
ret := _m.Called(teamID, groupIDs, page, perPage)
var r0 []*model.UserWithGroups
if rf, ok := ret.Get(0).(func(string, []string, int, int) []*model.UserWithGroups); ok {
r0 = rf(teamID, groupIDs, page, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.UserWithGroups)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string, int, int) error); ok {
r1 = rf(teamID, groupIDs, page, perPage)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// TeamMembersToAdd provides a mock function with given fields: since, teamID, includeRemovedMembers
func (_m *GroupStore) TeamMembersToAdd(since int64, teamID *string, includeRemovedMembers bool) ([]*model.UserTeamIDPair, error) {
ret := _m.Called(since, teamID, includeRemovedMembers)
var r0 []*model.UserTeamIDPair
if rf, ok := ret.Get(0).(func(int64, *string, bool) []*model.UserTeamIDPair); ok {
r0 = rf(since, teamID, includeRemovedMembers)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.UserTeamIDPair)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, *string, bool) error); ok {
r1 = rf(since, teamID, includeRemovedMembers)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// TeamMembersToRemove provides a mock function with given fields: teamID
func (_m *GroupStore) TeamMembersToRemove(teamID *string) ([]*model.TeamMember, error) {
ret := _m.Called(teamID)
var r0 []*model.TeamMember
if rf, ok := ret.Get(0).(func(*string) []*model.TeamMember); ok {
r0 = rf(teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.TeamMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*string) error); ok {
r1 = rf(teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: group
func (_m *GroupStore) Update(group *model.Group) (*model.Group, error) {
ret := _m.Called(group)
var r0 *model.Group
if rf, ok := ret.Get(0).(func(*model.Group) *model.Group); ok {
r0 = rf(group)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Group) error); ok {
r1 = rf(group)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateGroupSyncable provides a mock function with given fields: groupSyncable
func (_m *GroupStore) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
ret := _m.Called(groupSyncable)
var r0 *model.GroupSyncable
if rf, ok := ret.Get(0).(func(*model.GroupSyncable) *model.GroupSyncable); ok {
r0 = rf(groupSyncable)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.GroupSyncable)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.GroupSyncable) error); ok {
r1 = rf(groupSyncable)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpsertMember provides a mock function with given fields: groupID, userID
func (_m *GroupStore) UpsertMember(groupID string, userID string) (*model.GroupMember, error) {
ret := _m.Called(groupID, userID)
var r0 *model.GroupMember
if rf, ok := ret.Get(0).(func(string, string) *model.GroupMember); ok {
r0 = rf(groupID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.GroupMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(groupID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpsertMembers provides a mock function with given fields: groupID, userIDs
func (_m *GroupStore) UpsertMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
ret := _m.Called(groupID, userIDs)
var r0 []*model.GroupMember
if rf, ok := ret.Get(0).(func(string, []string) []*model.GroupMember); ok {
r0 = rf(groupID, userIDs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.GroupMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string) error); ok {
r1 = rf(groupID, userIDs)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// JobStore is an autogenerated mock type for the JobStore type
type JobStore struct {
mock.Mock
}
// Cleanup provides a mock function with given fields: expiryTime, batchSize
func (_m *JobStore) Cleanup(expiryTime int64, batchSize int) error {
ret := _m.Called(expiryTime, batchSize)
var r0 error
if rf, ok := ret.Get(0).(func(int64, int) error); ok {
r0 = rf(expiryTime, batchSize)
} else {
r0 = ret.Error(0)
}
return r0
}
// Delete provides a mock function with given fields: id
func (_m *JobStore) Delete(id string) (string, error) {
ret := _m.Called(id)
var r0 string
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(id)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: id
func (_m *JobStore) Get(id string) (*model.Job, error) {
ret := _m.Called(id)
var r0 *model.Job
if rf, ok := ret.Get(0).(func(string) *model.Job); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllByStatus provides a mock function with given fields: status
func (_m *JobStore) GetAllByStatus(status string) ([]*model.Job, error) {
ret := _m.Called(status)
var r0 []*model.Job
if rf, ok := ret.Get(0).(func(string) []*model.Job); ok {
r0 = rf(status)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(status)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllByType provides a mock function with given fields: jobType
func (_m *JobStore) GetAllByType(jobType string) ([]*model.Job, error) {
ret := _m.Called(jobType)
var r0 []*model.Job
if rf, ok := ret.Get(0).(func(string) []*model.Job); ok {
r0 = rf(jobType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(jobType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllByTypeAndStatus provides a mock function with given fields: jobType, status
func (_m *JobStore) GetAllByTypeAndStatus(jobType string, status string) ([]*model.Job, error) {
ret := _m.Called(jobType, status)
var r0 []*model.Job
if rf, ok := ret.Get(0).(func(string, string) []*model.Job); ok {
r0 = rf(jobType, status)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(jobType, status)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllByTypePage provides a mock function with given fields: jobType, offset, limit
func (_m *JobStore) GetAllByTypePage(jobType string, offset int, limit int) ([]*model.Job, error) {
ret := _m.Called(jobType, offset, limit)
var r0 []*model.Job
if rf, ok := ret.Get(0).(func(string, int, int) []*model.Job); ok {
r0 = rf(jobType, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(jobType, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllByTypesPage provides a mock function with given fields: jobTypes, offset, limit
func (_m *JobStore) GetAllByTypesPage(jobTypes []string, offset int, limit int) ([]*model.Job, error) {
ret := _m.Called(jobTypes, offset, limit)
var r0 []*model.Job
if rf, ok := ret.Get(0).(func([]string, int, int) []*model.Job); ok {
r0 = rf(jobTypes, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string, int, int) error); ok {
r1 = rf(jobTypes, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllPage provides a mock function with given fields: offset, limit
func (_m *JobStore) GetAllPage(offset int, limit int) ([]*model.Job, error) {
ret := _m.Called(offset, limit)
var r0 []*model.Job
if rf, ok := ret.Get(0).(func(int, int) []*model.Job); ok {
r0 = rf(offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int) error); ok {
r1 = rf(offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetCountByStatusAndType provides a mock function with given fields: status, jobType
func (_m *JobStore) GetCountByStatusAndType(status string, jobType string) (int64, error) {
ret := _m.Called(status, jobType)
var r0 int64
if rf, ok := ret.Get(0).(func(string, string) int64); ok {
r0 = rf(status, jobType)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(status, jobType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetNewestJobByStatusAndType provides a mock function with given fields: status, jobType
func (_m *JobStore) GetNewestJobByStatusAndType(status string, jobType string) (*model.Job, error) {
ret := _m.Called(status, jobType)
var r0 *model.Job
if rf, ok := ret.Get(0).(func(string, string) *model.Job); ok {
r0 = rf(status, jobType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(status, jobType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetNewestJobByStatusesAndType provides a mock function with given fields: statuses, jobType
func (_m *JobStore) GetNewestJobByStatusesAndType(statuses []string, jobType string) (*model.Job, error) {
ret := _m.Called(statuses, jobType)
var r0 *model.Job
if rf, ok := ret.Get(0).(func([]string, string) *model.Job); ok {
r0 = rf(statuses, jobType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string, string) error); ok {
r1 = rf(statuses, jobType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: job
func (_m *JobStore) Save(job *model.Job) (*model.Job, error) {
ret := _m.Called(job)
var r0 *model.Job
if rf, ok := ret.Get(0).(func(*model.Job) *model.Job); ok {
r0 = rf(job)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Job) error); ok {
r1 = rf(job)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateOptimistically provides a mock function with given fields: job, currentStatus
func (_m *JobStore) UpdateOptimistically(job *model.Job, currentStatus string) (bool, error) {
ret := _m.Called(job, currentStatus)
var r0 bool
if rf, ok := ret.Get(0).(func(*model.Job, string) bool); ok {
r0 = rf(job, currentStatus)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Job, string) error); ok {
r1 = rf(job, currentStatus)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateStatus provides a mock function with given fields: id, status
func (_m *JobStore) UpdateStatus(id string, status string) (*model.Job, error) {
ret := _m.Called(id, status)
var r0 *model.Job
if rf, ok := ret.Get(0).(func(string, string) *model.Job); ok {
r0 = rf(id, status)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(id, status)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateStatusOptimistically provides a mock function with given fields: id, currentStatus, newStatus
func (_m *JobStore) UpdateStatusOptimistically(id string, currentStatus string, newStatus string) (bool, error) {
ret := _m.Called(id, currentStatus, newStatus)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string, string) bool); ok {
r0 = rf(id, currentStatus, newStatus)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string) error); ok {
r1 = rf(id, currentStatus, newStatus)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// LicenseStore is an autogenerated mock type for the LicenseStore type
type LicenseStore struct {
mock.Mock
}
// Get provides a mock function with given fields: id
func (_m *LicenseStore) Get(id string) (*model.LicenseRecord, error) {
ret := _m.Called(id)
var r0 *model.LicenseRecord
if rf, ok := ret.Get(0).(func(string) *model.LicenseRecord); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.LicenseRecord)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields:
func (_m *LicenseStore) GetAll() ([]*model.LicenseRecord, error) {
ret := _m.Called()
var r0 []*model.LicenseRecord
if rf, ok := ret.Get(0).(func() []*model.LicenseRecord); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.LicenseRecord)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: license
func (_m *LicenseStore) Save(license *model.LicenseRecord) (*model.LicenseRecord, error) {
ret := _m.Called(license)
var r0 *model.LicenseRecord
if rf, ok := ret.Get(0).(func(*model.LicenseRecord) *model.LicenseRecord); ok {
r0 = rf(license)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.LicenseRecord)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.LicenseRecord) error); ok {
r1 = rf(license)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// LinkMetadataStore is an autogenerated mock type for the LinkMetadataStore type
type LinkMetadataStore struct {
mock.Mock
}
// Get provides a mock function with given fields: url, timestamp
func (_m *LinkMetadataStore) Get(url string, timestamp int64) (*model.LinkMetadata, error) {
ret := _m.Called(url, timestamp)
var r0 *model.LinkMetadata
if rf, ok := ret.Get(0).(func(string, int64) *model.LinkMetadata); ok {
r0 = rf(url, timestamp)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.LinkMetadata)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64) error); ok {
r1 = rf(url, timestamp)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: linkMetadata
func (_m *LinkMetadataStore) Save(linkMetadata *model.LinkMetadata) (*model.LinkMetadata, error) {
ret := _m.Called(linkMetadata)
var r0 *model.LinkMetadata
if rf, ok := ret.Get(0).(func(*model.LinkMetadata) *model.LinkMetadata); ok {
r0 = rf(linkMetadata)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.LinkMetadata)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.LinkMetadata) error); ok {
r1 = rf(linkMetadata)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// NotifyAdminStore is an autogenerated mock type for the NotifyAdminStore type
type NotifyAdminStore struct {
mock.Mock
}
// DeleteBefore provides a mock function with given fields: trial, now
func (_m *NotifyAdminStore) DeleteBefore(trial bool, now int64) error {
ret := _m.Called(trial, now)
var r0 error
if rf, ok := ret.Get(0).(func(bool, int64) error); ok {
r0 = rf(trial, now)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: trial
func (_m *NotifyAdminStore) Get(trial bool) ([]*model.NotifyAdminData, error) {
ret := _m.Called(trial)
var r0 []*model.NotifyAdminData
if rf, ok := ret.Get(0).(func(bool) []*model.NotifyAdminData); ok {
r0 = rf(trial)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.NotifyAdminData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(bool) error); ok {
r1 = rf(trial)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetDataByUserIdAndFeature provides a mock function with given fields: userId, feature
func (_m *NotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostFeature) ([]*model.NotifyAdminData, error) {
ret := _m.Called(userId, feature)
var r0 []*model.NotifyAdminData
if rf, ok := ret.Get(0).(func(string, model.MattermostFeature) []*model.NotifyAdminData); ok {
r0 = rf(userId, feature)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.NotifyAdminData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, model.MattermostFeature) error); ok {
r1 = rf(userId, feature)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: data
func (_m *NotifyAdminStore) Save(data *model.NotifyAdminData) (*model.NotifyAdminData, error) {
ret := _m.Called(data)
var r0 *model.NotifyAdminData
if rf, ok := ret.Get(0).(func(*model.NotifyAdminData) *model.NotifyAdminData); ok {
r0 = rf(data)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.NotifyAdminData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.NotifyAdminData) error); ok {
r1 = rf(data)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: userId, requiredPlan, requiredFeature, now
func (_m *NotifyAdminStore) Update(userId string, requiredPlan string, requiredFeature model.MattermostFeature, now int64) error {
ret := _m.Called(userId, requiredPlan, requiredFeature, now)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, model.MattermostFeature, int64) error); ok {
r0 = rf(userId, requiredPlan, requiredFeature, now)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// OAuthStore is an autogenerated mock type for the OAuthStore type
type OAuthStore struct {
mock.Mock
}
// DeleteApp provides a mock function with given fields: id
func (_m *OAuthStore) DeleteApp(id string) error {
ret := _m.Called(id)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(id)
} else {
r0 = ret.Error(0)
}
return r0
}
// GetAccessData provides a mock function with given fields: token
func (_m *OAuthStore) GetAccessData(token string) (*model.AccessData, error) {
ret := _m.Called(token)
var r0 *model.AccessData
if rf, ok := ret.Get(0).(func(string) *model.AccessData); ok {
r0 = rf(token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AccessData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(token)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAccessDataByRefreshToken provides a mock function with given fields: token
func (_m *OAuthStore) GetAccessDataByRefreshToken(token string) (*model.AccessData, error) {
ret := _m.Called(token)
var r0 *model.AccessData
if rf, ok := ret.Get(0).(func(string) *model.AccessData); ok {
r0 = rf(token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AccessData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(token)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAccessDataByUserForApp provides a mock function with given fields: userID, clientId
func (_m *OAuthStore) GetAccessDataByUserForApp(userID string, clientId string) ([]*model.AccessData, error) {
ret := _m.Called(userID, clientId)
var r0 []*model.AccessData
if rf, ok := ret.Get(0).(func(string, string) []*model.AccessData); ok {
r0 = rf(userID, clientId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.AccessData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, clientId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetApp provides a mock function with given fields: id
func (_m *OAuthStore) GetApp(id string) (*model.OAuthApp, error) {
ret := _m.Called(id)
var r0 *model.OAuthApp
if rf, ok := ret.Get(0).(func(string) *model.OAuthApp); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OAuthApp)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAppByUser provides a mock function with given fields: userID, offset, limit
func (_m *OAuthStore) GetAppByUser(userID string, offset int, limit int) ([]*model.OAuthApp, error) {
ret := _m.Called(userID, offset, limit)
var r0 []*model.OAuthApp
if rf, ok := ret.Get(0).(func(string, int, int) []*model.OAuthApp); ok {
r0 = rf(userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.OAuthApp)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetApps provides a mock function with given fields: offset, limit
func (_m *OAuthStore) GetApps(offset int, limit int) ([]*model.OAuthApp, error) {
ret := _m.Called(offset, limit)
var r0 []*model.OAuthApp
if rf, ok := ret.Get(0).(func(int, int) []*model.OAuthApp); ok {
r0 = rf(offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.OAuthApp)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int) error); ok {
r1 = rf(offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAuthData provides a mock function with given fields: code
func (_m *OAuthStore) GetAuthData(code string) (*model.AuthData, error) {
ret := _m.Called(code)
var r0 *model.AuthData
if rf, ok := ret.Get(0).(func(string) *model.AuthData); ok {
r0 = rf(code)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AuthData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(code)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAuthorizedApps provides a mock function with given fields: userID, offset, limit
func (_m *OAuthStore) GetAuthorizedApps(userID string, offset int, limit int) ([]*model.OAuthApp, error) {
ret := _m.Called(userID, offset, limit)
var r0 []*model.OAuthApp
if rf, ok := ret.Get(0).(func(string, int, int) []*model.OAuthApp); ok {
r0 = rf(userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.OAuthApp)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPreviousAccessData provides a mock function with given fields: userID, clientId
func (_m *OAuthStore) GetPreviousAccessData(userID string, clientId string) (*model.AccessData, error) {
ret := _m.Called(userID, clientId)
var r0 *model.AccessData
if rf, ok := ret.Get(0).(func(string, string) *model.AccessData); ok {
r0 = rf(userID, clientId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AccessData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, clientId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteAuthDataByUser provides a mock function with given fields: userID
func (_m *OAuthStore) PermanentDeleteAuthDataByUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveAccessData provides a mock function with given fields: token
func (_m *OAuthStore) RemoveAccessData(token string) error {
ret := _m.Called(token)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(token)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveAllAccessData provides a mock function with given fields:
func (_m *OAuthStore) RemoveAllAccessData() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveAuthData provides a mock function with given fields: code
func (_m *OAuthStore) RemoveAuthData(code string) error {
ret := _m.Called(code)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(code)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveAuthDataByClientId provides a mock function with given fields: clientId, userId
func (_m *OAuthStore) RemoveAuthDataByClientId(clientId string, userId string) error {
ret := _m.Called(clientId, userId)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(clientId, userId)
} else {
r0 = ret.Error(0)
}
return r0
}
// SaveAccessData provides a mock function with given fields: accessData
func (_m *OAuthStore) SaveAccessData(accessData *model.AccessData) (*model.AccessData, error) {
ret := _m.Called(accessData)
var r0 *model.AccessData
if rf, ok := ret.Get(0).(func(*model.AccessData) *model.AccessData); ok {
r0 = rf(accessData)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AccessData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.AccessData) error); ok {
r1 = rf(accessData)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveApp provides a mock function with given fields: app
func (_m *OAuthStore) SaveApp(app *model.OAuthApp) (*model.OAuthApp, error) {
ret := _m.Called(app)
var r0 *model.OAuthApp
if rf, ok := ret.Get(0).(func(*model.OAuthApp) *model.OAuthApp); ok {
r0 = rf(app)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OAuthApp)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.OAuthApp) error); ok {
r1 = rf(app)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveAuthData provides a mock function with given fields: authData
func (_m *OAuthStore) SaveAuthData(authData *model.AuthData) (*model.AuthData, error) {
ret := _m.Called(authData)
var r0 *model.AuthData
if rf, ok := ret.Get(0).(func(*model.AuthData) *model.AuthData); ok {
r0 = rf(authData)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AuthData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.AuthData) error); ok {
r1 = rf(authData)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateAccessData provides a mock function with given fields: accessData
func (_m *OAuthStore) UpdateAccessData(accessData *model.AccessData) (*model.AccessData, error) {
ret := _m.Called(accessData)
var r0 *model.AccessData
if rf, ok := ret.Get(0).(func(*model.AccessData) *model.AccessData); ok {
r0 = rf(accessData)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AccessData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.AccessData) error); ok {
r1 = rf(accessData)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateApp provides a mock function with given fields: app
func (_m *OAuthStore) UpdateApp(app *model.OAuthApp) (*model.OAuthApp, error) {
ret := _m.Called(app)
var r0 *model.OAuthApp
if rf, ok := ret.Get(0).(func(*model.OAuthApp) *model.OAuthApp); ok {
r0 = rf(app)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OAuthApp)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.OAuthApp) error); ok {
r1 = rf(app)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// PluginStore is an autogenerated mock type for the PluginStore type
type PluginStore struct {
mock.Mock
}
// CompareAndDelete provides a mock function with given fields: keyVal, oldValue
func (_m *PluginStore) CompareAndDelete(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error) {
ret := _m.Called(keyVal, oldValue)
var r0 bool
if rf, ok := ret.Get(0).(func(*model.PluginKeyValue, []byte) bool); ok {
r0 = rf(keyVal, oldValue)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.PluginKeyValue, []byte) error); ok {
r1 = rf(keyVal, oldValue)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CompareAndSet provides a mock function with given fields: keyVal, oldValue
func (_m *PluginStore) CompareAndSet(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error) {
ret := _m.Called(keyVal, oldValue)
var r0 bool
if rf, ok := ret.Get(0).(func(*model.PluginKeyValue, []byte) bool); ok {
r0 = rf(keyVal, oldValue)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.PluginKeyValue, []byte) error); ok {
r1 = rf(keyVal, oldValue)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: pluginID, key
func (_m *PluginStore) Delete(pluginID string, key string) error {
ret := _m.Called(pluginID, key)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(pluginID, key)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteAllExpired provides a mock function with given fields:
func (_m *PluginStore) DeleteAllExpired() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteAllForPlugin provides a mock function with given fields: PluginID
func (_m *PluginStore) DeleteAllForPlugin(PluginID string) error {
ret := _m.Called(PluginID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(PluginID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: pluginID, key
func (_m *PluginStore) Get(pluginID string, key string) (*model.PluginKeyValue, error) {
ret := _m.Called(pluginID, key)
var r0 *model.PluginKeyValue
if rf, ok := ret.Get(0).(func(string, string) *model.PluginKeyValue); ok {
r0 = rf(pluginID, key)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PluginKeyValue)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(pluginID, key)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// List provides a mock function with given fields: pluginID, page, perPage
func (_m *PluginStore) List(pluginID string, page int, perPage int) ([]string, error) {
ret := _m.Called(pluginID, page, perPage)
var r0 []string
if rf, ok := ret.Get(0).(func(string, int, int) []string); ok {
r0 = rf(pluginID, page, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(pluginID, page, perPage)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveOrUpdate provides a mock function with given fields: keyVal
func (_m *PluginStore) SaveOrUpdate(keyVal *model.PluginKeyValue) (*model.PluginKeyValue, error) {
ret := _m.Called(keyVal)
var r0 *model.PluginKeyValue
if rf, ok := ret.Get(0).(func(*model.PluginKeyValue) *model.PluginKeyValue); ok {
r0 = rf(keyVal)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PluginKeyValue)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.PluginKeyValue) error); ok {
r1 = rf(keyVal)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SetWithOptions provides a mock function with given fields: pluginID, key, value, options
func (_m *PluginStore) SetWithOptions(pluginID string, key string, value []byte, options model.PluginKVSetOptions) (bool, error) {
ret := _m.Called(pluginID, key, value, options)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string, []byte, model.PluginKVSetOptions) bool); ok {
r0 = rf(pluginID, key, value, options)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, []byte, model.PluginKVSetOptions) error); ok {
r1 = rf(pluginID, key, value, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// PostAcknowledgementStore is an autogenerated mock type for the PostAcknowledgementStore type
type PostAcknowledgementStore struct {
mock.Mock
}
// Delete provides a mock function with given fields: acknowledgement
func (_m *PostAcknowledgementStore) Delete(acknowledgement *model.PostAcknowledgement) error {
ret := _m.Called(acknowledgement)
var r0 error
if rf, ok := ret.Get(0).(func(*model.PostAcknowledgement) error); ok {
r0 = rf(acknowledgement)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: postID, userID
func (_m *PostAcknowledgementStore) Get(postID string, userID string) (*model.PostAcknowledgement, error) {
ret := _m.Called(postID, userID)
var r0 *model.PostAcknowledgement
if rf, ok := ret.Get(0).(func(string, string) *model.PostAcknowledgement); ok {
r0 = rf(postID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostAcknowledgement)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(postID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForPost provides a mock function with given fields: postID
func (_m *PostAcknowledgementStore) GetForPost(postID string) ([]*model.PostAcknowledgement, error) {
ret := _m.Called(postID)
var r0 []*model.PostAcknowledgement
if rf, ok := ret.Get(0).(func(string) []*model.PostAcknowledgement); ok {
r0 = rf(postID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.PostAcknowledgement)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(postID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForPosts provides a mock function with given fields: postIds
func (_m *PostAcknowledgementStore) GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) {
ret := _m.Called(postIds)
var r0 []*model.PostAcknowledgement
if rf, ok := ret.Get(0).(func([]string) []*model.PostAcknowledgement); ok {
r0 = rf(postIds)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.PostAcknowledgement)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(postIds)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: postID, userID, acknowledgedAt
func (_m *PostAcknowledgementStore) Save(postID string, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) {
ret := _m.Called(postID, userID, acknowledgedAt)
var r0 *model.PostAcknowledgement
if rf, ok := ret.Get(0).(func(string, string, int64) *model.PostAcknowledgement); ok {
r0 = rf(postID, userID, acknowledgedAt)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostAcknowledgement)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int64) error); ok {
r1 = rf(postID, userID, acknowledgedAt)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// PostPriorityStore is an autogenerated mock type for the PostPriorityStore type
type PostPriorityStore struct {
mock.Mock
}
// GetForPost provides a mock function with given fields: postId
func (_m *PostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) {
ret := _m.Called(postId)
var r0 *model.PostPriority
if rf, ok := ret.Get(0).(func(string) *model.PostPriority); ok {
r0 = rf(postId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostPriority)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(postId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForPosts provides a mock function with given fields: ids
func (_m *PostPriorityStore) GetForPosts(ids []string) ([]*model.PostPriority, error) {
ret := _m.Called(ids)
var r0 []*model.PostPriority
if rf, ok := ret.Get(0).(func([]string) []*model.PostPriority); ok {
r0 = rf(ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.PostPriority)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
context "context"
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
store "github.com/mattermost/mattermost-server/v6/server/channels/store"
)
// PostStore is an autogenerated mock type for the PostStore type
type PostStore struct {
mock.Mock
}
// AnalyticsPostCount provides a mock function with given fields: options
func (_m *PostStore) AnalyticsPostCount(options *model.PostCountOptions) (int64, error) {
ret := _m.Called(options)
var r0 int64
if rf, ok := ret.Get(0).(func(*model.PostCountOptions) int64); ok {
r0 = rf(options)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.PostCountOptions) error); ok {
r1 = rf(options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AnalyticsPostCountsByDay provides a mock function with given fields: options
func (_m *PostStore) AnalyticsPostCountsByDay(options *model.AnalyticsPostCountsOptions) (model.AnalyticsRows, error) {
ret := _m.Called(options)
var r0 model.AnalyticsRows
if rf, ok := ret.Get(0).(func(*model.AnalyticsPostCountsOptions) model.AnalyticsRows); ok {
r0 = rf(options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.AnalyticsRows)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.AnalyticsPostCountsOptions) error); ok {
r1 = rf(options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AnalyticsUserCountsWithPostsByDay provides a mock function with given fields: teamID
func (_m *PostStore) AnalyticsUserCountsWithPostsByDay(teamID string) (model.AnalyticsRows, error) {
ret := _m.Called(teamID)
var r0 model.AnalyticsRows
if rf, ok := ret.Get(0).(func(string) model.AnalyticsRows); ok {
r0 = rf(teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.AnalyticsRows)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ClearCaches provides a mock function with given fields:
func (_m *PostStore) ClearCaches() {
_m.Called()
}
// Delete provides a mock function with given fields: postID, timestamp, deleteByID
func (_m *PostStore) Delete(postID string, timestamp int64, deleteByID string) error {
ret := _m.Called(postID, timestamp, deleteByID)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64, string) error); ok {
r0 = rf(postID, timestamp, deleteByID)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteOrphanedRows provides a mock function with given fields: limit
func (_m *PostStore) DeleteOrphanedRows(limit int) (int64, error) {
ret := _m.Called(limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int) int64); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: ctx, id, opts, userID, sanitizeOptions
func (_m *PostStore) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) {
ret := _m.Called(ctx, id, opts, userID, sanitizeOptions)
var r0 *model.PostList
if rf, ok := ret.Get(0).(func(context.Context, string, model.GetPostsOptions, string, map[string]bool) *model.PostList); ok {
r0 = rf(ctx, id, opts, userID, sanitizeOptions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, model.GetPostsOptions, string, map[string]bool) error); ok {
r1 = rf(ctx, id, opts, userID, sanitizeOptions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetDirectPostParentsForExportAfter provides a mock function with given fields: limit, afterID
func (_m *PostStore) GetDirectPostParentsForExportAfter(limit int, afterID string) ([]*model.DirectPostForExport, error) {
ret := _m.Called(limit, afterID)
var r0 []*model.DirectPostForExport
if rf, ok := ret.Get(0).(func(int, string) []*model.DirectPostForExport); ok {
r0 = rf(limit, afterID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.DirectPostForExport)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, string) error); ok {
r1 = rf(limit, afterID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetEditHistoryForPost provides a mock function with given fields: postId
func (_m *PostStore) GetEditHistoryForPost(postId string) ([]*model.Post, error) {
ret := _m.Called(postId)
var r0 []*model.Post
if rf, ok := ret.Get(0).(func(string) []*model.Post); ok {
r0 = rf(postId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Post)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(postId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetEtag provides a mock function with given fields: channelID, allowFromCache, collapsedThreads
func (_m *PostStore) GetEtag(channelID string, allowFromCache bool, collapsedThreads bool) string {
ret := _m.Called(channelID, allowFromCache, collapsedThreads)
var r0 string
if rf, ok := ret.Get(0).(func(string, bool, bool) string); ok {
r0 = rf(channelID, allowFromCache, collapsedThreads)
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// GetFlaggedPosts provides a mock function with given fields: userID, offset, limit
func (_m *PostStore) GetFlaggedPosts(userID string, offset int, limit int) (*model.PostList, error) {
ret := _m.Called(userID, offset, limit)
var r0 *model.PostList
if rf, ok := ret.Get(0).(func(string, int, int) *model.PostList); ok {
r0 = rf(userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetFlaggedPostsForChannel provides a mock function with given fields: userID, channelID, offset, limit
func (_m *PostStore) GetFlaggedPostsForChannel(userID string, channelID string, offset int, limit int) (*model.PostList, error) {
ret := _m.Called(userID, channelID, offset, limit)
var r0 *model.PostList
if rf, ok := ret.Get(0).(func(string, string, int, int) *model.PostList); ok {
r0 = rf(userID, channelID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int, int) error); ok {
r1 = rf(userID, channelID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetFlaggedPostsForTeam provides a mock function with given fields: userID, teamID, offset, limit
func (_m *PostStore) GetFlaggedPostsForTeam(userID string, teamID string, offset int, limit int) (*model.PostList, error) {
ret := _m.Called(userID, teamID, offset, limit)
var r0 *model.PostList
if rf, ok := ret.Get(0).(func(string, string, int, int) *model.PostList); ok {
r0 = rf(userID, teamID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int, int) error); ok {
r1 = rf(userID, teamID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMaxPostSize provides a mock function with given fields:
func (_m *PostStore) GetMaxPostSize() int {
ret := _m.Called()
var r0 int
if rf, ok := ret.Get(0).(func() int); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int)
}
return r0
}
// GetNthRecentPostTime provides a mock function with given fields: n
func (_m *PostStore) GetNthRecentPostTime(n int64) (int64, error) {
ret := _m.Called(n)
var r0 int64
if rf, ok := ret.Get(0).(func(int64) int64); ok {
r0 = rf(n)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int64) error); ok {
r1 = rf(n)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetOldest provides a mock function with given fields:
func (_m *PostStore) GetOldest() (*model.Post, error) {
ret := _m.Called()
var r0 *model.Post
if rf, ok := ret.Get(0).(func() *model.Post); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Post)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetOldestEntityCreationTime provides a mock function with given fields:
func (_m *PostStore) GetOldestEntityCreationTime() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetParentsForExportAfter provides a mock function with given fields: limit, afterID
func (_m *PostStore) GetParentsForExportAfter(limit int, afterID string) ([]*model.PostForExport, error) {
ret := _m.Called(limit, afterID)
var r0 []*model.PostForExport
if rf, ok := ret.Get(0).(func(int, string) []*model.PostForExport); ok {
r0 = rf(limit, afterID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.PostForExport)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, string) error); ok {
r1 = rf(limit, afterID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostAfterTime provides a mock function with given fields: channelID, timestamp, collapsedThreads
func (_m *PostStore) GetPostAfterTime(channelID string, timestamp int64, collapsedThreads bool) (*model.Post, error) {
ret := _m.Called(channelID, timestamp, collapsedThreads)
var r0 *model.Post
if rf, ok := ret.Get(0).(func(string, int64, bool) *model.Post); ok {
r0 = rf(channelID, timestamp, collapsedThreads)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Post)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64, bool) error); ok {
r1 = rf(channelID, timestamp, collapsedThreads)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostIdAfterTime provides a mock function with given fields: channelID, timestamp, collapsedThreads
func (_m *PostStore) GetPostIdAfterTime(channelID string, timestamp int64, collapsedThreads bool) (string, error) {
ret := _m.Called(channelID, timestamp, collapsedThreads)
var r0 string
if rf, ok := ret.Get(0).(func(string, int64, bool) string); ok {
r0 = rf(channelID, timestamp, collapsedThreads)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64, bool) error); ok {
r1 = rf(channelID, timestamp, collapsedThreads)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostIdBeforeTime provides a mock function with given fields: channelID, timestamp, collapsedThreads
func (_m *PostStore) GetPostIdBeforeTime(channelID string, timestamp int64, collapsedThreads bool) (string, error) {
ret := _m.Called(channelID, timestamp, collapsedThreads)
var r0 string
if rf, ok := ret.Get(0).(func(string, int64, bool) string); ok {
r0 = rf(channelID, timestamp, collapsedThreads)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64, bool) error); ok {
r1 = rf(channelID, timestamp, collapsedThreads)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostReminderMetadata provides a mock function with given fields: postID
func (_m *PostStore) GetPostReminderMetadata(postID string) (*store.PostReminderMetadata, error) {
ret := _m.Called(postID)
var r0 *store.PostReminderMetadata
if rf, ok := ret.Get(0).(func(string) *store.PostReminderMetadata); ok {
r0 = rf(postID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*store.PostReminderMetadata)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(postID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostReminders provides a mock function with given fields: now
func (_m *PostStore) GetPostReminders(now int64) ([]*model.PostReminder, error) {
ret := _m.Called(now)
var r0 []*model.PostReminder
if rf, ok := ret.Get(0).(func(int64) []*model.PostReminder); ok {
r0 = rf(now)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.PostReminder)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64) error); ok {
r1 = rf(now)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPosts provides a mock function with given fields: options, allowFromCache, sanitizeOptions
func (_m *PostStore) GetPosts(options model.GetPostsOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
ret := _m.Called(options, allowFromCache, sanitizeOptions)
var r0 *model.PostList
if rf, ok := ret.Get(0).(func(model.GetPostsOptions, bool, map[string]bool) *model.PostList); ok {
r0 = rf(options, allowFromCache, sanitizeOptions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(model.GetPostsOptions, bool, map[string]bool) error); ok {
r1 = rf(options, allowFromCache, sanitizeOptions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostsAfter provides a mock function with given fields: options, sanitizeOptions
func (_m *PostStore) GetPostsAfter(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
ret := _m.Called(options, sanitizeOptions)
var r0 *model.PostList
if rf, ok := ret.Get(0).(func(model.GetPostsOptions, map[string]bool) *model.PostList); ok {
r0 = rf(options, sanitizeOptions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(model.GetPostsOptions, map[string]bool) error); ok {
r1 = rf(options, sanitizeOptions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostsBatchForIndexing provides a mock function with given fields: startTime, startPostID, limit
func (_m *PostStore) GetPostsBatchForIndexing(startTime int64, startPostID string, limit int) ([]*model.PostForIndexing, error) {
ret := _m.Called(startTime, startPostID, limit)
var r0 []*model.PostForIndexing
if rf, ok := ret.Get(0).(func(int64, string, int) []*model.PostForIndexing); ok {
r0 = rf(startTime, startPostID, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.PostForIndexing)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, string, int) error); ok {
r1 = rf(startTime, startPostID, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostsBefore provides a mock function with given fields: options, sanitizeOptions
func (_m *PostStore) GetPostsBefore(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
ret := _m.Called(options, sanitizeOptions)
var r0 *model.PostList
if rf, ok := ret.Get(0).(func(model.GetPostsOptions, map[string]bool) *model.PostList); ok {
r0 = rf(options, sanitizeOptions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(model.GetPostsOptions, map[string]bool) error); ok {
r1 = rf(options, sanitizeOptions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostsByIds provides a mock function with given fields: postIds
func (_m *PostStore) GetPostsByIds(postIds []string) ([]*model.Post, error) {
ret := _m.Called(postIds)
var r0 []*model.Post
if rf, ok := ret.Get(0).(func([]string) []*model.Post); ok {
r0 = rf(postIds)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Post)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(postIds)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostsByThread provides a mock function with given fields: threadID, since
func (_m *PostStore) GetPostsByThread(threadID string, since int64) ([]*model.Post, error) {
ret := _m.Called(threadID, since)
var r0 []*model.Post
if rf, ok := ret.Get(0).(func(string, int64) []*model.Post); ok {
r0 = rf(threadID, since)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Post)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64) error); ok {
r1 = rf(threadID, since)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostsCreatedAt provides a mock function with given fields: channelID, timestamp
func (_m *PostStore) GetPostsCreatedAt(channelID string, timestamp int64) ([]*model.Post, error) {
ret := _m.Called(channelID, timestamp)
var r0 []*model.Post
if rf, ok := ret.Get(0).(func(string, int64) []*model.Post); ok {
r0 = rf(channelID, timestamp)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Post)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64) error); ok {
r1 = rf(channelID, timestamp)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostsSince provides a mock function with given fields: options, allowFromCache, sanitizeOptions
func (_m *PostStore) GetPostsSince(options model.GetPostsSinceOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
ret := _m.Called(options, allowFromCache, sanitizeOptions)
var r0 *model.PostList
if rf, ok := ret.Get(0).(func(model.GetPostsSinceOptions, bool, map[string]bool) *model.PostList); ok {
r0 = rf(options, allowFromCache, sanitizeOptions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(model.GetPostsSinceOptions, bool, map[string]bool) error); ok {
r1 = rf(options, allowFromCache, sanitizeOptions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPostsSinceForSync provides a mock function with given fields: options, cursor, limit
func (_m *PostStore) GetPostsSinceForSync(options model.GetPostsSinceForSyncOptions, cursor model.GetPostsSinceForSyncCursor, limit int) ([]*model.Post, model.GetPostsSinceForSyncCursor, error) {
ret := _m.Called(options, cursor, limit)
var r0 []*model.Post
if rf, ok := ret.Get(0).(func(model.GetPostsSinceForSyncOptions, model.GetPostsSinceForSyncCursor, int) []*model.Post); ok {
r0 = rf(options, cursor, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Post)
}
}
var r1 model.GetPostsSinceForSyncCursor
if rf, ok := ret.Get(1).(func(model.GetPostsSinceForSyncOptions, model.GetPostsSinceForSyncCursor, int) model.GetPostsSinceForSyncCursor); ok {
r1 = rf(options, cursor, limit)
} else {
r1 = ret.Get(1).(model.GetPostsSinceForSyncCursor)
}
var r2 error
if rf, ok := ret.Get(2).(func(model.GetPostsSinceForSyncOptions, model.GetPostsSinceForSyncCursor, int) error); ok {
r2 = rf(options, cursor, limit)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// GetRecentSearchesForUser provides a mock function with given fields: userID
func (_m *PostStore) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) {
ret := _m.Called(userID)
var r0 []*model.SearchParams
if rf, ok := ret.Get(0).(func(string) []*model.SearchParams); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.SearchParams)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRepliesForExport provides a mock function with given fields: parentID
func (_m *PostStore) GetRepliesForExport(parentID string) ([]*model.ReplyForExport, error) {
ret := _m.Called(parentID)
var r0 []*model.ReplyForExport
if rf, ok := ret.Get(0).(func(string) []*model.ReplyForExport); ok {
r0 = rf(parentID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ReplyForExport)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(parentID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSingle provides a mock function with given fields: id, inclDeleted
func (_m *PostStore) GetSingle(id string, inclDeleted bool) (*model.Post, error) {
ret := _m.Called(id, inclDeleted)
var r0 *model.Post
if rf, ok := ret.Get(0).(func(string, bool) *model.Post); ok {
r0 = rf(id, inclDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Post)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(id, inclDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopDMsForUserSince provides a mock function with given fields: userID, since, offset, limit
func (_m *PostStore) GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error) {
ret := _m.Called(userID, since, offset, limit)
var r0 *model.TopDMList
if rf, ok := ret.Get(0).(func(string, int64, int, int) *model.TopDMList); ok {
r0 = rf(userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopDMList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64, int, int) error); ok {
r1 = rf(userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// HasAutoResponsePostByUserSince provides a mock function with given fields: options, userId
func (_m *PostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
ret := _m.Called(options, userId)
var r0 bool
if rf, ok := ret.Get(0).(func(model.GetPostsSinceOptions, string) bool); ok {
r0 = rf(options, userId)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(model.GetPostsSinceOptions, string) error); ok {
r1 = rf(options, userId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// InvalidateLastPostTimeCache provides a mock function with given fields: channelID
func (_m *PostStore) InvalidateLastPostTimeCache(channelID string) {
_m.Called(channelID)
}
// LogRecentSearch provides a mock function with given fields: userID, searchQuery, createAt
func (_m *PostStore) LogRecentSearch(userID string, searchQuery []byte, createAt int64) error {
ret := _m.Called(userID, searchQuery, createAt)
var r0 error
if rf, ok := ret.Get(0).(func(string, []byte, int64) error); ok {
r0 = rf(userID, searchQuery, createAt)
} else {
r0 = ret.Error(0)
}
return r0
}
// Overwrite provides a mock function with given fields: post
func (_m *PostStore) Overwrite(post *model.Post) (*model.Post, error) {
ret := _m.Called(post)
var r0 *model.Post
if rf, ok := ret.Get(0).(func(*model.Post) *model.Post); ok {
r0 = rf(post)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Post)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Post) error); ok {
r1 = rf(post)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// OverwriteMultiple provides a mock function with given fields: posts
func (_m *PostStore) OverwriteMultiple(posts []*model.Post) ([]*model.Post, int, error) {
ret := _m.Called(posts)
var r0 []*model.Post
if rf, ok := ret.Get(0).(func([]*model.Post) []*model.Post); ok {
r0 = rf(posts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Post)
}
}
var r1 int
if rf, ok := ret.Get(1).(func([]*model.Post) int); ok {
r1 = rf(posts)
} else {
r1 = ret.Get(1).(int)
}
var r2 error
if rf, ok := ret.Get(2).(func([]*model.Post) error); ok {
r2 = rf(posts)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// PermanentDeleteBatch provides a mock function with given fields: endTime, limit
func (_m *PostStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
ret := _m.Called(endTime, limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64) int64); ok {
r0 = rf(endTime, limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, int64) error); ok {
r1 = rf(endTime, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteBatchForRetentionPolicies provides a mock function with given fields: now, globalPolicyEndTime, limit, cursor
func (_m *PostStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
ret := _m.Called(now, globalPolicyEndTime, limit, cursor)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64, int64, model.RetentionPolicyCursor) int64); ok {
r0 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r0 = ret.Get(0).(int64)
}
var r1 model.RetentionPolicyCursor
if rf, ok := ret.Get(1).(func(int64, int64, int64, model.RetentionPolicyCursor) model.RetentionPolicyCursor); ok {
r1 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r1 = ret.Get(1).(model.RetentionPolicyCursor)
}
var r2 error
if rf, ok := ret.Get(2).(func(int64, int64, int64, model.RetentionPolicyCursor) error); ok {
r2 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// PermanentDeleteByChannel provides a mock function with given fields: channelID
func (_m *PostStore) PermanentDeleteByChannel(channelID string) error {
ret := _m.Called(channelID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(channelID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteByUser provides a mock function with given fields: userID
func (_m *PostStore) PermanentDeleteByUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: post
func (_m *PostStore) Save(post *model.Post) (*model.Post, error) {
ret := _m.Called(post)
var r0 *model.Post
if rf, ok := ret.Get(0).(func(*model.Post) *model.Post); ok {
r0 = rf(post)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Post)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Post) error); ok {
r1 = rf(post)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveMultiple provides a mock function with given fields: posts
func (_m *PostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, int, error) {
ret := _m.Called(posts)
var r0 []*model.Post
if rf, ok := ret.Get(0).(func([]*model.Post) []*model.Post); ok {
r0 = rf(posts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Post)
}
}
var r1 int
if rf, ok := ret.Get(1).(func([]*model.Post) int); ok {
r1 = rf(posts)
} else {
r1 = ret.Get(1).(int)
}
var r2 error
if rf, ok := ret.Get(2).(func([]*model.Post) error); ok {
r2 = rf(posts)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// Search provides a mock function with given fields: teamID, userID, params
func (_m *PostStore) Search(teamID string, userID string, params *model.SearchParams) (*model.PostList, error) {
ret := _m.Called(teamID, userID, params)
var r0 *model.PostList
if rf, ok := ret.Get(0).(func(string, string, *model.SearchParams) *model.PostList); ok {
r0 = rf(teamID, userID, params)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *model.SearchParams) error); ok {
r1 = rf(teamID, userID, params)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchPostsForUser provides a mock function with given fields: paramsList, userID, teamID, page, perPage
func (_m *PostStore) SearchPostsForUser(paramsList []*model.SearchParams, userID string, teamID string, page int, perPage int) (*model.PostSearchResults, error) {
ret := _m.Called(paramsList, userID, teamID, page, perPage)
var r0 *model.PostSearchResults
if rf, ok := ret.Get(0).(func([]*model.SearchParams, string, string, int, int) *model.PostSearchResults); ok {
r0 = rf(paramsList, userID, teamID, page, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostSearchResults)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]*model.SearchParams, string, string, int, int) error); ok {
r1 = rf(paramsList, userID, teamID, page, perPage)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SetPostReminder provides a mock function with given fields: reminder
func (_m *PostStore) SetPostReminder(reminder *model.PostReminder) error {
ret := _m.Called(reminder)
var r0 error
if rf, ok := ret.Get(0).(func(*model.PostReminder) error); ok {
r0 = rf(reminder)
} else {
r0 = ret.Error(0)
}
return r0
}
// Update provides a mock function with given fields: newPost, oldPost
func (_m *PostStore) Update(newPost *model.Post, oldPost *model.Post) (*model.Post, error) {
ret := _m.Called(newPost, oldPost)
var r0 *model.Post
if rf, ok := ret.Get(0).(func(*model.Post, *model.Post) *model.Post); ok {
r0 = rf(newPost, oldPost)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Post)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Post, *model.Post) error); ok {
r1 = rf(newPost, oldPost)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// PreferenceStore is an autogenerated mock type for the PreferenceStore type
type PreferenceStore struct {
mock.Mock
}
// CleanupFlagsBatch provides a mock function with given fields: limit
func (_m *PreferenceStore) CleanupFlagsBatch(limit int64) (int64, error) {
ret := _m.Called(limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int64) int64); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int64) error); ok {
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: userID, category, name
func (_m *PreferenceStore) Delete(userID string, category string, name string) error {
ret := _m.Called(userID, category, name)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = rf(userID, category, name)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteCategory provides a mock function with given fields: userID, category
func (_m *PreferenceStore) DeleteCategory(userID string, category string) error {
ret := _m.Called(userID, category)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(userID, category)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteCategoryAndName provides a mock function with given fields: category, name
func (_m *PreferenceStore) DeleteCategoryAndName(category string, name string) error {
ret := _m.Called(category, name)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(category, name)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteOrphanedRows provides a mock function with given fields: limit
func (_m *PreferenceStore) DeleteOrphanedRows(limit int) (int64, error) {
ret := _m.Called(limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int) int64); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: userID, category, name
func (_m *PreferenceStore) Get(userID string, category string, name string) (*model.Preference, error) {
ret := _m.Called(userID, category, name)
var r0 *model.Preference
if rf, ok := ret.Get(0).(func(string, string, string) *model.Preference); ok {
r0 = rf(userID, category, name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Preference)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string) error); ok {
r1 = rf(userID, category, name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields: userID
func (_m *PreferenceStore) GetAll(userID string) (model.Preferences, error) {
ret := _m.Called(userID)
var r0 model.Preferences
if rf, ok := ret.Get(0).(func(string) model.Preferences); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Preferences)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetCategory provides a mock function with given fields: userID, category
func (_m *PreferenceStore) GetCategory(userID string, category string) (model.Preferences, error) {
ret := _m.Called(userID, category)
var r0 model.Preferences
if rf, ok := ret.Get(0).(func(string, string) model.Preferences); ok {
r0 = rf(userID, category)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Preferences)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, category)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetCategoryAndName provides a mock function with given fields: category, nane
func (_m *PreferenceStore) GetCategoryAndName(category string, nane string) (model.Preferences, error) {
ret := _m.Called(category, nane)
var r0 model.Preferences
if rf, ok := ret.Get(0).(func(string, string) model.Preferences); ok {
r0 = rf(category, nane)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.Preferences)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(category, nane)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteByUser provides a mock function with given fields: userID
func (_m *PreferenceStore) PermanentDeleteByUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: preferences
func (_m *PreferenceStore) Save(preferences model.Preferences) error {
ret := _m.Called(preferences)
var r0 error
if rf, ok := ret.Get(0).(func(model.Preferences) error); ok {
r0 = rf(preferences)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// ProductNoticesStore is an autogenerated mock type for the ProductNoticesStore type
type ProductNoticesStore struct {
mock.Mock
}
// Clear provides a mock function with given fields: notices
func (_m *ProductNoticesStore) Clear(notices []string) error {
ret := _m.Called(notices)
var r0 error
if rf, ok := ret.Get(0).(func([]string) error); ok {
r0 = rf(notices)
} else {
r0 = ret.Error(0)
}
return r0
}
// ClearOldNotices provides a mock function with given fields: currentNotices
func (_m *ProductNoticesStore) ClearOldNotices(currentNotices model.ProductNotices) error {
ret := _m.Called(currentNotices)
var r0 error
if rf, ok := ret.Get(0).(func(model.ProductNotices) error); ok {
r0 = rf(currentNotices)
} else {
r0 = ret.Error(0)
}
return r0
}
// GetViews provides a mock function with given fields: userID
func (_m *ProductNoticesStore) GetViews(userID string) ([]model.ProductNoticeViewState, error) {
ret := _m.Called(userID)
var r0 []model.ProductNoticeViewState
if rf, ok := ret.Get(0).(func(string) []model.ProductNoticeViewState); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]model.ProductNoticeViewState)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// View provides a mock function with given fields: userID, notices
func (_m *ProductNoticesStore) View(userID string, notices []string) error {
ret := _m.Called(userID, notices)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(userID, notices)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// ReactionStore is an autogenerated mock type for the ReactionStore type
type ReactionStore struct {
mock.Mock
}
// BulkGetForPosts provides a mock function with given fields: postIds
func (_m *ReactionStore) BulkGetForPosts(postIds []string) ([]*model.Reaction, error) {
ret := _m.Called(postIds)
var r0 []*model.Reaction
if rf, ok := ret.Get(0).(func([]string) []*model.Reaction); ok {
r0 = rf(postIds)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Reaction)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(postIds)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: reaction
func (_m *ReactionStore) Delete(reaction *model.Reaction) (*model.Reaction, error) {
ret := _m.Called(reaction)
var r0 *model.Reaction
if rf, ok := ret.Get(0).(func(*model.Reaction) *model.Reaction); ok {
r0 = rf(reaction)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Reaction)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Reaction) error); ok {
r1 = rf(reaction)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteAllWithEmojiName provides a mock function with given fields: emojiName
func (_m *ReactionStore) DeleteAllWithEmojiName(emojiName string) error {
ret := _m.Called(emojiName)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(emojiName)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteOrphanedRows provides a mock function with given fields: limit
func (_m *ReactionStore) DeleteOrphanedRows(limit int) (int64, error) {
ret := _m.Called(limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int) int64); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForPost provides a mock function with given fields: postID, allowFromCache
func (_m *ReactionStore) GetForPost(postID string, allowFromCache bool) ([]*model.Reaction, error) {
ret := _m.Called(postID, allowFromCache)
var r0 []*model.Reaction
if rf, ok := ret.Get(0).(func(string, bool) []*model.Reaction); ok {
r0 = rf(postID, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Reaction)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(postID, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForPostSince provides a mock function with given fields: postId, since, excludeRemoteId, inclDeleted
func (_m *ReactionStore) GetForPostSince(postId string, since int64, excludeRemoteId string, inclDeleted bool) ([]*model.Reaction, error) {
ret := _m.Called(postId, since, excludeRemoteId, inclDeleted)
var r0 []*model.Reaction
if rf, ok := ret.Get(0).(func(string, int64, string, bool) []*model.Reaction); ok {
r0 = rf(postId, since, excludeRemoteId, inclDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Reaction)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64, string, bool) error); ok {
r1 = rf(postId, since, excludeRemoteId, inclDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopForTeamSince provides a mock function with given fields: teamID, userID, since, offset, limit
func (_m *ReactionStore) GetTopForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
ret := _m.Called(teamID, userID, since, offset, limit)
var r0 *model.TopReactionList
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopReactionList); ok {
r0 = rf(teamID, userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopReactionList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(teamID, userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopForUserSince provides a mock function with given fields: userID, teamID, since, offset, limit
func (_m *ReactionStore) GetTopForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
ret := _m.Called(userID, teamID, since, offset, limit)
var r0 *model.TopReactionList
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopReactionList); ok {
r0 = rf(userID, teamID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopReactionList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(userID, teamID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteBatch provides a mock function with given fields: endTime, limit
func (_m *ReactionStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
ret := _m.Called(endTime, limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64) int64); ok {
r0 = rf(endTime, limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, int64) error); ok {
r1 = rf(endTime, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: reaction
func (_m *ReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) {
ret := _m.Called(reaction)
var r0 *model.Reaction
if rf, ok := ret.Get(0).(func(*model.Reaction) *model.Reaction); ok {
r0 = rf(reaction)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Reaction)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Reaction) error); ok {
r1 = rf(reaction)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// RemoteClusterStore is an autogenerated mock type for the RemoteClusterStore type
type RemoteClusterStore struct {
mock.Mock
}
// Delete provides a mock function with given fields: remoteClusterId
func (_m *RemoteClusterStore) Delete(remoteClusterId string) (bool, error) {
ret := _m.Called(remoteClusterId)
var r0 bool
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(remoteClusterId)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(remoteClusterId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: remoteClusterId
func (_m *RemoteClusterStore) Get(remoteClusterId string) (*model.RemoteCluster, error) {
ret := _m.Called(remoteClusterId)
var r0 *model.RemoteCluster
if rf, ok := ret.Get(0).(func(string) *model.RemoteCluster); ok {
r0 = rf(remoteClusterId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RemoteCluster)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(remoteClusterId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields: filter
func (_m *RemoteClusterStore) GetAll(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
ret := _m.Called(filter)
var r0 []*model.RemoteCluster
if rf, ok := ret.Get(0).(func(model.RemoteClusterQueryFilter) []*model.RemoteCluster); ok {
r0 = rf(filter)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.RemoteCluster)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(model.RemoteClusterQueryFilter) error); ok {
r1 = rf(filter)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: rc
func (_m *RemoteClusterStore) Save(rc *model.RemoteCluster) (*model.RemoteCluster, error) {
ret := _m.Called(rc)
var r0 *model.RemoteCluster
if rf, ok := ret.Get(0).(func(*model.RemoteCluster) *model.RemoteCluster); ok {
r0 = rf(rc)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RemoteCluster)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.RemoteCluster) error); ok {
r1 = rf(rc)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SetLastPingAt provides a mock function with given fields: remoteClusterId
func (_m *RemoteClusterStore) SetLastPingAt(remoteClusterId string) error {
ret := _m.Called(remoteClusterId)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(remoteClusterId)
} else {
r0 = ret.Error(0)
}
return r0
}
// Update provides a mock function with given fields: rc
func (_m *RemoteClusterStore) Update(rc *model.RemoteCluster) (*model.RemoteCluster, error) {
ret := _m.Called(rc)
var r0 *model.RemoteCluster
if rf, ok := ret.Get(0).(func(*model.RemoteCluster) *model.RemoteCluster); ok {
r0 = rf(rc)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RemoteCluster)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.RemoteCluster) error); ok {
r1 = rf(rc)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateTopics provides a mock function with given fields: remoteClusterId, topics
func (_m *RemoteClusterStore) UpdateTopics(remoteClusterId string, topics string) (*model.RemoteCluster, error) {
ret := _m.Called(remoteClusterId, topics)
var r0 *model.RemoteCluster
if rf, ok := ret.Get(0).(func(string, string) *model.RemoteCluster); ok {
r0 = rf(remoteClusterId, topics)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RemoteCluster)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(remoteClusterId, topics)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// RetentionPolicyStore is an autogenerated mock type for the RetentionPolicyStore type
type RetentionPolicyStore struct {
mock.Mock
}
// AddChannels provides a mock function with given fields: policyId, channelIds
func (_m *RetentionPolicyStore) AddChannels(policyId string, channelIds []string) error {
ret := _m.Called(policyId, channelIds)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(policyId, channelIds)
} else {
r0 = ret.Error(0)
}
return r0
}
// AddTeams provides a mock function with given fields: policyId, teamIds
func (_m *RetentionPolicyStore) AddTeams(policyId string, teamIds []string) error {
ret := _m.Called(policyId, teamIds)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(policyId, teamIds)
} else {
r0 = ret.Error(0)
}
return r0
}
// Delete provides a mock function with given fields: id
func (_m *RetentionPolicyStore) Delete(id string) error {
ret := _m.Called(id)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(id)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteOrphanedRows provides a mock function with given fields: limit
func (_m *RetentionPolicyStore) DeleteOrphanedRows(limit int) (int64, error) {
ret := _m.Called(limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int) int64); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: id
func (_m *RetentionPolicyStore) Get(id string) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
ret := _m.Called(id)
var r0 *model.RetentionPolicyWithTeamAndChannelCounts
if rf, ok := ret.Get(0).(func(string) *model.RetentionPolicyWithTeamAndChannelCounts); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RetentionPolicyWithTeamAndChannelCounts)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields: offset, limit
func (_m *RetentionPolicyStore) GetAll(offset int, limit int) ([]*model.RetentionPolicyWithTeamAndChannelCounts, error) {
ret := _m.Called(offset, limit)
var r0 []*model.RetentionPolicyWithTeamAndChannelCounts
if rf, ok := ret.Get(0).(func(int, int) []*model.RetentionPolicyWithTeamAndChannelCounts); ok {
r0 = rf(offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.RetentionPolicyWithTeamAndChannelCounts)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int) error); ok {
r1 = rf(offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelPoliciesCountForUser provides a mock function with given fields: userID
func (_m *RetentionPolicyStore) GetChannelPoliciesCountForUser(userID string) (int64, error) {
ret := _m.Called(userID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(userID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelPoliciesForUser provides a mock function with given fields: userID, offset, limit
func (_m *RetentionPolicyStore) GetChannelPoliciesForUser(userID string, offset int, limit int) ([]*model.RetentionPolicyForChannel, error) {
ret := _m.Called(userID, offset, limit)
var r0 []*model.RetentionPolicyForChannel
if rf, ok := ret.Get(0).(func(string, int, int) []*model.RetentionPolicyForChannel); ok {
r0 = rf(userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.RetentionPolicyForChannel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannels provides a mock function with given fields: policyId, offset, limit
func (_m *RetentionPolicyStore) GetChannels(policyId string, offset int, limit int) (model.ChannelListWithTeamData, error) {
ret := _m.Called(policyId, offset, limit)
var r0 model.ChannelListWithTeamData
if rf, ok := ret.Get(0).(func(string, int, int) model.ChannelListWithTeamData); ok {
r0 = rf(policyId, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.ChannelListWithTeamData)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(policyId, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelsCount provides a mock function with given fields: policyId
func (_m *RetentionPolicyStore) GetChannelsCount(policyId string) (int64, error) {
ret := _m.Called(policyId)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(policyId)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(policyId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetCount provides a mock function with given fields:
func (_m *RetentionPolicyStore) GetCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamPoliciesCountForUser provides a mock function with given fields: userID
func (_m *RetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) {
ret := _m.Called(userID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(userID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamPoliciesForUser provides a mock function with given fields: userID, offset, limit
func (_m *RetentionPolicyStore) GetTeamPoliciesForUser(userID string, offset int, limit int) ([]*model.RetentionPolicyForTeam, error) {
ret := _m.Called(userID, offset, limit)
var r0 []*model.RetentionPolicyForTeam
if rf, ok := ret.Get(0).(func(string, int, int) []*model.RetentionPolicyForTeam); ok {
r0 = rf(userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.RetentionPolicyForTeam)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeams provides a mock function with given fields: policyId, offset, limit
func (_m *RetentionPolicyStore) GetTeams(policyId string, offset int, limit int) ([]*model.Team, error) {
ret := _m.Called(policyId, offset, limit)
var r0 []*model.Team
if rf, ok := ret.Get(0).(func(string, int, int) []*model.Team); ok {
r0 = rf(policyId, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(policyId, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamsCount provides a mock function with given fields: policyId
func (_m *RetentionPolicyStore) GetTeamsCount(policyId string) (int64, error) {
ret := _m.Called(policyId)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(policyId)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(policyId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Patch provides a mock function with given fields: patch
func (_m *RetentionPolicyStore) Patch(patch *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
ret := _m.Called(patch)
var r0 *model.RetentionPolicyWithTeamAndChannelCounts
if rf, ok := ret.Get(0).(func(*model.RetentionPolicyWithTeamAndChannelIDs) *model.RetentionPolicyWithTeamAndChannelCounts); ok {
r0 = rf(patch)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RetentionPolicyWithTeamAndChannelCounts)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.RetentionPolicyWithTeamAndChannelIDs) error); ok {
r1 = rf(patch)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RemoveChannels provides a mock function with given fields: policyId, channelIds
func (_m *RetentionPolicyStore) RemoveChannels(policyId string, channelIds []string) error {
ret := _m.Called(policyId, channelIds)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(policyId, channelIds)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveTeams provides a mock function with given fields: policyId, teamIds
func (_m *RetentionPolicyStore) RemoveTeams(policyId string, teamIds []string) error {
ret := _m.Called(policyId, teamIds)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(policyId, teamIds)
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: policy
func (_m *RetentionPolicyStore) Save(policy *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
ret := _m.Called(policy)
var r0 *model.RetentionPolicyWithTeamAndChannelCounts
if rf, ok := ret.Get(0).(func(*model.RetentionPolicyWithTeamAndChannelIDs) *model.RetentionPolicyWithTeamAndChannelCounts); ok {
r0 = rf(policy)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RetentionPolicyWithTeamAndChannelCounts)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.RetentionPolicyWithTeamAndChannelIDs) error); ok {
r1 = rf(policy)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
context "context"
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// RoleStore is an autogenerated mock type for the RoleStore type
type RoleStore struct {
mock.Mock
}
// AllChannelSchemeRoles provides a mock function with given fields:
func (_m *RoleStore) AllChannelSchemeRoles() ([]*model.Role, error) {
ret := _m.Called()
var r0 []*model.Role
if rf, ok := ret.Get(0).(func() []*model.Role); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Role)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ChannelHigherScopedPermissions provides a mock function with given fields: roleNames
func (_m *RoleStore) ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, error) {
ret := _m.Called(roleNames)
var r0 map[string]*model.RolePermissions
if rf, ok := ret.Get(0).(func([]string) map[string]*model.RolePermissions); ok {
r0 = rf(roleNames)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]*model.RolePermissions)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(roleNames)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ChannelRolesUnderTeamRole provides a mock function with given fields: roleName
func (_m *RoleStore) ChannelRolesUnderTeamRole(roleName string) ([]*model.Role, error) {
ret := _m.Called(roleName)
var r0 []*model.Role
if rf, ok := ret.Get(0).(func(string) []*model.Role); ok {
r0 = rf(roleName)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Role)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(roleName)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: roleID
func (_m *RoleStore) Delete(roleID string) (*model.Role, error) {
ret := _m.Called(roleID)
var r0 *model.Role
if rf, ok := ret.Get(0).(func(string) *model.Role); ok {
r0 = rf(roleID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Role)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(roleID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: roleID
func (_m *RoleStore) Get(roleID string) (*model.Role, error) {
ret := _m.Called(roleID)
var r0 *model.Role
if rf, ok := ret.Get(0).(func(string) *model.Role); ok {
r0 = rf(roleID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Role)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(roleID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields:
func (_m *RoleStore) GetAll() ([]*model.Role, error) {
ret := _m.Called()
var r0 []*model.Role
if rf, ok := ret.Get(0).(func() []*model.Role); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Role)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByName provides a mock function with given fields: ctx, name
func (_m *RoleStore) GetByName(ctx context.Context, name string) (*model.Role, error) {
ret := _m.Called(ctx, name)
var r0 *model.Role
if rf, ok := ret.Get(0).(func(context.Context, string) *model.Role); ok {
r0 = rf(ctx, name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Role)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByNames provides a mock function with given fields: names
func (_m *RoleStore) GetByNames(names []string) ([]*model.Role, error) {
ret := _m.Called(names)
var r0 []*model.Role
if rf, ok := ret.Get(0).(func([]string) []*model.Role); ok {
r0 = rf(names)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Role)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(names)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteAll provides a mock function with given fields:
func (_m *RoleStore) PermanentDeleteAll() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: role
func (_m *RoleStore) Save(role *model.Role) (*model.Role, error) {
ret := _m.Called(role)
var r0 *model.Role
if rf, ok := ret.Get(0).(func(*model.Role) *model.Role); ok {
r0 = rf(role)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Role)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Role) error); ok {
r1 = rf(role)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// SchemeStore is an autogenerated mock type for the SchemeStore type
type SchemeStore struct {
mock.Mock
}
// CountByScope provides a mock function with given fields: scope
func (_m *SchemeStore) CountByScope(scope string) (int64, error) {
ret := _m.Called(scope)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(scope)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(scope)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountWithoutPermission provides a mock function with given fields: scope, permissionID, roleScope, roleType
func (_m *SchemeStore) CountWithoutPermission(scope string, permissionID string, roleScope model.RoleScope, roleType model.RoleType) (int64, error) {
ret := _m.Called(scope, permissionID, roleScope, roleType)
var r0 int64
if rf, ok := ret.Get(0).(func(string, string, model.RoleScope, model.RoleType) int64); ok {
r0 = rf(scope, permissionID, roleScope, roleType)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, model.RoleScope, model.RoleType) error); ok {
r1 = rf(scope, permissionID, roleScope, roleType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: schemeID
func (_m *SchemeStore) Delete(schemeID string) (*model.Scheme, error) {
ret := _m.Called(schemeID)
var r0 *model.Scheme
if rf, ok := ret.Get(0).(func(string) *model.Scheme); ok {
r0 = rf(schemeID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Scheme)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(schemeID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: schemeID
func (_m *SchemeStore) Get(schemeID string) (*model.Scheme, error) {
ret := _m.Called(schemeID)
var r0 *model.Scheme
if rf, ok := ret.Get(0).(func(string) *model.Scheme); ok {
r0 = rf(schemeID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Scheme)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(schemeID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllPage provides a mock function with given fields: scope, offset, limit
func (_m *SchemeStore) GetAllPage(scope string, offset int, limit int) ([]*model.Scheme, error) {
ret := _m.Called(scope, offset, limit)
var r0 []*model.Scheme
if rf, ok := ret.Get(0).(func(string, int, int) []*model.Scheme); ok {
r0 = rf(scope, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Scheme)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(scope, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByName provides a mock function with given fields: schemeName
func (_m *SchemeStore) GetByName(schemeName string) (*model.Scheme, error) {
ret := _m.Called(schemeName)
var r0 *model.Scheme
if rf, ok := ret.Get(0).(func(string) *model.Scheme); ok {
r0 = rf(schemeName)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Scheme)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(schemeName)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteAll provides a mock function with given fields:
func (_m *SchemeStore) PermanentDeleteAll() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: scheme
func (_m *SchemeStore) Save(scheme *model.Scheme) (*model.Scheme, error) {
ret := _m.Called(scheme)
var r0 *model.Scheme
if rf, ok := ret.Get(0).(func(*model.Scheme) *model.Scheme); ok {
r0 = rf(scheme)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Scheme)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Scheme) error); ok {
r1 = rf(scheme)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
context "context"
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// SessionStore is an autogenerated mock type for the SessionStore type
type SessionStore struct {
mock.Mock
}
// AnalyticsSessionCount provides a mock function with given fields:
func (_m *SessionStore) AnalyticsSessionCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Cleanup provides a mock function with given fields: expiryTime, batchSize
func (_m *SessionStore) Cleanup(expiryTime int64, batchSize int64) error {
ret := _m.Called(expiryTime, batchSize)
var r0 error
if rf, ok := ret.Get(0).(func(int64, int64) error); ok {
r0 = rf(expiryTime, batchSize)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: ctx, sessionIDOrToken
func (_m *SessionStore) Get(ctx context.Context, sessionIDOrToken string) (*model.Session, error) {
ret := _m.Called(ctx, sessionIDOrToken)
var r0 *model.Session
if rf, ok := ret.Get(0).(func(context.Context, string) *model.Session); ok {
r0 = rf(ctx, sessionIDOrToken)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Session)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, sessionIDOrToken)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSessions provides a mock function with given fields: userID
func (_m *SessionStore) GetSessions(userID string) ([]*model.Session, error) {
ret := _m.Called(userID)
var r0 []*model.Session
if rf, ok := ret.Get(0).(func(string) []*model.Session); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Session)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSessionsExpired provides a mock function with given fields: thresholdMillis, mobileOnly, unnotifiedOnly
func (_m *SessionStore) GetSessionsExpired(thresholdMillis int64, mobileOnly bool, unnotifiedOnly bool) ([]*model.Session, error) {
ret := _m.Called(thresholdMillis, mobileOnly, unnotifiedOnly)
var r0 []*model.Session
if rf, ok := ret.Get(0).(func(int64, bool, bool) []*model.Session); ok {
r0 = rf(thresholdMillis, mobileOnly, unnotifiedOnly)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Session)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, bool, bool) error); ok {
r1 = rf(thresholdMillis, mobileOnly, unnotifiedOnly)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSessionsWithActiveDeviceIds provides a mock function with given fields: userID
func (_m *SessionStore) GetSessionsWithActiveDeviceIds(userID string) ([]*model.Session, error) {
ret := _m.Called(userID)
var r0 []*model.Session
if rf, ok := ret.Get(0).(func(string) []*model.Session); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Session)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteSessionsByUser provides a mock function with given fields: teamID
func (_m *SessionStore) PermanentDeleteSessionsByUser(teamID string) error {
ret := _m.Called(teamID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(teamID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Remove provides a mock function with given fields: sessionIDOrToken
func (_m *SessionStore) Remove(sessionIDOrToken string) error {
ret := _m.Called(sessionIDOrToken)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(sessionIDOrToken)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveAllSessions provides a mock function with given fields:
func (_m *SessionStore) RemoveAllSessions() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: session
func (_m *SessionStore) Save(session *model.Session) (*model.Session, error) {
ret := _m.Called(session)
var r0 *model.Session
if rf, ok := ret.Get(0).(func(*model.Session) *model.Session); ok {
r0 = rf(session)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Session)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Session) error); ok {
r1 = rf(session)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateDeviceId provides a mock function with given fields: id, deviceID, expiresAt
func (_m *SessionStore) UpdateDeviceId(id string, deviceID string, expiresAt int64) (string, error) {
ret := _m.Called(id, deviceID, expiresAt)
var r0 string
if rf, ok := ret.Get(0).(func(string, string, int64) string); ok {
r0 = rf(id, deviceID, expiresAt)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int64) error); ok {
r1 = rf(id, deviceID, expiresAt)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateExpiredNotify provides a mock function with given fields: sessionid, notified
func (_m *SessionStore) UpdateExpiredNotify(sessionid string, notified bool) error {
ret := _m.Called(sessionid, notified)
var r0 error
if rf, ok := ret.Get(0).(func(string, bool) error); ok {
r0 = rf(sessionid, notified)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateExpiresAt provides a mock function with given fields: sessionID, timestamp
func (_m *SessionStore) UpdateExpiresAt(sessionID string, timestamp int64) error {
ret := _m.Called(sessionID, timestamp)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64) error); ok {
r0 = rf(sessionID, timestamp)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateLastActivityAt provides a mock function with given fields: sessionID, timestamp
func (_m *SessionStore) UpdateLastActivityAt(sessionID string, timestamp int64) error {
ret := _m.Called(sessionID, timestamp)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64) error); ok {
r0 = rf(sessionID, timestamp)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateProps provides a mock function with given fields: session
func (_m *SessionStore) UpdateProps(session *model.Session) error {
ret := _m.Called(session)
var r0 error
if rf, ok := ret.Get(0).(func(*model.Session) error); ok {
r0 = rf(session)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateRoles provides a mock function with given fields: userID, roles
func (_m *SessionStore) UpdateRoles(userID string, roles string) (string, error) {
ret := _m.Called(userID, roles)
var r0 string
if rf, ok := ret.Get(0).(func(string, string) string); ok {
r0 = rf(userID, roles)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, roles)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// SharedChannelStore is an autogenerated mock type for the SharedChannelStore type
type SharedChannelStore struct {
mock.Mock
}
// Delete provides a mock function with given fields: channelId
func (_m *SharedChannelStore) Delete(channelId string) (bool, error) {
ret := _m.Called(channelId)
var r0 bool
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(channelId)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteRemote provides a mock function with given fields: remoteId
func (_m *SharedChannelStore) DeleteRemote(remoteId string) (bool, error) {
ret := _m.Called(remoteId)
var r0 bool
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(remoteId)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(remoteId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: channelId
func (_m *SharedChannelStore) Get(channelId string) (*model.SharedChannel, error) {
ret := _m.Called(channelId)
var r0 *model.SharedChannel
if rf, ok := ret.Get(0).(func(string) *model.SharedChannel); ok {
r0 = rf(channelId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields: offset, limit, opts
func (_m *SharedChannelStore) GetAll(offset int, limit int, opts model.SharedChannelFilterOpts) ([]*model.SharedChannel, error) {
ret := _m.Called(offset, limit, opts)
var r0 []*model.SharedChannel
if rf, ok := ret.Get(0).(func(int, int, model.SharedChannelFilterOpts) []*model.SharedChannel); ok {
r0 = rf(offset, limit, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.SharedChannel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int, model.SharedChannelFilterOpts) error); ok {
r1 = rf(offset, limit, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllCount provides a mock function with given fields: opts
func (_m *SharedChannelStore) GetAllCount(opts model.SharedChannelFilterOpts) (int64, error) {
ret := _m.Called(opts)
var r0 int64
if rf, ok := ret.Get(0).(func(model.SharedChannelFilterOpts) int64); ok {
r0 = rf(opts)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(model.SharedChannelFilterOpts) error); ok {
r1 = rf(opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAttachment provides a mock function with given fields: fileId, remoteId
func (_m *SharedChannelStore) GetAttachment(fileId string, remoteId string) (*model.SharedChannelAttachment, error) {
ret := _m.Called(fileId, remoteId)
var r0 *model.SharedChannelAttachment
if rf, ok := ret.Get(0).(func(string, string) *model.SharedChannelAttachment); ok {
r0 = rf(fileId, remoteId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannelAttachment)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(fileId, remoteId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRemote provides a mock function with given fields: id
func (_m *SharedChannelStore) GetRemote(id string) (*model.SharedChannelRemote, error) {
ret := _m.Called(id)
var r0 *model.SharedChannelRemote
if rf, ok := ret.Get(0).(func(string) *model.SharedChannelRemote); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannelRemote)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRemoteByIds provides a mock function with given fields: channelId, remoteId
func (_m *SharedChannelStore) GetRemoteByIds(channelId string, remoteId string) (*model.SharedChannelRemote, error) {
ret := _m.Called(channelId, remoteId)
var r0 *model.SharedChannelRemote
if rf, ok := ret.Get(0).(func(string, string) *model.SharedChannelRemote); ok {
r0 = rf(channelId, remoteId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannelRemote)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(channelId, remoteId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRemoteForUser provides a mock function with given fields: remoteId, userId
func (_m *SharedChannelStore) GetRemoteForUser(remoteId string, userId string) (*model.RemoteCluster, error) {
ret := _m.Called(remoteId, userId)
var r0 *model.RemoteCluster
if rf, ok := ret.Get(0).(func(string, string) *model.RemoteCluster); ok {
r0 = rf(remoteId, userId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.RemoteCluster)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(remoteId, userId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRemotes provides a mock function with given fields: opts
func (_m *SharedChannelStore) GetRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) {
ret := _m.Called(opts)
var r0 []*model.SharedChannelRemote
if rf, ok := ret.Get(0).(func(model.SharedChannelRemoteFilterOpts) []*model.SharedChannelRemote); ok {
r0 = rf(opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.SharedChannelRemote)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(model.SharedChannelRemoteFilterOpts) error); ok {
r1 = rf(opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRemotesStatus provides a mock function with given fields: channelId
func (_m *SharedChannelStore) GetRemotesStatus(channelId string) ([]*model.SharedChannelRemoteStatus, error) {
ret := _m.Called(channelId)
var r0 []*model.SharedChannelRemoteStatus
if rf, ok := ret.Get(0).(func(string) []*model.SharedChannelRemoteStatus); ok {
r0 = rf(channelId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.SharedChannelRemoteStatus)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSingleUser provides a mock function with given fields: userID, channelID, remoteID
func (_m *SharedChannelStore) GetSingleUser(userID string, channelID string, remoteID string) (*model.SharedChannelUser, error) {
ret := _m.Called(userID, channelID, remoteID)
var r0 *model.SharedChannelUser
if rf, ok := ret.Get(0).(func(string, string, string) *model.SharedChannelUser); ok {
r0 = rf(userID, channelID, remoteID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannelUser)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string) error); ok {
r1 = rf(userID, channelID, remoteID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUsersForSync provides a mock function with given fields: filter
func (_m *SharedChannelStore) GetUsersForSync(filter model.GetUsersForSyncFilter) ([]*model.User, error) {
ret := _m.Called(filter)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(model.GetUsersForSyncFilter) []*model.User); ok {
r0 = rf(filter)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(model.GetUsersForSyncFilter) error); ok {
r1 = rf(filter)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUsersForUser provides a mock function with given fields: userID
func (_m *SharedChannelStore) GetUsersForUser(userID string) ([]*model.SharedChannelUser, error) {
ret := _m.Called(userID)
var r0 []*model.SharedChannelUser
if rf, ok := ret.Get(0).(func(string) []*model.SharedChannelUser); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.SharedChannelUser)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// HasChannel provides a mock function with given fields: channelID
func (_m *SharedChannelStore) HasChannel(channelID string) (bool, error) {
ret := _m.Called(channelID)
var r0 bool
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(channelID)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// HasRemote provides a mock function with given fields: channelID, remoteId
func (_m *SharedChannelStore) HasRemote(channelID string, remoteId string) (bool, error) {
ret := _m.Called(channelID, remoteId)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string) bool); ok {
r0 = rf(channelID, remoteId)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(channelID, remoteId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: sc
func (_m *SharedChannelStore) Save(sc *model.SharedChannel) (*model.SharedChannel, error) {
ret := _m.Called(sc)
var r0 *model.SharedChannel
if rf, ok := ret.Get(0).(func(*model.SharedChannel) *model.SharedChannel); ok {
r0 = rf(sc)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.SharedChannel) error); ok {
r1 = rf(sc)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveAttachment provides a mock function with given fields: remote
func (_m *SharedChannelStore) SaveAttachment(remote *model.SharedChannelAttachment) (*model.SharedChannelAttachment, error) {
ret := _m.Called(remote)
var r0 *model.SharedChannelAttachment
if rf, ok := ret.Get(0).(func(*model.SharedChannelAttachment) *model.SharedChannelAttachment); ok {
r0 = rf(remote)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannelAttachment)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.SharedChannelAttachment) error); ok {
r1 = rf(remote)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveRemote provides a mock function with given fields: remote
func (_m *SharedChannelStore) SaveRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
ret := _m.Called(remote)
var r0 *model.SharedChannelRemote
if rf, ok := ret.Get(0).(func(*model.SharedChannelRemote) *model.SharedChannelRemote); ok {
r0 = rf(remote)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannelRemote)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.SharedChannelRemote) error); ok {
r1 = rf(remote)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveUser provides a mock function with given fields: remote
func (_m *SharedChannelStore) SaveUser(remote *model.SharedChannelUser) (*model.SharedChannelUser, error) {
ret := _m.Called(remote)
var r0 *model.SharedChannelUser
if rf, ok := ret.Get(0).(func(*model.SharedChannelUser) *model.SharedChannelUser); ok {
r0 = rf(remote)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannelUser)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.SharedChannelUser) error); ok {
r1 = rf(remote)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: sc
func (_m *SharedChannelStore) Update(sc *model.SharedChannel) (*model.SharedChannel, error) {
ret := _m.Called(sc)
var r0 *model.SharedChannel
if rf, ok := ret.Get(0).(func(*model.SharedChannel) *model.SharedChannel); ok {
r0 = rf(sc)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.SharedChannel) error); ok {
r1 = rf(sc)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateAttachmentLastSyncAt provides a mock function with given fields: id, syncTime
func (_m *SharedChannelStore) UpdateAttachmentLastSyncAt(id string, syncTime int64) error {
ret := _m.Called(id, syncTime)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64) error); ok {
r0 = rf(id, syncTime)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateRemote provides a mock function with given fields: remote
func (_m *SharedChannelStore) UpdateRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
ret := _m.Called(remote)
var r0 *model.SharedChannelRemote
if rf, ok := ret.Get(0).(func(*model.SharedChannelRemote) *model.SharedChannelRemote); ok {
r0 = rf(remote)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SharedChannelRemote)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.SharedChannelRemote) error); ok {
r1 = rf(remote)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateRemoteCursor provides a mock function with given fields: id, cursor
func (_m *SharedChannelStore) UpdateRemoteCursor(id string, cursor model.GetPostsSinceForSyncCursor) error {
ret := _m.Called(id, cursor)
var r0 error
if rf, ok := ret.Get(0).(func(string, model.GetPostsSinceForSyncCursor) error); ok {
r0 = rf(id, cursor)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateUserLastSyncAt provides a mock function with given fields: userID, channelID, remoteID
func (_m *SharedChannelStore) UpdateUserLastSyncAt(userID string, channelID string, remoteID string) error {
ret := _m.Called(userID, channelID, remoteID)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = rf(userID, channelID, remoteID)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpsertAttachment provides a mock function with given fields: remote
func (_m *SharedChannelStore) UpsertAttachment(remote *model.SharedChannelAttachment) (string, error) {
ret := _m.Called(remote)
var r0 string
if rf, ok := ret.Get(0).(func(*model.SharedChannelAttachment) string); ok {
r0 = rf(remote)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.SharedChannelAttachment) error); ok {
r1 = rf(remote)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// StatusStore is an autogenerated mock type for the StatusStore type
type StatusStore struct {
mock.Mock
}
// Get provides a mock function with given fields: userID
func (_m *StatusStore) Get(userID string) (*model.Status, error) {
ret := _m.Called(userID)
var r0 *model.Status
if rf, ok := ret.Get(0).(func(string) *model.Status); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Status)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByIds provides a mock function with given fields: userIds
func (_m *StatusStore) GetByIds(userIds []string) ([]*model.Status, error) {
ret := _m.Called(userIds)
var r0 []*model.Status
if rf, ok := ret.Get(0).(func([]string) []*model.Status); ok {
r0 = rf(userIds)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Status)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(userIds)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTotalActiveUsersCount provides a mock function with given fields:
func (_m *StatusStore) GetTotalActiveUsersCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ResetAll provides a mock function with given fields:
func (_m *StatusStore) ResetAll() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// SaveOrUpdate provides a mock function with given fields: status
func (_m *StatusStore) SaveOrUpdate(status *model.Status) error {
ret := _m.Called(status)
var r0 error
if rf, ok := ret.Get(0).(func(*model.Status) error); ok {
r0 = rf(status)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateExpiredDNDStatuses provides a mock function with given fields:
func (_m *StatusStore) UpdateExpiredDNDStatuses() ([]*model.Status, error) {
ret := _m.Called()
var r0 []*model.Status
if rf, ok := ret.Get(0).(func() []*model.Status); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Status)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateLastActivityAt provides a mock function with given fields: userID, lastActivityAt
func (_m *StatusStore) UpdateLastActivityAt(userID string, lastActivityAt int64) error {
ret := _m.Called(userID, lastActivityAt)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64) error); ok {
r0 = rf(userID, lastActivityAt)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
context "context"
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
sql "database/sql"
store "github.com/mattermost/mattermost-server/v6/server/channels/store"
time "time"
)
// Store is an autogenerated mock type for the Store type
type Store struct {
mock.Mock
}
// Audit provides a mock function with given fields:
func (_m *Store) Audit() store.AuditStore {
ret := _m.Called()
var r0 store.AuditStore
if rf, ok := ret.Get(0).(func() store.AuditStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.AuditStore)
}
}
return r0
}
// Bot provides a mock function with given fields:
func (_m *Store) Bot() store.BotStore {
ret := _m.Called()
var r0 store.BotStore
if rf, ok := ret.Get(0).(func() store.BotStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.BotStore)
}
}
return r0
}
// Channel provides a mock function with given fields:
func (_m *Store) Channel() store.ChannelStore {
ret := _m.Called()
var r0 store.ChannelStore
if rf, ok := ret.Get(0).(func() store.ChannelStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.ChannelStore)
}
}
return r0
}
// ChannelMemberHistory provides a mock function with given fields:
func (_m *Store) ChannelMemberHistory() store.ChannelMemberHistoryStore {
ret := _m.Called()
var r0 store.ChannelMemberHistoryStore
if rf, ok := ret.Get(0).(func() store.ChannelMemberHistoryStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.ChannelMemberHistoryStore)
}
}
return r0
}
// CheckIntegrity provides a mock function with given fields:
func (_m *Store) CheckIntegrity() <-chan model.IntegrityCheckResult {
ret := _m.Called()
var r0 <-chan model.IntegrityCheckResult
if rf, ok := ret.Get(0).(func() <-chan model.IntegrityCheckResult); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(<-chan model.IntegrityCheckResult)
}
}
return r0
}
// Close provides a mock function with given fields:
func (_m *Store) Close() {
_m.Called()
}
// ClusterDiscovery provides a mock function with given fields:
func (_m *Store) ClusterDiscovery() store.ClusterDiscoveryStore {
ret := _m.Called()
var r0 store.ClusterDiscoveryStore
if rf, ok := ret.Get(0).(func() store.ClusterDiscoveryStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.ClusterDiscoveryStore)
}
}
return r0
}
// Command provides a mock function with given fields:
func (_m *Store) Command() store.CommandStore {
ret := _m.Called()
var r0 store.CommandStore
if rf, ok := ret.Get(0).(func() store.CommandStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.CommandStore)
}
}
return r0
}
// CommandWebhook provides a mock function with given fields:
func (_m *Store) CommandWebhook() store.CommandWebhookStore {
ret := _m.Called()
var r0 store.CommandWebhookStore
if rf, ok := ret.Get(0).(func() store.CommandWebhookStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.CommandWebhookStore)
}
}
return r0
}
// Compliance provides a mock function with given fields:
func (_m *Store) Compliance() store.ComplianceStore {
ret := _m.Called()
var r0 store.ComplianceStore
if rf, ok := ret.Get(0).(func() store.ComplianceStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.ComplianceStore)
}
}
return r0
}
// Context provides a mock function with given fields:
func (_m *Store) Context() context.Context {
ret := _m.Called()
var r0 context.Context
if rf, ok := ret.Get(0).(func() context.Context); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(context.Context)
}
}
return r0
}
// Draft provides a mock function with given fields:
func (_m *Store) Draft() store.DraftStore {
ret := _m.Called()
var r0 store.DraftStore
if rf, ok := ret.Get(0).(func() store.DraftStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.DraftStore)
}
}
return r0
}
// DropAllTables provides a mock function with given fields:
func (_m *Store) DropAllTables() {
_m.Called()
}
// Emoji provides a mock function with given fields:
func (_m *Store) Emoji() store.EmojiStore {
ret := _m.Called()
var r0 store.EmojiStore
if rf, ok := ret.Get(0).(func() store.EmojiStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.EmojiStore)
}
}
return r0
}
// FileInfo provides a mock function with given fields:
func (_m *Store) FileInfo() store.FileInfoStore {
ret := _m.Called()
var r0 store.FileInfoStore
if rf, ok := ret.Get(0).(func() store.FileInfoStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.FileInfoStore)
}
}
return r0
}
// GetAppliedMigrations provides a mock function with given fields:
func (_m *Store) GetAppliedMigrations() ([]model.AppliedMigration, error) {
ret := _m.Called()
var r0 []model.AppliedMigration
if rf, ok := ret.Get(0).(func() []model.AppliedMigration); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]model.AppliedMigration)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetDBSchemaVersion provides a mock function with given fields:
func (_m *Store) GetDBSchemaVersion() (int, error) {
ret := _m.Called()
var r0 int
if rf, ok := ret.Get(0).(func() int); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetDbVersion provides a mock function with given fields: numerical
func (_m *Store) GetDbVersion(numerical bool) (string, error) {
ret := _m.Called(numerical)
var r0 string
if rf, ok := ret.Get(0).(func(bool) string); ok {
r0 = rf(numerical)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(bool) error); ok {
r1 = rf(numerical)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetInternalMasterDB provides a mock function with given fields:
func (_m *Store) GetInternalMasterDB() *sql.DB {
ret := _m.Called()
var r0 *sql.DB
if rf, ok := ret.Get(0).(func() *sql.DB); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*sql.DB)
}
}
return r0
}
// GetInternalReplicaDB provides a mock function with given fields:
func (_m *Store) GetInternalReplicaDB() *sql.DB {
ret := _m.Called()
var r0 *sql.DB
if rf, ok := ret.Get(0).(func() *sql.DB); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*sql.DB)
}
}
return r0
}
// GetInternalReplicaDBs provides a mock function with given fields:
func (_m *Store) GetInternalReplicaDBs() []*sql.DB {
ret := _m.Called()
var r0 []*sql.DB
if rf, ok := ret.Get(0).(func() []*sql.DB); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*sql.DB)
}
}
return r0
}
// Group provides a mock function with given fields:
func (_m *Store) Group() store.GroupStore {
ret := _m.Called()
var r0 store.GroupStore
if rf, ok := ret.Get(0).(func() store.GroupStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.GroupStore)
}
}
return r0
}
// Job provides a mock function with given fields:
func (_m *Store) Job() store.JobStore {
ret := _m.Called()
var r0 store.JobStore
if rf, ok := ret.Get(0).(func() store.JobStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.JobStore)
}
}
return r0
}
// License provides a mock function with given fields:
func (_m *Store) License() store.LicenseStore {
ret := _m.Called()
var r0 store.LicenseStore
if rf, ok := ret.Get(0).(func() store.LicenseStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.LicenseStore)
}
}
return r0
}
// LinkMetadata provides a mock function with given fields:
func (_m *Store) LinkMetadata() store.LinkMetadataStore {
ret := _m.Called()
var r0 store.LinkMetadataStore
if rf, ok := ret.Get(0).(func() store.LinkMetadataStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.LinkMetadataStore)
}
}
return r0
}
// LockToMaster provides a mock function with given fields:
func (_m *Store) LockToMaster() {
_m.Called()
}
// MarkSystemRanUnitTests provides a mock function with given fields:
func (_m *Store) MarkSystemRanUnitTests() {
_m.Called()
}
// NotifyAdmin provides a mock function with given fields:
func (_m *Store) NotifyAdmin() store.NotifyAdminStore {
ret := _m.Called()
var r0 store.NotifyAdminStore
if rf, ok := ret.Get(0).(func() store.NotifyAdminStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.NotifyAdminStore)
}
}
return r0
}
// OAuth provides a mock function with given fields:
func (_m *Store) OAuth() store.OAuthStore {
ret := _m.Called()
var r0 store.OAuthStore
if rf, ok := ret.Get(0).(func() store.OAuthStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.OAuthStore)
}
}
return r0
}
// Plugin provides a mock function with given fields:
func (_m *Store) Plugin() store.PluginStore {
ret := _m.Called()
var r0 store.PluginStore
if rf, ok := ret.Get(0).(func() store.PluginStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.PluginStore)
}
}
return r0
}
// Post provides a mock function with given fields:
func (_m *Store) Post() store.PostStore {
ret := _m.Called()
var r0 store.PostStore
if rf, ok := ret.Get(0).(func() store.PostStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.PostStore)
}
}
return r0
}
// PostAcknowledgement provides a mock function with given fields:
func (_m *Store) PostAcknowledgement() store.PostAcknowledgementStore {
ret := _m.Called()
var r0 store.PostAcknowledgementStore
if rf, ok := ret.Get(0).(func() store.PostAcknowledgementStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.PostAcknowledgementStore)
}
}
return r0
}
// PostPriority provides a mock function with given fields:
func (_m *Store) PostPriority() store.PostPriorityStore {
ret := _m.Called()
var r0 store.PostPriorityStore
if rf, ok := ret.Get(0).(func() store.PostPriorityStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.PostPriorityStore)
}
}
return r0
}
// Preference provides a mock function with given fields:
func (_m *Store) Preference() store.PreferenceStore {
ret := _m.Called()
var r0 store.PreferenceStore
if rf, ok := ret.Get(0).(func() store.PreferenceStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.PreferenceStore)
}
}
return r0
}
// ProductNotices provides a mock function with given fields:
func (_m *Store) ProductNotices() store.ProductNoticesStore {
ret := _m.Called()
var r0 store.ProductNoticesStore
if rf, ok := ret.Get(0).(func() store.ProductNoticesStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.ProductNoticesStore)
}
}
return r0
}
// Reaction provides a mock function with given fields:
func (_m *Store) Reaction() store.ReactionStore {
ret := _m.Called()
var r0 store.ReactionStore
if rf, ok := ret.Get(0).(func() store.ReactionStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.ReactionStore)
}
}
return r0
}
// RecycleDBConnections provides a mock function with given fields: d
func (_m *Store) RecycleDBConnections(d time.Duration) {
_m.Called(d)
}
// RemoteCluster provides a mock function with given fields:
func (_m *Store) RemoteCluster() store.RemoteClusterStore {
ret := _m.Called()
var r0 store.RemoteClusterStore
if rf, ok := ret.Get(0).(func() store.RemoteClusterStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.RemoteClusterStore)
}
}
return r0
}
// ReplicaLagAbs provides a mock function with given fields:
func (_m *Store) ReplicaLagAbs() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// ReplicaLagTime provides a mock function with given fields:
func (_m *Store) ReplicaLagTime() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// RetentionPolicy provides a mock function with given fields:
func (_m *Store) RetentionPolicy() store.RetentionPolicyStore {
ret := _m.Called()
var r0 store.RetentionPolicyStore
if rf, ok := ret.Get(0).(func() store.RetentionPolicyStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.RetentionPolicyStore)
}
}
return r0
}
// Role provides a mock function with given fields:
func (_m *Store) Role() store.RoleStore {
ret := _m.Called()
var r0 store.RoleStore
if rf, ok := ret.Get(0).(func() store.RoleStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.RoleStore)
}
}
return r0
}
// Scheme provides a mock function with given fields:
func (_m *Store) Scheme() store.SchemeStore {
ret := _m.Called()
var r0 store.SchemeStore
if rf, ok := ret.Get(0).(func() store.SchemeStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.SchemeStore)
}
}
return r0
}
// Session provides a mock function with given fields:
func (_m *Store) Session() store.SessionStore {
ret := _m.Called()
var r0 store.SessionStore
if rf, ok := ret.Get(0).(func() store.SessionStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.SessionStore)
}
}
return r0
}
// SetContext provides a mock function with given fields: _a0
func (_m *Store) SetContext(_a0 context.Context) {
_m.Called(_a0)
}
// SharedChannel provides a mock function with given fields:
func (_m *Store) SharedChannel() store.SharedChannelStore {
ret := _m.Called()
var r0 store.SharedChannelStore
if rf, ok := ret.Get(0).(func() store.SharedChannelStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.SharedChannelStore)
}
}
return r0
}
// Status provides a mock function with given fields:
func (_m *Store) Status() store.StatusStore {
ret := _m.Called()
var r0 store.StatusStore
if rf, ok := ret.Get(0).(func() store.StatusStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.StatusStore)
}
}
return r0
}
// System provides a mock function with given fields:
func (_m *Store) System() store.SystemStore {
ret := _m.Called()
var r0 store.SystemStore
if rf, ok := ret.Get(0).(func() store.SystemStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.SystemStore)
}
}
return r0
}
// Team provides a mock function with given fields:
func (_m *Store) Team() store.TeamStore {
ret := _m.Called()
var r0 store.TeamStore
if rf, ok := ret.Get(0).(func() store.TeamStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.TeamStore)
}
}
return r0
}
// TermsOfService provides a mock function with given fields:
func (_m *Store) TermsOfService() store.TermsOfServiceStore {
ret := _m.Called()
var r0 store.TermsOfServiceStore
if rf, ok := ret.Get(0).(func() store.TermsOfServiceStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.TermsOfServiceStore)
}
}
return r0
}
// Thread provides a mock function with given fields:
func (_m *Store) Thread() store.ThreadStore {
ret := _m.Called()
var r0 store.ThreadStore
if rf, ok := ret.Get(0).(func() store.ThreadStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.ThreadStore)
}
}
return r0
}
// Token provides a mock function with given fields:
func (_m *Store) Token() store.TokenStore {
ret := _m.Called()
var r0 store.TokenStore
if rf, ok := ret.Get(0).(func() store.TokenStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.TokenStore)
}
}
return r0
}
// TotalMasterDbConnections provides a mock function with given fields:
func (_m *Store) TotalMasterDbConnections() int {
ret := _m.Called()
var r0 int
if rf, ok := ret.Get(0).(func() int); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int)
}
return r0
}
// TotalReadDbConnections provides a mock function with given fields:
func (_m *Store) TotalReadDbConnections() int {
ret := _m.Called()
var r0 int
if rf, ok := ret.Get(0).(func() int); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int)
}
return r0
}
// TotalSearchDbConnections provides a mock function with given fields:
func (_m *Store) TotalSearchDbConnections() int {
ret := _m.Called()
var r0 int
if rf, ok := ret.Get(0).(func() int); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int)
}
return r0
}
// TrueUpReview provides a mock function with given fields:
func (_m *Store) TrueUpReview() store.TrueUpReviewStore {
ret := _m.Called()
var r0 store.TrueUpReviewStore
if rf, ok := ret.Get(0).(func() store.TrueUpReviewStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.TrueUpReviewStore)
}
}
return r0
}
// UnlockFromMaster provides a mock function with given fields:
func (_m *Store) UnlockFromMaster() {
_m.Called()
}
// UploadSession provides a mock function with given fields:
func (_m *Store) UploadSession() store.UploadSessionStore {
ret := _m.Called()
var r0 store.UploadSessionStore
if rf, ok := ret.Get(0).(func() store.UploadSessionStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.UploadSessionStore)
}
}
return r0
}
// User provides a mock function with given fields:
func (_m *Store) User() store.UserStore {
ret := _m.Called()
var r0 store.UserStore
if rf, ok := ret.Get(0).(func() store.UserStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.UserStore)
}
}
return r0
}
// UserAccessToken provides a mock function with given fields:
func (_m *Store) UserAccessToken() store.UserAccessTokenStore {
ret := _m.Called()
var r0 store.UserAccessTokenStore
if rf, ok := ret.Get(0).(func() store.UserAccessTokenStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.UserAccessTokenStore)
}
}
return r0
}
// UserTermsOfService provides a mock function with given fields:
func (_m *Store) UserTermsOfService() store.UserTermsOfServiceStore {
ret := _m.Called()
var r0 store.UserTermsOfServiceStore
if rf, ok := ret.Get(0).(func() store.UserTermsOfServiceStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.UserTermsOfServiceStore)
}
}
return r0
}
// Webhook provides a mock function with given fields:
func (_m *Store) Webhook() store.WebhookStore {
ret := _m.Called()
var r0 store.WebhookStore
if rf, ok := ret.Get(0).(func() store.WebhookStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.WebhookStore)
}
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// SystemStore is an autogenerated mock type for the SystemStore type
type SystemStore struct {
mock.Mock
}
// Get provides a mock function with given fields:
func (_m *SystemStore) Get() (model.StringMap, error) {
ret := _m.Called()
var r0 model.StringMap
if rf, ok := ret.Get(0).(func() model.StringMap); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(model.StringMap)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByName provides a mock function with given fields: name
func (_m *SystemStore) GetByName(name string) (*model.System, error) {
ret := _m.Called(name)
var r0 *model.System
if rf, ok := ret.Get(0).(func(string) *model.System); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.System)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// InsertIfExists provides a mock function with given fields: system
func (_m *SystemStore) InsertIfExists(system *model.System) (*model.System, error) {
ret := _m.Called(system)
var r0 *model.System
if rf, ok := ret.Get(0).(func(*model.System) *model.System); ok {
r0 = rf(system)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.System)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.System) error); ok {
r1 = rf(system)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteByName provides a mock function with given fields: name
func (_m *SystemStore) PermanentDeleteByName(name string) (*model.System, error) {
ret := _m.Called(name)
var r0 *model.System
if rf, ok := ret.Get(0).(func(string) *model.System); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.System)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: system
func (_m *SystemStore) Save(system *model.System) error {
ret := _m.Called(system)
var r0 error
if rf, ok := ret.Get(0).(func(*model.System) error); ok {
r0 = rf(system)
} else {
r0 = ret.Error(0)
}
return r0
}
// SaveOrUpdate provides a mock function with given fields: system
func (_m *SystemStore) SaveOrUpdate(system *model.System) error {
ret := _m.Called(system)
var r0 error
if rf, ok := ret.Get(0).(func(*model.System) error); ok {
r0 = rf(system)
} else {
r0 = ret.Error(0)
}
return r0
}
// SaveOrUpdateWithWarnMetricHandling provides a mock function with given fields: system
func (_m *SystemStore) SaveOrUpdateWithWarnMetricHandling(system *model.System) error {
ret := _m.Called(system)
var r0 error
if rf, ok := ret.Get(0).(func(*model.System) error); ok {
r0 = rf(system)
} else {
r0 = ret.Error(0)
}
return r0
}
// Update provides a mock function with given fields: system
func (_m *SystemStore) Update(system *model.System) error {
ret := _m.Called(system)
var r0 error
if rf, ok := ret.Get(0).(func(*model.System) error); ok {
r0 = rf(system)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
context "context"
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// TeamStore is an autogenerated mock type for the TeamStore type
type TeamStore struct {
mock.Mock
}
// AnalyticsGetTeamCountForScheme provides a mock function with given fields: schemeID
func (_m *TeamStore) AnalyticsGetTeamCountForScheme(schemeID string) (int64, error) {
ret := _m.Called(schemeID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(schemeID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(schemeID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AnalyticsTeamCount provides a mock function with given fields: opts
func (_m *TeamStore) AnalyticsTeamCount(opts *model.TeamSearch) (int64, error) {
ret := _m.Called(opts)
var r0 int64
if rf, ok := ret.Get(0).(func(*model.TeamSearch) int64); ok {
r0 = rf(opts)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.TeamSearch) error); ok {
r1 = rf(opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ClearAllCustomRoleAssignments provides a mock function with given fields:
func (_m *TeamStore) ClearAllCustomRoleAssignments() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// ClearCaches provides a mock function with given fields:
func (_m *TeamStore) ClearCaches() {
_m.Called()
}
// Get provides a mock function with given fields: id
func (_m *TeamStore) Get(id string) (*model.Team, error) {
ret := _m.Called(id)
var r0 *model.Team
if rf, ok := ret.Get(0).(func(string) *model.Team); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetActiveMemberCount provides a mock function with given fields: teamID, restrictions
func (_m *TeamStore) GetActiveMemberCount(teamID string, restrictions *model.ViewUsersRestrictions) (int64, error) {
ret := _m.Called(teamID, restrictions)
var r0 int64
if rf, ok := ret.Get(0).(func(string, *model.ViewUsersRestrictions) int64); ok {
r0 = rf(teamID, restrictions)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, *model.ViewUsersRestrictions) error); ok {
r1 = rf(teamID, restrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields:
func (_m *TeamStore) GetAll() ([]*model.Team, error) {
ret := _m.Called()
var r0 []*model.Team
if rf, ok := ret.Get(0).(func() []*model.Team); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllForExportAfter provides a mock function with given fields: limit, afterID
func (_m *TeamStore) GetAllForExportAfter(limit int, afterID string) ([]*model.TeamForExport, error) {
ret := _m.Called(limit, afterID)
var r0 []*model.TeamForExport
if rf, ok := ret.Get(0).(func(int, string) []*model.TeamForExport); ok {
r0 = rf(limit, afterID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.TeamForExport)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, string) error); ok {
r1 = rf(limit, afterID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllPage provides a mock function with given fields: offset, limit, opts
func (_m *TeamStore) GetAllPage(offset int, limit int, opts *model.TeamSearch) ([]*model.Team, error) {
ret := _m.Called(offset, limit, opts)
var r0 []*model.Team
if rf, ok := ret.Get(0).(func(int, int, *model.TeamSearch) []*model.Team); ok {
r0 = rf(offset, limit, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int, *model.TeamSearch) error); ok {
r1 = rf(offset, limit, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllPrivateTeamListing provides a mock function with given fields:
func (_m *TeamStore) GetAllPrivateTeamListing() ([]*model.Team, error) {
ret := _m.Called()
var r0 []*model.Team
if rf, ok := ret.Get(0).(func() []*model.Team); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllTeamListing provides a mock function with given fields:
func (_m *TeamStore) GetAllTeamListing() ([]*model.Team, error) {
ret := _m.Called()
var r0 []*model.Team
if rf, ok := ret.Get(0).(func() []*model.Team); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByEmptyInviteID provides a mock function with given fields:
func (_m *TeamStore) GetByEmptyInviteID() ([]*model.Team, error) {
ret := _m.Called()
var r0 []*model.Team
if rf, ok := ret.Get(0).(func() []*model.Team); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByInviteId provides a mock function with given fields: inviteID
func (_m *TeamStore) GetByInviteId(inviteID string) (*model.Team, error) {
ret := _m.Called(inviteID)
var r0 *model.Team
if rf, ok := ret.Get(0).(func(string) *model.Team); ok {
r0 = rf(inviteID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(inviteID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByName provides a mock function with given fields: name
func (_m *TeamStore) GetByName(name string) (*model.Team, error) {
ret := _m.Called(name)
var r0 *model.Team
if rf, ok := ret.Get(0).(func(string) *model.Team); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByNames provides a mock function with given fields: name
func (_m *TeamStore) GetByNames(name []string) ([]*model.Team, error) {
ret := _m.Called(name)
var r0 []*model.Team
if rf, ok := ret.Get(0).(func([]string) []*model.Team); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelUnreadsForAllTeams provides a mock function with given fields: excludeTeamID, userID
func (_m *TeamStore) GetChannelUnreadsForAllTeams(excludeTeamID string, userID string) ([]*model.ChannelUnread, error) {
ret := _m.Called(excludeTeamID, userID)
var r0 []*model.ChannelUnread
if rf, ok := ret.Get(0).(func(string, string) []*model.ChannelUnread); ok {
r0 = rf(excludeTeamID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelUnread)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(excludeTeamID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelUnreadsForTeam provides a mock function with given fields: teamID, userID
func (_m *TeamStore) GetChannelUnreadsForTeam(teamID string, userID string) ([]*model.ChannelUnread, error) {
ret := _m.Called(teamID, userID)
var r0 []*model.ChannelUnread
if rf, ok := ret.Get(0).(func(string, string) []*model.ChannelUnread); ok {
r0 = rf(teamID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ChannelUnread)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(teamID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetCommonTeamIDsForTwoUsers provides a mock function with given fields: userID, otherUserID
func (_m *TeamStore) GetCommonTeamIDsForTwoUsers(userID string, otherUserID string) ([]string, error) {
ret := _m.Called(userID, otherUserID)
var r0 []string
if rf, ok := ret.Get(0).(func(string, string) []string); ok {
r0 = rf(userID, otherUserID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, otherUserID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMany provides a mock function with given fields: ids
func (_m *TeamStore) GetMany(ids []string) ([]*model.Team, error) {
ret := _m.Called(ids)
var r0 []*model.Team
if rf, ok := ret.Get(0).(func([]string) []*model.Team); ok {
r0 = rf(ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMember provides a mock function with given fields: ctx, teamID, userID
func (_m *TeamStore) GetMember(ctx context.Context, teamID string, userID string) (*model.TeamMember, error) {
ret := _m.Called(ctx, teamID, userID)
var r0 *model.TeamMember
if rf, ok := ret.Get(0).(func(context.Context, string, string) *model.TeamMember); ok {
r0 = rf(ctx, teamID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TeamMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = rf(ctx, teamID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembers provides a mock function with given fields: teamID, offset, limit, teamMembersGetOptions
func (_m *TeamStore) GetMembers(teamID string, offset int, limit int, teamMembersGetOptions *model.TeamMembersGetOptions) ([]*model.TeamMember, error) {
ret := _m.Called(teamID, offset, limit, teamMembersGetOptions)
var r0 []*model.TeamMember
if rf, ok := ret.Get(0).(func(string, int, int, *model.TeamMembersGetOptions) []*model.TeamMember); ok {
r0 = rf(teamID, offset, limit, teamMembersGetOptions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.TeamMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int, *model.TeamMembersGetOptions) error); ok {
r1 = rf(teamID, offset, limit, teamMembersGetOptions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembersByIds provides a mock function with given fields: teamID, userIds, restrictions
func (_m *TeamStore) GetMembersByIds(teamID string, userIds []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, error) {
ret := _m.Called(teamID, userIds, restrictions)
var r0 []*model.TeamMember
if rf, ok := ret.Get(0).(func(string, []string, *model.ViewUsersRestrictions) []*model.TeamMember); ok {
r0 = rf(teamID, userIds, restrictions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.TeamMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string, *model.ViewUsersRestrictions) error); ok {
r1 = rf(teamID, userIds, restrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetNewTeamMembersSince provides a mock function with given fields: teamID, since, offset, limit
func (_m *TeamStore) GetNewTeamMembersSince(teamID string, since int64, offset int, limit int) (*model.NewTeamMembersList, int64, error) {
ret := _m.Called(teamID, since, offset, limit)
var r0 *model.NewTeamMembersList
if rf, ok := ret.Get(0).(func(string, int64, int, int) *model.NewTeamMembersList); ok {
r0 = rf(teamID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.NewTeamMembersList)
}
}
var r1 int64
if rf, ok := ret.Get(1).(func(string, int64, int, int) int64); ok {
r1 = rf(teamID, since, offset, limit)
} else {
r1 = ret.Get(1).(int64)
}
var r2 error
if rf, ok := ret.Get(2).(func(string, int64, int, int) error); ok {
r2 = rf(teamID, since, offset, limit)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// GetTeamMembersForExport provides a mock function with given fields: userID
func (_m *TeamStore) GetTeamMembersForExport(userID string) ([]*model.TeamMemberForExport, error) {
ret := _m.Called(userID)
var r0 []*model.TeamMemberForExport
if rf, ok := ret.Get(0).(func(string) []*model.TeamMemberForExport); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.TeamMemberForExport)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamsByScheme provides a mock function with given fields: schemeID, offset, limit
func (_m *TeamStore) GetTeamsByScheme(schemeID string, offset int, limit int) ([]*model.Team, error) {
ret := _m.Called(schemeID, offset, limit)
var r0 []*model.Team
if rf, ok := ret.Get(0).(func(string, int, int) []*model.Team); ok {
r0 = rf(schemeID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(schemeID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamsByUserId provides a mock function with given fields: userID
func (_m *TeamStore) GetTeamsByUserId(userID string) ([]*model.Team, error) {
ret := _m.Called(userID)
var r0 []*model.Team
if rf, ok := ret.Get(0).(func(string) []*model.Team); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamsForUser provides a mock function with given fields: ctx, userID, excludeTeamID, includeDeleted
func (_m *TeamStore) GetTeamsForUser(ctx context.Context, userID string, excludeTeamID string, includeDeleted bool) ([]*model.TeamMember, error) {
ret := _m.Called(ctx, userID, excludeTeamID, includeDeleted)
var r0 []*model.TeamMember
if rf, ok := ret.Get(0).(func(context.Context, string, string, bool) []*model.TeamMember); ok {
r0 = rf(ctx, userID, excludeTeamID, includeDeleted)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.TeamMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, string, bool) error); ok {
r1 = rf(ctx, userID, excludeTeamID, includeDeleted)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamsForUserWithPagination provides a mock function with given fields: userID, page, perPage
func (_m *TeamStore) GetTeamsForUserWithPagination(userID string, page int, perPage int) ([]*model.TeamMember, error) {
ret := _m.Called(userID, page, perPage)
var r0 []*model.TeamMember
if rf, ok := ret.Get(0).(func(string, int, int) []*model.TeamMember); ok {
r0 = rf(userID, page, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.TeamMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(userID, page, perPage)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTotalMemberCount provides a mock function with given fields: teamID, restrictions
func (_m *TeamStore) GetTotalMemberCount(teamID string, restrictions *model.ViewUsersRestrictions) (int64, error) {
ret := _m.Called(teamID, restrictions)
var r0 int64
if rf, ok := ret.Get(0).(func(string, *model.ViewUsersRestrictions) int64); ok {
r0 = rf(teamID, restrictions)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, *model.ViewUsersRestrictions) error); ok {
r1 = rf(teamID, restrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUserTeamIds provides a mock function with given fields: userID, allowFromCache
func (_m *TeamStore) GetUserTeamIds(userID string, allowFromCache bool) ([]string, error) {
ret := _m.Called(userID, allowFromCache)
var r0 []string
if rf, ok := ret.Get(0).(func(string, bool) []string); ok {
r0 = rf(userID, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(userID, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GroupSyncedTeamCount provides a mock function with given fields:
func (_m *TeamStore) GroupSyncedTeamCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// InvalidateAllTeamIdsForUser provides a mock function with given fields: userID
func (_m *TeamStore) InvalidateAllTeamIdsForUser(userID string) {
_m.Called(userID)
}
// MigrateTeamMembers provides a mock function with given fields: fromTeamID, fromUserID
func (_m *TeamStore) MigrateTeamMembers(fromTeamID string, fromUserID string) (map[string]string, error) {
ret := _m.Called(fromTeamID, fromUserID)
var r0 map[string]string
if rf, ok := ret.Get(0).(func(string, string) map[string]string); ok {
r0 = rf(fromTeamID, fromUserID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(fromTeamID, fromUserID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDelete provides a mock function with given fields: teamID
func (_m *TeamStore) PermanentDelete(teamID string) error {
ret := _m.Called(teamID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(teamID)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveAllMembersByTeam provides a mock function with given fields: teamID
func (_m *TeamStore) RemoveAllMembersByTeam(teamID string) error {
ret := _m.Called(teamID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(teamID)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveAllMembersByUser provides a mock function with given fields: userID
func (_m *TeamStore) RemoveAllMembersByUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveMember provides a mock function with given fields: teamID, userID
func (_m *TeamStore) RemoveMember(teamID string, userID string) error {
ret := _m.Called(teamID, userID)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(teamID, userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveMembers provides a mock function with given fields: teamID, userIds
func (_m *TeamStore) RemoveMembers(teamID string, userIds []string) error {
ret := _m.Called(teamID, userIds)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(teamID, userIds)
} else {
r0 = ret.Error(0)
}
return r0
}
// ResetAllTeamSchemes provides a mock function with given fields:
func (_m *TeamStore) ResetAllTeamSchemes() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: team
func (_m *TeamStore) Save(team *model.Team) (*model.Team, error) {
ret := _m.Called(team)
var r0 *model.Team
if rf, ok := ret.Get(0).(func(*model.Team) *model.Team); ok {
r0 = rf(team)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Team) error); ok {
r1 = rf(team)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveMember provides a mock function with given fields: member, maxUsersPerTeam
func (_m *TeamStore) SaveMember(member *model.TeamMember, maxUsersPerTeam int) (*model.TeamMember, error) {
ret := _m.Called(member, maxUsersPerTeam)
var r0 *model.TeamMember
if rf, ok := ret.Get(0).(func(*model.TeamMember, int) *model.TeamMember); ok {
r0 = rf(member, maxUsersPerTeam)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TeamMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.TeamMember, int) error); ok {
r1 = rf(member, maxUsersPerTeam)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveMultipleMembers provides a mock function with given fields: members, maxUsersPerTeam
func (_m *TeamStore) SaveMultipleMembers(members []*model.TeamMember, maxUsersPerTeam int) ([]*model.TeamMember, error) {
ret := _m.Called(members, maxUsersPerTeam)
var r0 []*model.TeamMember
if rf, ok := ret.Get(0).(func([]*model.TeamMember, int) []*model.TeamMember); ok {
r0 = rf(members, maxUsersPerTeam)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.TeamMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]*model.TeamMember, int) error); ok {
r1 = rf(members, maxUsersPerTeam)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchAll provides a mock function with given fields: opts
func (_m *TeamStore) SearchAll(opts *model.TeamSearch) ([]*model.Team, error) {
ret := _m.Called(opts)
var r0 []*model.Team
if rf, ok := ret.Get(0).(func(*model.TeamSearch) []*model.Team); ok {
r0 = rf(opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.TeamSearch) error); ok {
r1 = rf(opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchAllPaged provides a mock function with given fields: opts
func (_m *TeamStore) SearchAllPaged(opts *model.TeamSearch) ([]*model.Team, int64, error) {
ret := _m.Called(opts)
var r0 []*model.Team
if rf, ok := ret.Get(0).(func(*model.TeamSearch) []*model.Team); ok {
r0 = rf(opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 int64
if rf, ok := ret.Get(1).(func(*model.TeamSearch) int64); ok {
r1 = rf(opts)
} else {
r1 = ret.Get(1).(int64)
}
var r2 error
if rf, ok := ret.Get(2).(func(*model.TeamSearch) error); ok {
r2 = rf(opts)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// SearchOpen provides a mock function with given fields: opts
func (_m *TeamStore) SearchOpen(opts *model.TeamSearch) ([]*model.Team, error) {
ret := _m.Called(opts)
var r0 []*model.Team
if rf, ok := ret.Get(0).(func(*model.TeamSearch) []*model.Team); ok {
r0 = rf(opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.TeamSearch) error); ok {
r1 = rf(opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchPrivate provides a mock function with given fields: opts
func (_m *TeamStore) SearchPrivate(opts *model.TeamSearch) ([]*model.Team, error) {
ret := _m.Called(opts)
var r0 []*model.Team
if rf, ok := ret.Get(0).(func(*model.TeamSearch) []*model.Team); ok {
r0 = rf(opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.TeamSearch) error); ok {
r1 = rf(opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: team
func (_m *TeamStore) Update(team *model.Team) (*model.Team, error) {
ret := _m.Called(team)
var r0 *model.Team
if rf, ok := ret.Get(0).(func(*model.Team) *model.Team); ok {
r0 = rf(team)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Team)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.Team) error); ok {
r1 = rf(team)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateLastTeamIconUpdate provides a mock function with given fields: teamID, curTime
func (_m *TeamStore) UpdateLastTeamIconUpdate(teamID string, curTime int64) error {
ret := _m.Called(teamID, curTime)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64) error); ok {
r0 = rf(teamID, curTime)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateMember provides a mock function with given fields: member
func (_m *TeamStore) UpdateMember(member *model.TeamMember) (*model.TeamMember, error) {
ret := _m.Called(member)
var r0 *model.TeamMember
if rf, ok := ret.Get(0).(func(*model.TeamMember) *model.TeamMember); ok {
r0 = rf(member)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TeamMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.TeamMember) error); ok {
r1 = rf(member)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateMembersRole provides a mock function with given fields: teamID, userIDs
func (_m *TeamStore) UpdateMembersRole(teamID string, userIDs []string) error {
ret := _m.Called(teamID, userIDs)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(teamID, userIDs)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateMultipleMembers provides a mock function with given fields: members
func (_m *TeamStore) UpdateMultipleMembers(members []*model.TeamMember) ([]*model.TeamMember, error) {
ret := _m.Called(members)
var r0 []*model.TeamMember
if rf, ok := ret.Get(0).(func([]*model.TeamMember) []*model.TeamMember); ok {
r0 = rf(members)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.TeamMember)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]*model.TeamMember) error); ok {
r1 = rf(members)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UserBelongsToTeams provides a mock function with given fields: userID, teamIds
func (_m *TeamStore) UserBelongsToTeams(userID string, teamIds []string) (bool, error) {
ret := _m.Called(userID, teamIds)
var r0 bool
if rf, ok := ret.Get(0).(func(string, []string) bool); ok {
r0 = rf(userID, teamIds)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string) error); ok {
r1 = rf(userID, teamIds)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// TermsOfServiceStore is an autogenerated mock type for the TermsOfServiceStore type
type TermsOfServiceStore struct {
mock.Mock
}
// Get provides a mock function with given fields: id, allowFromCache
func (_m *TermsOfServiceStore) Get(id string, allowFromCache bool) (*model.TermsOfService, error) {
ret := _m.Called(id, allowFromCache)
var r0 *model.TermsOfService
if rf, ok := ret.Get(0).(func(string, bool) *model.TermsOfService); ok {
r0 = rf(id, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TermsOfService)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(id, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetLatest provides a mock function with given fields: allowFromCache
func (_m *TermsOfServiceStore) GetLatest(allowFromCache bool) (*model.TermsOfService, error) {
ret := _m.Called(allowFromCache)
var r0 *model.TermsOfService
if rf, ok := ret.Get(0).(func(bool) *model.TermsOfService); ok {
r0 = rf(allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TermsOfService)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(bool) error); ok {
r1 = rf(allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: termsOfService
func (_m *TermsOfServiceStore) Save(termsOfService *model.TermsOfService) (*model.TermsOfService, error) {
ret := _m.Called(termsOfService)
var r0 *model.TermsOfService
if rf, ok := ret.Get(0).(func(*model.TermsOfService) *model.TermsOfService); ok {
r0 = rf(termsOfService)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TermsOfService)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.TermsOfService) error); ok {
r1 = rf(termsOfService)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
store "github.com/mattermost/mattermost-server/v6/server/channels/store"
mock "github.com/stretchr/testify/mock"
)
// ThreadStore is an autogenerated mock type for the ThreadStore type
type ThreadStore struct {
mock.Mock
}
// DeleteMembershipForUser provides a mock function with given fields: userId, postID
func (_m *ThreadStore) DeleteMembershipForUser(userId string, postID string) error {
ret := _m.Called(userId, postID)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(userId, postID)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteOrphanedRows provides a mock function with given fields: limit
func (_m *ThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
ret := _m.Called(limit)
var r0 int64
if rf, ok := ret.Get(0).(func(int) int64); ok {
r0 = rf(limit)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: id
func (_m *ThreadStore) Get(id string) (*model.Thread, error) {
ret := _m.Called(id)
var r0 *model.Thread
if rf, ok := ret.Get(0).(func(string) *model.Thread); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Thread)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembershipForUser provides a mock function with given fields: userId, postID
func (_m *ThreadStore) GetMembershipForUser(userId string, postID string) (*model.ThreadMembership, error) {
ret := _m.Called(userId, postID)
var r0 *model.ThreadMembership
if rf, ok := ret.Get(0).(func(string, string) *model.ThreadMembership); ok {
r0 = rf(userId, postID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ThreadMembership)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userId, postID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMembershipsForUser provides a mock function with given fields: userId, teamID
func (_m *ThreadStore) GetMembershipsForUser(userId string, teamID string) ([]*model.ThreadMembership, error) {
ret := _m.Called(userId, teamID)
var r0 []*model.ThreadMembership
if rf, ok := ret.Get(0).(func(string, string) []*model.ThreadMembership); ok {
r0 = rf(userId, teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ThreadMembership)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userId, teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamsUnreadForUser provides a mock function with given fields: userID, teamIDs, includeUrgentMentionCount
func (_m *ThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string, includeUrgentMentionCount bool) (map[string]*model.TeamUnread, error) {
ret := _m.Called(userID, teamIDs, includeUrgentMentionCount)
var r0 map[string]*model.TeamUnread
if rf, ok := ret.Get(0).(func(string, []string, bool) map[string]*model.TeamUnread); ok {
r0 = rf(userID, teamIDs, includeUrgentMentionCount)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]*model.TeamUnread)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string, bool) error); ok {
r1 = rf(userID, teamIDs, includeUrgentMentionCount)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetThreadFollowers provides a mock function with given fields: threadID, fetchOnlyActive
func (_m *ThreadStore) GetThreadFollowers(threadID string, fetchOnlyActive bool) ([]string, error) {
ret := _m.Called(threadID, fetchOnlyActive)
var r0 []string
if rf, ok := ret.Get(0).(func(string, bool) []string); ok {
r0 = rf(threadID, fetchOnlyActive)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(threadID, fetchOnlyActive)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetThreadForUser provides a mock function with given fields: threadMembership, extended, postPriorityIsEnabled
func (_m *ThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool, postPriorityIsEnabled bool) (*model.ThreadResponse, error) {
ret := _m.Called(threadMembership, extended, postPriorityIsEnabled)
var r0 *model.ThreadResponse
if rf, ok := ret.Get(0).(func(*model.ThreadMembership, bool, bool) *model.ThreadResponse); ok {
r0 = rf(threadMembership, extended, postPriorityIsEnabled)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ThreadResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.ThreadMembership, bool, bool) error); ok {
r1 = rf(threadMembership, extended, postPriorityIsEnabled)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetThreadUnreadReplyCount provides a mock function with given fields: threadMembership
func (_m *ThreadStore) GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error) {
ret := _m.Called(threadMembership)
var r0 int64
if rf, ok := ret.Get(0).(func(*model.ThreadMembership) int64); ok {
r0 = rf(threadMembership)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.ThreadMembership) error); ok {
r1 = rf(threadMembership)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetThreadsForUser provides a mock function with given fields: userId, teamID, opts
func (_m *ThreadStore) GetThreadsForUser(userId string, teamID string, opts model.GetUserThreadsOpts) ([]*model.ThreadResponse, error) {
ret := _m.Called(userId, teamID, opts)
var r0 []*model.ThreadResponse
if rf, ok := ret.Get(0).(func(string, string, model.GetUserThreadsOpts) []*model.ThreadResponse); ok {
r0 = rf(userId, teamID, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ThreadResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, model.GetUserThreadsOpts) error); ok {
r1 = rf(userId, teamID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopThreadsForTeamSince provides a mock function with given fields: teamID, userID, since, offset, limit
func (_m *ThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
ret := _m.Called(teamID, userID, since, offset, limit)
var r0 *model.TopThreadList
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopThreadList); ok {
r0 = rf(teamID, userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopThreadList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(teamID, userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopThreadsForUserSince provides a mock function with given fields: teamID, userID, since, offset, limit
func (_m *ThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
ret := _m.Called(teamID, userID, since, offset, limit)
var r0 *model.TopThreadList
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopThreadList); ok {
r0 = rf(teamID, userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopThreadList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(teamID, userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTotalThreads provides a mock function with given fields: userId, teamID, opts
func (_m *ThreadStore) GetTotalThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
ret := _m.Called(userId, teamID, opts)
var r0 int64
if rf, ok := ret.Get(0).(func(string, string, model.GetUserThreadsOpts) int64); ok {
r0 = rf(userId, teamID, opts)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, model.GetUserThreadsOpts) error); ok {
r1 = rf(userId, teamID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTotalUnreadMentions provides a mock function with given fields: userId, teamID, opts
func (_m *ThreadStore) GetTotalUnreadMentions(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
ret := _m.Called(userId, teamID, opts)
var r0 int64
if rf, ok := ret.Get(0).(func(string, string, model.GetUserThreadsOpts) int64); ok {
r0 = rf(userId, teamID, opts)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, model.GetUserThreadsOpts) error); ok {
r1 = rf(userId, teamID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTotalUnreadThreads provides a mock function with given fields: userId, teamID, opts
func (_m *ThreadStore) GetTotalUnreadThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
ret := _m.Called(userId, teamID, opts)
var r0 int64
if rf, ok := ret.Get(0).(func(string, string, model.GetUserThreadsOpts) int64); ok {
r0 = rf(userId, teamID, opts)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, model.GetUserThreadsOpts) error); ok {
r1 = rf(userId, teamID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTotalUnreadUrgentMentions provides a mock function with given fields: userId, teamID, opts
func (_m *ThreadStore) GetTotalUnreadUrgentMentions(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
ret := _m.Called(userId, teamID, opts)
var r0 int64
if rf, ok := ret.Get(0).(func(string, string, model.GetUserThreadsOpts) int64); ok {
r0 = rf(userId, teamID, opts)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, model.GetUserThreadsOpts) error); ok {
r1 = rf(userId, teamID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MaintainMembership provides a mock function with given fields: userID, postID, opts
func (_m *ThreadStore) MaintainMembership(userID string, postID string, opts store.ThreadMembershipOpts) (*model.ThreadMembership, error) {
ret := _m.Called(userID, postID, opts)
var r0 *model.ThreadMembership
if rf, ok := ret.Get(0).(func(string, string, store.ThreadMembershipOpts) *model.ThreadMembership); ok {
r0 = rf(userID, postID, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ThreadMembership)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, store.ThreadMembershipOpts) error); ok {
r1 = rf(userID, postID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MarkAllAsRead provides a mock function with given fields: userID, threadIds
func (_m *ThreadStore) MarkAllAsRead(userID string, threadIds []string) error {
ret := _m.Called(userID, threadIds)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(userID, threadIds)
} else {
r0 = ret.Error(0)
}
return r0
}
// MarkAllAsReadByChannels provides a mock function with given fields: userID, channelIDs
func (_m *ThreadStore) MarkAllAsReadByChannels(userID string, channelIDs []string) error {
ret := _m.Called(userID, channelIDs)
var r0 error
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
r0 = rf(userID, channelIDs)
} else {
r0 = ret.Error(0)
}
return r0
}
// MarkAllAsReadByTeam provides a mock function with given fields: userID, teamID
func (_m *ThreadStore) MarkAllAsReadByTeam(userID string, teamID string) error {
ret := _m.Called(userID, teamID)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(userID, teamID)
} else {
r0 = ret.Error(0)
}
return r0
}
// MarkAsRead provides a mock function with given fields: userID, threadID, timestamp
func (_m *ThreadStore) MarkAsRead(userID string, threadID string, timestamp int64) error {
ret := _m.Called(userID, threadID, timestamp)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, int64) error); ok {
r0 = rf(userID, threadID, timestamp)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteBatchForRetentionPolicies provides a mock function with given fields: now, globalPolicyEndTime, limit, cursor
func (_m *ThreadStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
ret := _m.Called(now, globalPolicyEndTime, limit, cursor)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64, int64, model.RetentionPolicyCursor) int64); ok {
r0 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r0 = ret.Get(0).(int64)
}
var r1 model.RetentionPolicyCursor
if rf, ok := ret.Get(1).(func(int64, int64, int64, model.RetentionPolicyCursor) model.RetentionPolicyCursor); ok {
r1 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r1 = ret.Get(1).(model.RetentionPolicyCursor)
}
var r2 error
if rf, ok := ret.Get(2).(func(int64, int64, int64, model.RetentionPolicyCursor) error); ok {
r2 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// PermanentDeleteBatchThreadMembershipsForRetentionPolicies provides a mock function with given fields: now, globalPolicyEndTime, limit, cursor
func (_m *ThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
ret := _m.Called(now, globalPolicyEndTime, limit, cursor)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64, int64, model.RetentionPolicyCursor) int64); ok {
r0 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r0 = ret.Get(0).(int64)
}
var r1 model.RetentionPolicyCursor
if rf, ok := ret.Get(1).(func(int64, int64, int64, model.RetentionPolicyCursor) model.RetentionPolicyCursor); ok {
r1 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r1 = ret.Get(1).(model.RetentionPolicyCursor)
}
var r2 error
if rf, ok := ret.Get(2).(func(int64, int64, int64, model.RetentionPolicyCursor) error); ok {
r2 = rf(now, globalPolicyEndTime, limit, cursor)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// UpdateMembership provides a mock function with given fields: membership
func (_m *ThreadStore) UpdateMembership(membership *model.ThreadMembership) (*model.ThreadMembership, error) {
ret := _m.Called(membership)
var r0 *model.ThreadMembership
if rf, ok := ret.Get(0).(func(*model.ThreadMembership) *model.ThreadMembership); ok {
r0 = rf(membership)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ThreadMembership)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.ThreadMembership) error); ok {
r1 = rf(membership)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// TokenStore is an autogenerated mock type for the TokenStore type
type TokenStore struct {
mock.Mock
}
// Cleanup provides a mock function with given fields: expiryTime
func (_m *TokenStore) Cleanup(expiryTime int64) {
_m.Called(expiryTime)
}
// Delete provides a mock function with given fields: token
func (_m *TokenStore) Delete(token string) error {
ret := _m.Called(token)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(token)
} else {
r0 = ret.Error(0)
}
return r0
}
// GetAllTokensByType provides a mock function with given fields: tokenType
func (_m *TokenStore) GetAllTokensByType(tokenType string) ([]*model.Token, error) {
ret := _m.Called(tokenType)
var r0 []*model.Token
if rf, ok := ret.Get(0).(func(string) []*model.Token); ok {
r0 = rf(tokenType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Token)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(tokenType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByToken provides a mock function with given fields: token
func (_m *TokenStore) GetByToken(token string) (*model.Token, error) {
ret := _m.Called(token)
var r0 *model.Token
if rf, ok := ret.Get(0).(func(string) *model.Token); ok {
r0 = rf(token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Token)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(token)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RemoveAllTokensByType provides a mock function with given fields: tokenType
func (_m *TokenStore) RemoveAllTokensByType(tokenType string) error {
ret := _m.Called(tokenType)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(tokenType)
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: recovery
func (_m *TokenStore) Save(recovery *model.Token) error {
ret := _m.Called(recovery)
var r0 error
if rf, ok := ret.Get(0).(func(*model.Token) error); ok {
r0 = rf(recovery)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// TrueUpReviewStore is an autogenerated mock type for the TrueUpReviewStore type
type TrueUpReviewStore struct {
mock.Mock
}
// CreateTrueUpReviewStatusRecord provides a mock function with given fields: reviewStatus
func (_m *TrueUpReviewStore) CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
ret := _m.Called(reviewStatus)
var r0 *model.TrueUpReviewStatus
if rf, ok := ret.Get(0).(func(*model.TrueUpReviewStatus) *model.TrueUpReviewStatus); ok {
r0 = rf(reviewStatus)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TrueUpReviewStatus)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.TrueUpReviewStatus) error); ok {
r1 = rf(reviewStatus)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTrueUpReviewStatus provides a mock function with given fields: dueDate
func (_m *TrueUpReviewStore) GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error) {
ret := _m.Called(dueDate)
var r0 *model.TrueUpReviewStatus
if rf, ok := ret.Get(0).(func(int64) *model.TrueUpReviewStatus); ok {
r0 = rf(dueDate)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TrueUpReviewStatus)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64) error); ok {
r1 = rf(dueDate)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: reviewStatus
func (_m *TrueUpReviewStore) Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
ret := _m.Called(reviewStatus)
var r0 *model.TrueUpReviewStatus
if rf, ok := ret.Get(0).(func(*model.TrueUpReviewStatus) *model.TrueUpReviewStatus); ok {
r0 = rf(reviewStatus)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TrueUpReviewStatus)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.TrueUpReviewStatus) error); ok {
r1 = rf(reviewStatus)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
context "context"
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// UploadSessionStore is an autogenerated mock type for the UploadSessionStore type
type UploadSessionStore struct {
mock.Mock
}
// Delete provides a mock function with given fields: id
func (_m *UploadSessionStore) Delete(id string) error {
ret := _m.Called(id)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(id)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: ctx, id
func (_m *UploadSessionStore) Get(ctx context.Context, id string) (*model.UploadSession, error) {
ret := _m.Called(ctx, id)
var r0 *model.UploadSession
if rf, ok := ret.Get(0).(func(context.Context, string) *model.UploadSession); ok {
r0 = rf(ctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.UploadSession)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForUser provides a mock function with given fields: userID
func (_m *UploadSessionStore) GetForUser(userID string) ([]*model.UploadSession, error) {
ret := _m.Called(userID)
var r0 []*model.UploadSession
if rf, ok := ret.Get(0).(func(string) []*model.UploadSession); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.UploadSession)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: session
func (_m *UploadSessionStore) Save(session *model.UploadSession) (*model.UploadSession, error) {
ret := _m.Called(session)
var r0 *model.UploadSession
if rf, ok := ret.Get(0).(func(*model.UploadSession) *model.UploadSession); ok {
r0 = rf(session)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.UploadSession)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.UploadSession) error); ok {
r1 = rf(session)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: session
func (_m *UploadSessionStore) Update(session *model.UploadSession) error {
ret := _m.Called(session)
var r0 error
if rf, ok := ret.Get(0).(func(*model.UploadSession) error); ok {
r0 = rf(session)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// UserAccessTokenStore is an autogenerated mock type for the UserAccessTokenStore type
type UserAccessTokenStore struct {
mock.Mock
}
// Delete provides a mock function with given fields: tokenID
func (_m *UserAccessTokenStore) Delete(tokenID string) error {
ret := _m.Called(tokenID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(tokenID)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteAllForUser provides a mock function with given fields: userID
func (_m *UserAccessTokenStore) DeleteAllForUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: tokenID
func (_m *UserAccessTokenStore) Get(tokenID string) (*model.UserAccessToken, error) {
ret := _m.Called(tokenID)
var r0 *model.UserAccessToken
if rf, ok := ret.Get(0).(func(string) *model.UserAccessToken); ok {
r0 = rf(tokenID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.UserAccessToken)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(tokenID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields: offset, limit
func (_m *UserAccessTokenStore) GetAll(offset int, limit int) ([]*model.UserAccessToken, error) {
ret := _m.Called(offset, limit)
var r0 []*model.UserAccessToken
if rf, ok := ret.Get(0).(func(int, int) []*model.UserAccessToken); ok {
r0 = rf(offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.UserAccessToken)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int) error); ok {
r1 = rf(offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByToken provides a mock function with given fields: tokenString
func (_m *UserAccessTokenStore) GetByToken(tokenString string) (*model.UserAccessToken, error) {
ret := _m.Called(tokenString)
var r0 *model.UserAccessToken
if rf, ok := ret.Get(0).(func(string) *model.UserAccessToken); ok {
r0 = rf(tokenString)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.UserAccessToken)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(tokenString)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByUser provides a mock function with given fields: userID, page, perPage
func (_m *UserAccessTokenStore) GetByUser(userID string, page int, perPage int) ([]*model.UserAccessToken, error) {
ret := _m.Called(userID, page, perPage)
var r0 []*model.UserAccessToken
if rf, ok := ret.Get(0).(func(string, int, int) []*model.UserAccessToken); ok {
r0 = rf(userID, page, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.UserAccessToken)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(userID, page, perPage)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: token
func (_m *UserAccessTokenStore) Save(token *model.UserAccessToken) (*model.UserAccessToken, error) {
ret := _m.Called(token)
var r0 *model.UserAccessToken
if rf, ok := ret.Get(0).(func(*model.UserAccessToken) *model.UserAccessToken); ok {
r0 = rf(token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.UserAccessToken)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.UserAccessToken) error); ok {
r1 = rf(token)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Search provides a mock function with given fields: term
func (_m *UserAccessTokenStore) Search(term string) ([]*model.UserAccessToken, error) {
ret := _m.Called(term)
var r0 []*model.UserAccessToken
if rf, ok := ret.Get(0).(func(string) []*model.UserAccessToken); ok {
r0 = rf(term)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.UserAccessToken)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(term)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateTokenDisable provides a mock function with given fields: tokenID
func (_m *UserAccessTokenStore) UpdateTokenDisable(tokenID string) error {
ret := _m.Called(tokenID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(tokenID)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateTokenEnable provides a mock function with given fields: tokenID
func (_m *UserAccessTokenStore) UpdateTokenEnable(tokenID string) error {
ret := _m.Called(tokenID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(tokenID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
context "context"
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
store "github.com/mattermost/mattermost-server/v6/server/channels/store"
)
// UserStore is an autogenerated mock type for the UserStore type
type UserStore struct {
mock.Mock
}
// AnalyticsActiveCount provides a mock function with given fields: timestamp, options
func (_m *UserStore) AnalyticsActiveCount(timestamp int64, options model.UserCountOptions) (int64, error) {
ret := _m.Called(timestamp, options)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, model.UserCountOptions) int64); ok {
r0 = rf(timestamp, options)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, model.UserCountOptions) error); ok {
r1 = rf(timestamp, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AnalyticsActiveCountForPeriod provides a mock function with given fields: startTime, endTime, options
func (_m *UserStore) AnalyticsActiveCountForPeriod(startTime int64, endTime int64, options model.UserCountOptions) (int64, error) {
ret := _m.Called(startTime, endTime, options)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int64, model.UserCountOptions) int64); ok {
r0 = rf(startTime, endTime, options)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, int64, model.UserCountOptions) error); ok {
r1 = rf(startTime, endTime, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AnalyticsGetExternalUsers provides a mock function with given fields: hostDomain
func (_m *UserStore) AnalyticsGetExternalUsers(hostDomain string) (bool, error) {
ret := _m.Called(hostDomain)
var r0 bool
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(hostDomain)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(hostDomain)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AnalyticsGetGuestCount provides a mock function with given fields:
func (_m *UserStore) AnalyticsGetGuestCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AnalyticsGetInactiveUsersCount provides a mock function with given fields:
func (_m *UserStore) AnalyticsGetInactiveUsersCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AnalyticsGetSystemAdminCount provides a mock function with given fields:
func (_m *UserStore) AnalyticsGetSystemAdminCount() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AutocompleteUsersInChannel provides a mock function with given fields: teamID, channelID, term, options
func (_m *UserStore) AutocompleteUsersInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error) {
ret := _m.Called(teamID, channelID, term, options)
var r0 *model.UserAutocompleteInChannel
if rf, ok := ret.Get(0).(func(string, string, string, *model.UserSearchOptions) *model.UserAutocompleteInChannel); ok {
r0 = rf(teamID, channelID, term, options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.UserAutocompleteInChannel)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string, *model.UserSearchOptions) error); ok {
r1 = rf(teamID, channelID, term, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ClearAllCustomRoleAssignments provides a mock function with given fields:
func (_m *UserStore) ClearAllCustomRoleAssignments() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// ClearCaches provides a mock function with given fields:
func (_m *UserStore) ClearCaches() {
_m.Called()
}
// Count provides a mock function with given fields: options
func (_m *UserStore) Count(options model.UserCountOptions) (int64, error) {
ret := _m.Called(options)
var r0 int64
if rf, ok := ret.Get(0).(func(model.UserCountOptions) int64); ok {
r0 = rf(options)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(model.UserCountOptions) error); ok {
r1 = rf(options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeactivateGuests provides a mock function with given fields:
func (_m *UserStore) DeactivateGuests() ([]string, error) {
ret := _m.Called()
var r0 []string
if rf, ok := ret.Get(0).(func() []string); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DemoteUserToGuest provides a mock function with given fields: userID
func (_m *UserStore) DemoteUserToGuest(userID string) (*model.User, error) {
ret := _m.Called(userID)
var r0 *model.User
if rf, ok := ret.Get(0).(func(string) *model.User); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: ctx, id
func (_m *UserStore) Get(ctx context.Context, id string) (*model.User, error) {
ret := _m.Called(ctx, id)
var r0 *model.User
if rf, ok := ret.Get(0).(func(context.Context, string) *model.User); ok {
r0 = rf(ctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAll provides a mock function with given fields:
func (_m *UserStore) GetAll() ([]*model.User, error) {
ret := _m.Called()
var r0 []*model.User
if rf, ok := ret.Get(0).(func() []*model.User); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllAfter provides a mock function with given fields: limit, afterID
func (_m *UserStore) GetAllAfter(limit int, afterID string) ([]*model.User, error) {
ret := _m.Called(limit, afterID)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(int, string) []*model.User); ok {
r0 = rf(limit, afterID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, string) error); ok {
r1 = rf(limit, afterID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllNotInAuthService provides a mock function with given fields: authServices
func (_m *UserStore) GetAllNotInAuthService(authServices []string) ([]*model.User, error) {
ret := _m.Called(authServices)
var r0 []*model.User
if rf, ok := ret.Get(0).(func([]string) []*model.User); ok {
r0 = rf(authServices)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(authServices)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllProfiles provides a mock function with given fields: options
func (_m *UserStore) GetAllProfiles(options *model.UserGetOptions) ([]*model.User, error) {
ret := _m.Called(options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(*model.UserGetOptions) []*model.User); ok {
r0 = rf(options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.UserGetOptions) error); ok {
r1 = rf(options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllProfilesInChannel provides a mock function with given fields: ctx, channelID, allowFromCache
func (_m *UserStore) GetAllProfilesInChannel(ctx context.Context, channelID string, allowFromCache bool) (map[string]*model.User, error) {
ret := _m.Called(ctx, channelID, allowFromCache)
var r0 map[string]*model.User
if rf, ok := ret.Get(0).(func(context.Context, string, bool) map[string]*model.User); ok {
r0 = rf(ctx, channelID, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, bool) error); ok {
r1 = rf(ctx, channelID, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllUsingAuthService provides a mock function with given fields: authService
func (_m *UserStore) GetAllUsingAuthService(authService string) ([]*model.User, error) {
ret := _m.Called(authService)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string) []*model.User); ok {
r0 = rf(authService)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(authService)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAnyUnreadPostCountForChannel provides a mock function with given fields: userID, channelID
func (_m *UserStore) GetAnyUnreadPostCountForChannel(userID string, channelID string) (int64, error) {
ret := _m.Called(userID, channelID)
var r0 int64
if rf, ok := ret.Get(0).(func(string, string) int64); ok {
r0 = rf(userID, channelID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByAuth provides a mock function with given fields: authData, authService
func (_m *UserStore) GetByAuth(authData *string, authService string) (*model.User, error) {
ret := _m.Called(authData, authService)
var r0 *model.User
if rf, ok := ret.Get(0).(func(*string, string) *model.User); ok {
r0 = rf(authData, authService)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*string, string) error); ok {
r1 = rf(authData, authService)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByEmail provides a mock function with given fields: email
func (_m *UserStore) GetByEmail(email string) (*model.User, error) {
ret := _m.Called(email)
var r0 *model.User
if rf, ok := ret.Get(0).(func(string) *model.User); ok {
r0 = rf(email)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(email)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByUsername provides a mock function with given fields: username
func (_m *UserStore) GetByUsername(username string) (*model.User, error) {
ret := _m.Called(username)
var r0 *model.User
if rf, ok := ret.Get(0).(func(string) *model.User); ok {
r0 = rf(username)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(username)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetChannelGroupUsers provides a mock function with given fields: channelID
func (_m *UserStore) GetChannelGroupUsers(channelID string) ([]*model.User, error) {
ret := _m.Called(channelID)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string) []*model.User); ok {
r0 = rf(channelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetEtagForAllProfiles provides a mock function with given fields:
func (_m *UserStore) GetEtagForAllProfiles() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// GetEtagForProfiles provides a mock function with given fields: teamID
func (_m *UserStore) GetEtagForProfiles(teamID string) string {
ret := _m.Called(teamID)
var r0 string
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(teamID)
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// GetEtagForProfilesNotInTeam provides a mock function with given fields: teamID
func (_m *UserStore) GetEtagForProfilesNotInTeam(teamID string) string {
ret := _m.Called(teamID)
var r0 string
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(teamID)
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// GetFirstSystemAdminID provides a mock function with given fields:
func (_m *UserStore) GetFirstSystemAdminID() (string, error) {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForLogin provides a mock function with given fields: loginID, allowSignInWithUsername, allowSignInWithEmail
func (_m *UserStore) GetForLogin(loginID string, allowSignInWithUsername bool, allowSignInWithEmail bool) (*model.User, error) {
ret := _m.Called(loginID, allowSignInWithUsername, allowSignInWithEmail)
var r0 *model.User
if rf, ok := ret.Get(0).(func(string, bool, bool) *model.User); ok {
r0 = rf(loginID, allowSignInWithUsername, allowSignInWithEmail)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool, bool) error); ok {
r1 = rf(loginID, allowSignInWithUsername, allowSignInWithEmail)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetKnownUsers provides a mock function with given fields: userID
func (_m *UserStore) GetKnownUsers(userID string) ([]string, error) {
ret := _m.Called(userID)
var r0 []string
if rf, ok := ret.Get(0).(func(string) []string); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMany provides a mock function with given fields: ctx, ids
func (_m *UserStore) GetMany(ctx context.Context, ids []string) ([]*model.User, error) {
ret := _m.Called(ctx, ids)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(context.Context, []string) []*model.User); ok {
r0 = rf(ctx, ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, []string) error); ok {
r1 = rf(ctx, ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetNewUsersForTeam provides a mock function with given fields: teamID, offset, limit, viewRestrictions
func (_m *UserStore) GetNewUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
ret := _m.Called(teamID, offset, limit, viewRestrictions)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, int, int, *model.ViewUsersRestrictions) []*model.User); ok {
r0 = rf(teamID, offset, limit, viewRestrictions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int, *model.ViewUsersRestrictions) error); ok {
r1 = rf(teamID, offset, limit, viewRestrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProfileByGroupChannelIdsForUser provides a mock function with given fields: userID, channelIds
func (_m *UserStore) GetProfileByGroupChannelIdsForUser(userID string, channelIds []string) (map[string][]*model.User, error) {
ret := _m.Called(userID, channelIds)
var r0 map[string][]*model.User
if rf, ok := ret.Get(0).(func(string, []string) map[string][]*model.User); ok {
r0 = rf(userID, channelIds)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string][]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string) error); ok {
r1 = rf(userID, channelIds)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProfileByIds provides a mock function with given fields: ctx, userIds, options, allowFromCache
func (_m *UserStore) GetProfileByIds(ctx context.Context, userIds []string, options *store.UserGetByIdsOpts, allowFromCache bool) ([]*model.User, error) {
ret := _m.Called(ctx, userIds, options, allowFromCache)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(context.Context, []string, *store.UserGetByIdsOpts, bool) []*model.User); ok {
r0 = rf(ctx, userIds, options, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, []string, *store.UserGetByIdsOpts, bool) error); ok {
r1 = rf(ctx, userIds, options, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProfiles provides a mock function with given fields: options
func (_m *UserStore) GetProfiles(options *model.UserGetOptions) ([]*model.User, error) {
ret := _m.Called(options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(*model.UserGetOptions) []*model.User); ok {
r0 = rf(options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.UserGetOptions) error); ok {
r1 = rf(options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProfilesByUsernames provides a mock function with given fields: usernames, viewRestrictions
func (_m *UserStore) GetProfilesByUsernames(usernames []string, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
ret := _m.Called(usernames, viewRestrictions)
var r0 []*model.User
if rf, ok := ret.Get(0).(func([]string, *model.ViewUsersRestrictions) []*model.User); ok {
r0 = rf(usernames, viewRestrictions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string, *model.ViewUsersRestrictions) error); ok {
r1 = rf(usernames, viewRestrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProfilesInChannel provides a mock function with given fields: options
func (_m *UserStore) GetProfilesInChannel(options *model.UserGetOptions) ([]*model.User, error) {
ret := _m.Called(options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(*model.UserGetOptions) []*model.User); ok {
r0 = rf(options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.UserGetOptions) error); ok {
r1 = rf(options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProfilesInChannelByAdmin provides a mock function with given fields: options
func (_m *UserStore) GetProfilesInChannelByAdmin(options *model.UserGetOptions) ([]*model.User, error) {
ret := _m.Called(options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(*model.UserGetOptions) []*model.User); ok {
r0 = rf(options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.UserGetOptions) error); ok {
r1 = rf(options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProfilesInChannelByStatus provides a mock function with given fields: options
func (_m *UserStore) GetProfilesInChannelByStatus(options *model.UserGetOptions) ([]*model.User, error) {
ret := _m.Called(options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(*model.UserGetOptions) []*model.User); ok {
r0 = rf(options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.UserGetOptions) error); ok {
r1 = rf(options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProfilesNotInChannel provides a mock function with given fields: teamID, channelId, groupConstrained, offset, limit, viewRestrictions
func (_m *UserStore) GetProfilesNotInChannel(teamID string, channelId string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
ret := _m.Called(teamID, channelId, groupConstrained, offset, limit, viewRestrictions)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, string, bool, int, int, *model.ViewUsersRestrictions) []*model.User); ok {
r0 = rf(teamID, channelId, groupConstrained, offset, limit, viewRestrictions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, bool, int, int, *model.ViewUsersRestrictions) error); ok {
r1 = rf(teamID, channelId, groupConstrained, offset, limit, viewRestrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProfilesNotInTeam provides a mock function with given fields: teamID, groupConstrained, offset, limit, viewRestrictions
func (_m *UserStore) GetProfilesNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
ret := _m.Called(teamID, groupConstrained, offset, limit, viewRestrictions)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, bool, int, int, *model.ViewUsersRestrictions) []*model.User); ok {
r0 = rf(teamID, groupConstrained, offset, limit, viewRestrictions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool, int, int, *model.ViewUsersRestrictions) error); ok {
r1 = rf(teamID, groupConstrained, offset, limit, viewRestrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProfilesWithoutTeam provides a mock function with given fields: options
func (_m *UserStore) GetProfilesWithoutTeam(options *model.UserGetOptions) ([]*model.User, error) {
ret := _m.Called(options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(*model.UserGetOptions) []*model.User); ok {
r0 = rf(options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.UserGetOptions) error); ok {
r1 = rf(options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRecentlyActiveUsersForTeam provides a mock function with given fields: teamID, offset, limit, viewRestrictions
func (_m *UserStore) GetRecentlyActiveUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
ret := _m.Called(teamID, offset, limit, viewRestrictions)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, int, int, *model.ViewUsersRestrictions) []*model.User); ok {
r0 = rf(teamID, offset, limit, viewRestrictions)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int, *model.ViewUsersRestrictions) error); ok {
r1 = rf(teamID, offset, limit, viewRestrictions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSystemAdminProfiles provides a mock function with given fields:
func (_m *UserStore) GetSystemAdminProfiles() (map[string]*model.User, error) {
ret := _m.Called()
var r0 map[string]*model.User
if rf, ok := ret.Get(0).(func() map[string]*model.User); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTeamGroupUsers provides a mock function with given fields: teamID
func (_m *UserStore) GetTeamGroupUsers(teamID string) ([]*model.User, error) {
ret := _m.Called(teamID)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string) []*model.User); ok {
r0 = rf(teamID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUnreadCount provides a mock function with given fields: userID, isCRTEnabled
func (_m *UserStore) GetUnreadCount(userID string, isCRTEnabled bool) (int64, error) {
ret := _m.Called(userID, isCRTEnabled)
var r0 int64
if rf, ok := ret.Get(0).(func(string, bool) int64); ok {
r0 = rf(userID, isCRTEnabled)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(userID, isCRTEnabled)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUnreadCountForChannel provides a mock function with given fields: userID, channelID
func (_m *UserStore) GetUnreadCountForChannel(userID string, channelID string) (int64, error) {
ret := _m.Called(userID, channelID)
var r0 int64
if rf, ok := ret.Get(0).(func(string, string) int64); ok {
r0 = rf(userID, channelID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUsersBatchForIndexing provides a mock function with given fields: startTime, startFileID, limit
func (_m *UserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
ret := _m.Called(startTime, startFileID, limit)
var r0 []*model.UserForIndexing
if rf, ok := ret.Get(0).(func(int64, string, int) []*model.UserForIndexing); ok {
r0 = rf(startTime, startFileID, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.UserForIndexing)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, string, int) error); ok {
r1 = rf(startTime, startFileID, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUsersWithInvalidEmails provides a mock function with given fields: page, perPage, restrictedDomains
func (_m *UserStore) GetUsersWithInvalidEmails(page int, perPage int, restrictedDomains string) ([]*model.User, error) {
ret := _m.Called(page, perPage, restrictedDomains)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(int, int, string) []*model.User); ok {
r0 = rf(page, perPage, restrictedDomains)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int, string) error); ok {
r1 = rf(page, perPage, restrictedDomains)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// InferSystemInstallDate provides a mock function with given fields:
func (_m *UserStore) InferSystemInstallDate() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// InsertUsers provides a mock function with given fields: users
func (_m *UserStore) InsertUsers(users []*model.User) error {
ret := _m.Called(users)
var r0 error
if rf, ok := ret.Get(0).(func([]*model.User) error); ok {
r0 = rf(users)
} else {
r0 = ret.Error(0)
}
return r0
}
// InvalidateProfileCacheForUser provides a mock function with given fields: userID
func (_m *UserStore) InvalidateProfileCacheForUser(userID string) {
_m.Called(userID)
}
// InvalidateProfilesInChannelCache provides a mock function with given fields: channelID
func (_m *UserStore) InvalidateProfilesInChannelCache(channelID string) {
_m.Called(channelID)
}
// InvalidateProfilesInChannelCacheByUser provides a mock function with given fields: userID
func (_m *UserStore) InvalidateProfilesInChannelCacheByUser(userID string) {
_m.Called(userID)
}
// IsEmpty provides a mock function with given fields: excludeBots
func (_m *UserStore) IsEmpty(excludeBots bool) (bool, error) {
ret := _m.Called(excludeBots)
var r0 bool
if rf, ok := ret.Get(0).(func(bool) bool); ok {
r0 = rf(excludeBots)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(bool) error); ok {
r1 = rf(excludeBots)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDelete provides a mock function with given fields: userID
func (_m *UserStore) PermanentDelete(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PromoteGuestToUser provides a mock function with given fields: userID
func (_m *UserStore) PromoteGuestToUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// ResetAuthDataToEmailForUsers provides a mock function with given fields: service, userIDs, includeDeleted, dryRun
func (_m *UserStore) ResetAuthDataToEmailForUsers(service string, userIDs []string, includeDeleted bool, dryRun bool) (int, error) {
ret := _m.Called(service, userIDs, includeDeleted, dryRun)
var r0 int
if rf, ok := ret.Get(0).(func(string, []string, bool, bool) int); ok {
r0 = rf(service, userIDs, includeDeleted, dryRun)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []string, bool, bool) error); ok {
r1 = rf(service, userIDs, includeDeleted, dryRun)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ResetLastPictureUpdate provides a mock function with given fields: userID
func (_m *UserStore) ResetLastPictureUpdate(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Save provides a mock function with given fields: user
func (_m *UserStore) Save(user *model.User) (*model.User, error) {
ret := _m.Called(user)
var r0 *model.User
if rf, ok := ret.Get(0).(func(*model.User) *model.User); ok {
r0 = rf(user)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.User) error); ok {
r1 = rf(user)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Search provides a mock function with given fields: teamID, term, options
func (_m *UserStore) Search(teamID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
ret := _m.Called(teamID, term, options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, string, *model.UserSearchOptions) []*model.User); ok {
r0 = rf(teamID, term, options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *model.UserSearchOptions) error); ok {
r1 = rf(teamID, term, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchInChannel provides a mock function with given fields: channelID, term, options
func (_m *UserStore) SearchInChannel(channelID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
ret := _m.Called(channelID, term, options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, string, *model.UserSearchOptions) []*model.User); ok {
r0 = rf(channelID, term, options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *model.UserSearchOptions) error); ok {
r1 = rf(channelID, term, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchInGroup provides a mock function with given fields: groupID, term, options
func (_m *UserStore) SearchInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
ret := _m.Called(groupID, term, options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, string, *model.UserSearchOptions) []*model.User); ok {
r0 = rf(groupID, term, options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *model.UserSearchOptions) error); ok {
r1 = rf(groupID, term, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchNotInChannel provides a mock function with given fields: teamID, channelID, term, options
func (_m *UserStore) SearchNotInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
ret := _m.Called(teamID, channelID, term, options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, string, string, *model.UserSearchOptions) []*model.User); ok {
r0 = rf(teamID, channelID, term, options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string, *model.UserSearchOptions) error); ok {
r1 = rf(teamID, channelID, term, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchNotInGroup provides a mock function with given fields: groupID, term, options
func (_m *UserStore) SearchNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
ret := _m.Called(groupID, term, options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, string, *model.UserSearchOptions) []*model.User); ok {
r0 = rf(groupID, term, options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *model.UserSearchOptions) error); ok {
r1 = rf(groupID, term, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchNotInTeam provides a mock function with given fields: notInTeamID, term, options
func (_m *UserStore) SearchNotInTeam(notInTeamID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
ret := _m.Called(notInTeamID, term, options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, string, *model.UserSearchOptions) []*model.User); ok {
r0 = rf(notInTeamID, term, options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *model.UserSearchOptions) error); ok {
r1 = rf(notInTeamID, term, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchWithoutTeam provides a mock function with given fields: term, options
func (_m *UserStore) SearchWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, error) {
ret := _m.Called(term, options)
var r0 []*model.User
if rf, ok := ret.Get(0).(func(string, *model.UserSearchOptions) []*model.User); ok {
r0 = rf(term, options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, *model.UserSearchOptions) error); ok {
r1 = rf(term, options)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: user, allowRoleUpdate
func (_m *UserStore) Update(user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error) {
ret := _m.Called(user, allowRoleUpdate)
var r0 *model.UserUpdate
if rf, ok := ret.Get(0).(func(*model.User, bool) *model.UserUpdate); ok {
r0 = rf(user, allowRoleUpdate)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.UserUpdate)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.User, bool) error); ok {
r1 = rf(user, allowRoleUpdate)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateAuthData provides a mock function with given fields: userID, service, authData, email, resetMfa
func (_m *UserStore) UpdateAuthData(userID string, service string, authData *string, email string, resetMfa bool) (string, error) {
ret := _m.Called(userID, service, authData, email, resetMfa)
var r0 string
if rf, ok := ret.Get(0).(func(string, string, *string, string, bool) string); ok {
r0 = rf(userID, service, authData, email, resetMfa)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *string, string, bool) error); ok {
r1 = rf(userID, service, authData, email, resetMfa)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateFailedPasswordAttempts provides a mock function with given fields: userID, attempts
func (_m *UserStore) UpdateFailedPasswordAttempts(userID string, attempts int) error {
ret := _m.Called(userID, attempts)
var r0 error
if rf, ok := ret.Get(0).(func(string, int) error); ok {
r0 = rf(userID, attempts)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateLastPictureUpdate provides a mock function with given fields: userID
func (_m *UserStore) UpdateLastPictureUpdate(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateMfaActive provides a mock function with given fields: userID, active
func (_m *UserStore) UpdateMfaActive(userID string, active bool) error {
ret := _m.Called(userID, active)
var r0 error
if rf, ok := ret.Get(0).(func(string, bool) error); ok {
r0 = rf(userID, active)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateMfaSecret provides a mock function with given fields: userID, secret
func (_m *UserStore) UpdateMfaSecret(userID string, secret string) error {
ret := _m.Called(userID, secret)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(userID, secret)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateNotifyProps provides a mock function with given fields: userID, props
func (_m *UserStore) UpdateNotifyProps(userID string, props map[string]string) error {
ret := _m.Called(userID, props)
var r0 error
if rf, ok := ret.Get(0).(func(string, map[string]string) error); ok {
r0 = rf(userID, props)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdatePassword provides a mock function with given fields: userID, newPassword
func (_m *UserStore) UpdatePassword(userID string, newPassword string) error {
ret := _m.Called(userID, newPassword)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(userID, newPassword)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateUpdateAt provides a mock function with given fields: userID
func (_m *UserStore) UpdateUpdateAt(userID string) (int64, error) {
ret := _m.Called(userID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(userID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// VerifyEmail provides a mock function with given fields: userID, email
func (_m *UserStore) VerifyEmail(userID string, email string) (string, error) {
ret := _m.Called(userID, email)
var r0 string
if rf, ok := ret.Get(0).(func(string, string) string); ok {
r0 = rf(userID, email)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, email)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// UserTermsOfServiceStore is an autogenerated mock type for the UserTermsOfServiceStore type
type UserTermsOfServiceStore struct {
mock.Mock
}
// Delete provides a mock function with given fields: userID, termsOfServiceId
func (_m *UserTermsOfServiceStore) Delete(userID string, termsOfServiceId string) error {
ret := _m.Called(userID, termsOfServiceId)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(userID, termsOfServiceId)
} else {
r0 = ret.Error(0)
}
return r0
}
// GetByUser provides a mock function with given fields: userID
func (_m *UserTermsOfServiceStore) GetByUser(userID string) (*model.UserTermsOfService, error) {
ret := _m.Called(userID)
var r0 *model.UserTermsOfService
if rf, ok := ret.Get(0).(func(string) *model.UserTermsOfService); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.UserTermsOfService)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: userTermsOfService
func (_m *UserTermsOfServiceStore) Save(userTermsOfService *model.UserTermsOfService) (*model.UserTermsOfService, error) {
ret := _m.Called(userTermsOfService)
var r0 *model.UserTermsOfService
if rf, ok := ret.Get(0).(func(*model.UserTermsOfService) *model.UserTermsOfService); ok {
r0 = rf(userTermsOfService)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.UserTermsOfService)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.UserTermsOfService) error); ok {
r1 = rf(userTermsOfService)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// WebhookStore is an autogenerated mock type for the WebhookStore type
type WebhookStore struct {
mock.Mock
}
// AnalyticsIncomingCount provides a mock function with given fields: teamID
func (_m *WebhookStore) AnalyticsIncomingCount(teamID string) (int64, error) {
ret := _m.Called(teamID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(teamID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AnalyticsOutgoingCount provides a mock function with given fields: teamID
func (_m *WebhookStore) AnalyticsOutgoingCount(teamID string) (int64, error) {
ret := _m.Called(teamID)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(teamID)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(teamID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ClearCaches provides a mock function with given fields:
func (_m *WebhookStore) ClearCaches() {
_m.Called()
}
// DeleteIncoming provides a mock function with given fields: webhookID, timestamp
func (_m *WebhookStore) DeleteIncoming(webhookID string, timestamp int64) error {
ret := _m.Called(webhookID, timestamp)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64) error); ok {
r0 = rf(webhookID, timestamp)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteOutgoing provides a mock function with given fields: webhookID, timestamp
func (_m *WebhookStore) DeleteOutgoing(webhookID string, timestamp int64) error {
ret := _m.Called(webhookID, timestamp)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64) error); ok {
r0 = rf(webhookID, timestamp)
} else {
r0 = ret.Error(0)
}
return r0
}
// GetIncoming provides a mock function with given fields: id, allowFromCache
func (_m *WebhookStore) GetIncoming(id string, allowFromCache bool) (*model.IncomingWebhook, error) {
ret := _m.Called(id, allowFromCache)
var r0 *model.IncomingWebhook
if rf, ok := ret.Get(0).(func(string, bool) *model.IncomingWebhook); ok {
r0 = rf(id, allowFromCache)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.IncomingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(id, allowFromCache)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetIncomingByChannel provides a mock function with given fields: channelID
func (_m *WebhookStore) GetIncomingByChannel(channelID string) ([]*model.IncomingWebhook, error) {
ret := _m.Called(channelID)
var r0 []*model.IncomingWebhook
if rf, ok := ret.Get(0).(func(string) []*model.IncomingWebhook); ok {
r0 = rf(channelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.IncomingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(channelID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetIncomingByTeam provides a mock function with given fields: teamID, offset, limit
func (_m *WebhookStore) GetIncomingByTeam(teamID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
ret := _m.Called(teamID, offset, limit)
var r0 []*model.IncomingWebhook
if rf, ok := ret.Get(0).(func(string, int, int) []*model.IncomingWebhook); ok {
r0 = rf(teamID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.IncomingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(teamID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetIncomingByTeamByUser provides a mock function with given fields: teamID, userID, offset, limit
func (_m *WebhookStore) GetIncomingByTeamByUser(teamID string, userID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
ret := _m.Called(teamID, userID, offset, limit)
var r0 []*model.IncomingWebhook
if rf, ok := ret.Get(0).(func(string, string, int, int) []*model.IncomingWebhook); ok {
r0 = rf(teamID, userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.IncomingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int, int) error); ok {
r1 = rf(teamID, userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetIncomingList provides a mock function with given fields: offset, limit
func (_m *WebhookStore) GetIncomingList(offset int, limit int) ([]*model.IncomingWebhook, error) {
ret := _m.Called(offset, limit)
var r0 []*model.IncomingWebhook
if rf, ok := ret.Get(0).(func(int, int) []*model.IncomingWebhook); ok {
r0 = rf(offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.IncomingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int) error); ok {
r1 = rf(offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetIncomingListByUser provides a mock function with given fields: userID, offset, limit
func (_m *WebhookStore) GetIncomingListByUser(userID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
ret := _m.Called(userID, offset, limit)
var r0 []*model.IncomingWebhook
if rf, ok := ret.Get(0).(func(string, int, int) []*model.IncomingWebhook); ok {
r0 = rf(userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.IncomingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetOutgoing provides a mock function with given fields: id
func (_m *WebhookStore) GetOutgoing(id string) (*model.OutgoingWebhook, error) {
ret := _m.Called(id)
var r0 *model.OutgoingWebhook
if rf, ok := ret.Get(0).(func(string) *model.OutgoingWebhook); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OutgoingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetOutgoingByChannel provides a mock function with given fields: channelID, offset, limit
func (_m *WebhookStore) GetOutgoingByChannel(channelID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
ret := _m.Called(channelID, offset, limit)
var r0 []*model.OutgoingWebhook
if rf, ok := ret.Get(0).(func(string, int, int) []*model.OutgoingWebhook); ok {
r0 = rf(channelID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.OutgoingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(channelID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetOutgoingByChannelByUser provides a mock function with given fields: channelID, userID, offset, limit
func (_m *WebhookStore) GetOutgoingByChannelByUser(channelID string, userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
ret := _m.Called(channelID, userID, offset, limit)
var r0 []*model.OutgoingWebhook
if rf, ok := ret.Get(0).(func(string, string, int, int) []*model.OutgoingWebhook); ok {
r0 = rf(channelID, userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.OutgoingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int, int) error); ok {
r1 = rf(channelID, userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetOutgoingByTeam provides a mock function with given fields: teamID, offset, limit
func (_m *WebhookStore) GetOutgoingByTeam(teamID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
ret := _m.Called(teamID, offset, limit)
var r0 []*model.OutgoingWebhook
if rf, ok := ret.Get(0).(func(string, int, int) []*model.OutgoingWebhook); ok {
r0 = rf(teamID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.OutgoingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(teamID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetOutgoingByTeamByUser provides a mock function with given fields: teamID, userID, offset, limit
func (_m *WebhookStore) GetOutgoingByTeamByUser(teamID string, userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
ret := _m.Called(teamID, userID, offset, limit)
var r0 []*model.OutgoingWebhook
if rf, ok := ret.Get(0).(func(string, string, int, int) []*model.OutgoingWebhook); ok {
r0 = rf(teamID, userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.OutgoingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int, int) error); ok {
r1 = rf(teamID, userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetOutgoingList provides a mock function with given fields: offset, limit
func (_m *WebhookStore) GetOutgoingList(offset int, limit int) ([]*model.OutgoingWebhook, error) {
ret := _m.Called(offset, limit)
var r0 []*model.OutgoingWebhook
if rf, ok := ret.Get(0).(func(int, int) []*model.OutgoingWebhook); ok {
r0 = rf(offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.OutgoingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int, int) error); ok {
r1 = rf(offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetOutgoingListByUser provides a mock function with given fields: userID, offset, limit
func (_m *WebhookStore) GetOutgoingListByUser(userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
ret := _m.Called(userID, offset, limit)
var r0 []*model.OutgoingWebhook
if rf, ok := ret.Get(0).(func(string, int, int) []*model.OutgoingWebhook); ok {
r0 = rf(userID, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.OutgoingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
r1 = rf(userID, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// InvalidateWebhookCache provides a mock function with given fields: webhook
func (_m *WebhookStore) InvalidateWebhookCache(webhook string) {
_m.Called(webhook)
}
// PermanentDeleteIncomingByChannel provides a mock function with given fields: channelID
func (_m *WebhookStore) PermanentDeleteIncomingByChannel(channelID string) error {
ret := _m.Called(channelID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(channelID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteIncomingByUser provides a mock function with given fields: userID
func (_m *WebhookStore) PermanentDeleteIncomingByUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteOutgoingByChannel provides a mock function with given fields: channelID
func (_m *WebhookStore) PermanentDeleteOutgoingByChannel(channelID string) error {
ret := _m.Called(channelID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(channelID)
} else {
r0 = ret.Error(0)
}
return r0
}
// PermanentDeleteOutgoingByUser provides a mock function with given fields: userID
func (_m *WebhookStore) PermanentDeleteOutgoingByUser(userID string) error {
ret := _m.Called(userID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// SaveIncoming provides a mock function with given fields: webhook
func (_m *WebhookStore) SaveIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error) {
ret := _m.Called(webhook)
var r0 *model.IncomingWebhook
if rf, ok := ret.Get(0).(func(*model.IncomingWebhook) *model.IncomingWebhook); ok {
r0 = rf(webhook)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.IncomingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.IncomingWebhook) error); ok {
r1 = rf(webhook)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveOutgoing provides a mock function with given fields: webhook
func (_m *WebhookStore) SaveOutgoing(webhook *model.OutgoingWebhook) (*model.OutgoingWebhook, error) {
ret := _m.Called(webhook)
var r0 *model.OutgoingWebhook
if rf, ok := ret.Get(0).(func(*model.OutgoingWebhook) *model.OutgoingWebhook); ok {
r0 = rf(webhook)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OutgoingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.OutgoingWebhook) error); ok {
r1 = rf(webhook)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateIncoming provides a mock function with given fields: webhook
func (_m *WebhookStore) UpdateIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error) {
ret := _m.Called(webhook)
var r0 *model.IncomingWebhook
if rf, ok := ret.Get(0).(func(*model.IncomingWebhook) *model.IncomingWebhook); ok {
r0 = rf(webhook)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.IncomingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.IncomingWebhook) error); ok {
r1 = rf(webhook)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateOutgoing provides a mock function with given fields: hook
func (_m *WebhookStore) UpdateOutgoing(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, error) {
ret := _m.Called(hook)
var r0 *model.OutgoingWebhook
if rf, ok := ret.Get(0).(func(*model.OutgoingWebhook) *model.OutgoingWebhook); ok {
r0 = rf(hook)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.OutgoingWebhook)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.OutgoingWebhook) error); ok {
r1 = rf(hook)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"database/sql"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const PluginIdJenkins = "jenkins"
func TestNotifyAdminStore(t *testing.T, ss store.Store) {
t.Run("Save", func(t *testing.T) { testNotifyAdminStoreSave(t, ss) })
t.Run("testGetDataByUserIdAndFeature", func(t *testing.T) { testGetDataByUserIdAndFeature(t, ss) })
t.Run("testGet", func(t *testing.T) { testGet(t, ss) })
t.Run("testDeleteBefore", func(t *testing.T) { testDeleteBefore(t, ss) })
t.Run("testUpdate", func(t *testing.T) { testUpdate(t, ss) })
}
func tearDown(t *testing.T, ss store.Store) {
err := ss.NotifyAdmin().DeleteBefore(true, model.GetMillis()+model.GetMillis())
require.NoError(t, err)
err = ss.NotifyAdmin().DeleteBefore(false, model.GetMillis()+model.GetMillis())
require.NoError(t, err)
}
func testNotifyAdminStoreSave(t *testing.T, ss store.Store) {
d1 := &model.NotifyAdminData{
UserId: model.NewId(),
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
}
_, err := ss.NotifyAdmin().Save(d1)
require.NoError(t, err)
// unknow plan error
d2 := &model.NotifyAdminData{
UserId: model.NewId(),
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: "Unknown feature",
}
_, err = ss.NotifyAdmin().Save(d2)
require.Error(t, err)
// unknown feature error
d3 := &model.NotifyAdminData{
UserId: model.NewId(),
RequiredPlan: "Unknown plan",
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
}
_, err = ss.NotifyAdmin().Save(d3)
require.Error(t, err)
// same user requesting same feature error
singleUserId := model.NewId()
d5 := &model.NotifyAdminData{
UserId: singleUserId,
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
}
_, err = ss.NotifyAdmin().Save(d5)
require.NoError(t, err)
d6 := &model.NotifyAdminData{
UserId: singleUserId,
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
}
_, err = ss.NotifyAdmin().Save(d6)
require.Error(t, err)
tearDown(t, ss)
}
func testGet(t *testing.T, ss store.Store) {
userId1 := model.NewId()
d1 := &model.NotifyAdminData{
UserId: userId1,
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
}
_, err := ss.NotifyAdmin().Save(d1)
require.NoError(t, err)
d1Trial := &model.NotifyAdminData{
UserId: userId1,
RequiredPlan: model.LicenseShortSkuEnterprise,
RequiredFeature: model.PaidFeatureAllEnterprisefeatures,
Trial: true,
}
_, err = ss.NotifyAdmin().Save(d1Trial)
require.NoError(t, err)
d1Trial2 := &model.NotifyAdminData{
UserId: model.NewId(),
RequiredPlan: model.LicenseShortSkuEnterprise,
RequiredFeature: model.PaidFeatureAllEnterprisefeatures,
Trial: true,
}
_, err = ss.NotifyAdmin().Save(d1Trial2)
require.NoError(t, err)
upgradeRequests, err := ss.NotifyAdmin().Get(false)
require.NoError(t, err)
require.Equal(t, len(upgradeRequests), 1)
trialRequests, err := ss.NotifyAdmin().Get(true)
require.NoError(t, err)
require.Equal(t, len(trialRequests), 2)
tearDown(t, ss)
}
func testGetDataByUserIdAndFeature(t *testing.T, ss store.Store) {
userId1 := model.NewId()
d1 := &model.NotifyAdminData{
UserId: userId1,
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
}
_, err := ss.NotifyAdmin().Save(d1)
require.NoError(t, err)
userId2 := model.NewId()
d2 := &model.NotifyAdminData{
UserId: userId2,
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureCustomUsergroups,
}
_, err = ss.NotifyAdmin().Save(d2)
require.NoError(t, err)
user1Request, err := ss.NotifyAdmin().GetDataByUserIdAndFeature(userId1, model.PaidFeatureAllProfessionalfeatures)
require.NoError(t, err)
require.Equal(t, len(user1Request), 1)
require.Equal(t, user1Request[0].RequiredFeature, model.PaidFeatureAllProfessionalfeatures)
tearDown(t, ss)
}
func testUpdate(t *testing.T, ss store.Store) {
userId1 := model.NewId()
d1 := &model.NotifyAdminData{
UserId: userId1,
RequiredPlan: PluginIdJenkins,
RequiredFeature: model.PluginFeature,
}
_, err := ss.NotifyAdmin().Save(d1)
require.NoError(t, err)
err = ss.NotifyAdmin().Update(d1.UserId, d1.RequiredPlan, d1.RequiredFeature, 100)
require.NoError(t, err)
userRequest, err := ss.NotifyAdmin().GetDataByUserIdAndFeature(d1.UserId, d1.RequiredFeature)
require.NoError(t, err)
require.Equal(t, len(userRequest), 1)
require.Equal(t, userRequest[0].SentAt, sql.NullInt64{Int64: 100, Valid: true})
tearDown(t, ss)
}
func testDeleteBefore(t *testing.T, ss store.Store) {
userId1 := model.NewId()
d1 := &model.NotifyAdminData{
UserId: userId1,
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
}
_, err := ss.NotifyAdmin().Save(d1)
require.NoError(t, err)
d1Trial := &model.NotifyAdminData{
UserId: userId1,
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureAllEnterprisefeatures,
Trial: true,
}
_, err = ss.NotifyAdmin().Save(d1Trial)
require.NoError(t, err)
d1Trial2 := &model.NotifyAdminData{
UserId: model.NewId(),
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureAllEnterprisefeatures,
Trial: true,
}
_, err = ss.NotifyAdmin().Save(d1Trial2)
require.NoError(t, err)
err = ss.NotifyAdmin().DeleteBefore(false, model.GetMillis()+model.GetMillis()) // delete all upgrade requests
require.NoError(t, err)
upgradeRequests, err := ss.NotifyAdmin().Get(false)
require.NoError(t, err)
require.Equal(t, len(upgradeRequests), 0)
trialRequests, err := ss.NotifyAdmin().Get(true)
require.NoError(t, err)
require.Equal(t, len(trialRequests), 2) // trial requests should still exist
err = ss.NotifyAdmin().DeleteBefore(true, model.GetMillis()+model.GetMillis()) // delete all trial requests
require.NoError(t, err)
trialRequests, err = ss.NotifyAdmin().Get(false)
require.NoError(t, err)
require.Equal(t, len(trialRequests), 0)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestOAuthStore(t *testing.T, ss store.Store) {
t.Run("SaveApp", func(t *testing.T) { testOAuthStoreSaveApp(t, ss) })
t.Run("GetApp", func(t *testing.T) { testOAuthStoreGetApp(t, ss) })
t.Run("UpdateApp", func(t *testing.T) { testOAuthStoreUpdateApp(t, ss) })
t.Run("SaveAccessData", func(t *testing.T) { testOAuthStoreSaveAccessData(t, ss) })
t.Run("OAuthUpdateAccessData", func(t *testing.T) { testOAuthUpdateAccessData(t, ss) })
t.Run("GetAccessData", func(t *testing.T) { testOAuthStoreGetAccessData(t, ss) })
t.Run("RemoveAccessData", func(t *testing.T) { testOAuthStoreRemoveAccessData(t, ss) })
t.Run("RemoveAllAccessData", func(t *testing.T) { testOAuthStoreRemoveAllAccessData(t, ss) })
t.Run("SaveAuthData", func(t *testing.T) { testOAuthStoreSaveAuthData(t, ss) })
t.Run("GetAuthData", func(t *testing.T) { testOAuthStoreGetAuthData(t, ss) })
t.Run("RemoveAuthData", func(t *testing.T) { testOAuthStoreRemoveAuthData(t, ss) })
t.Run("RemoveAuthDataByUser", func(t *testing.T) { testOAuthStoreRemoveAuthDataByUser(t, ss) })
t.Run("OAuthGetAuthorizedApps", func(t *testing.T) { testOAuthGetAuthorizedApps(t, ss) })
t.Run("OAuthGetAccessDataByUserForApp", func(t *testing.T) { testOAuthGetAccessDataByUserForApp(t, ss) })
t.Run("DeleteApp", func(t *testing.T) { testOAuthStoreDeleteApp(t, ss) })
}
func testOAuthStoreSaveApp(t *testing.T, ss store.Store) {
a1 := model.OAuthApp{}
a1.CreatorId = model.NewId()
a1.CallbackUrls = []string{"https://nowhere.com"}
a1.Homepage = "https://nowhere.com"
// Try to save an app that already has an Id
a1.Id = model.NewId()
_, err := ss.OAuth().SaveApp(&a1)
require.Error(t, err, "Should have failed, cannot add an OAuth app cannot be save with an Id, it has to be updated")
// Try to save an Invalid App
a1.Id = ""
_, err = ss.OAuth().SaveApp(&a1)
require.Error(t, err, "Should have failed, app should be invalid cause it doesn' have a name set")
a1.Name = "TestApp" + model.NewId() // Valid name
a1.MattermostAppID = "a very, very, very, very, very, very, very long id"
_, err = ss.OAuth().SaveApp(&a1)
require.Error(t, err, "Should have failed, app should be invalid cause the MattermostAppID is to long")
// Save the app
a1.Id = ""
a1.MattermostAppID = "some small id" // Valid id
_, err = ss.OAuth().SaveApp(&a1)
require.NoError(t, err)
}
func testOAuthStoreGetApp(t *testing.T, ss store.Store) {
a1 := model.OAuthApp{}
a1.CreatorId = model.NewId()
a1.Name = "TestApp" + model.NewId()
a1.CallbackUrls = []string{"https://nowhere.com"}
a1.Homepage = "https://nowhere.com"
_, err := ss.OAuth().SaveApp(&a1)
require.NoError(t, err)
// Lets try to get and app that does not exists
_, err = ss.OAuth().GetApp("fake0123456789abcderfgret1")
require.Error(t, err, "Should have failed. App does not exists")
_, err = ss.OAuth().GetApp(a1.Id)
require.NoError(t, err)
// Lets try and get the app from a user that hasn't created any apps
apps, err := ss.OAuth().GetAppByUser("fake0123456789abcderfgret1", 0, 1000)
require.NoError(t, err)
assert.Empty(t, apps, "Should have failed. Fake user hasn't created any apps")
_, err = ss.OAuth().GetAppByUser(a1.CreatorId, 0, 1000)
require.NoError(t, err)
_, err = ss.OAuth().GetApps(0, 1000)
require.NoError(t, err)
}
func testOAuthStoreUpdateApp(t *testing.T, ss store.Store) {
a1 := model.OAuthApp{}
a1.CreatorId = model.NewId()
a1.Name = "TestApp" + model.NewId()
a1.CallbackUrls = []string{"https://nowhere.com"}
a1.Homepage = "https://nowhere.com"
_, err := ss.OAuth().SaveApp(&a1)
require.NoError(t, err)
// temporarily save the created app id
id := a1.Id
a1.CreateAt = 1
a1.ClientSecret = "pwd"
a1.CreatorId = "12345678901234567890123456"
// Lets update the app by removing the name
a1.Name = ""
_, err = ss.OAuth().UpdateApp(&a1)
require.Error(t, err, "Should have failed. App name is not set")
// Lets not find the app that we are trying to update
a1.Id = "fake0123456789abcderfgret1"
a1.Name = "NewName"
_, err = ss.OAuth().UpdateApp(&a1)
require.Error(t, err, "Should have failed. Not able to find the app")
a1.Id = id
ua, err := ss.OAuth().UpdateApp(&a1)
require.NoError(t, err)
require.Equal(t, ua.Name, "NewName", "name did not update")
require.NotEqual(t, ua.CreateAt, 1, "create at should not have updated")
require.NotEqual(t, ua.CreatorId, "12345678901234567890123456", "creator id should not have updated")
}
func testOAuthStoreSaveAccessData(t *testing.T, ss store.Store) {
a1 := model.AccessData{}
a1.ClientId = model.NewId()
a1.UserId = model.NewId()
// Lets try and save an incomplete access data
_, err := ss.OAuth().SaveAccessData(&a1)
require.Error(t, err, "Should have failed. Access data needs the token")
a1.Token = model.NewId()
a1.RefreshToken = model.NewId()
a1.RedirectUri = "http://example.com"
_, err = ss.OAuth().SaveAccessData(&a1)
require.NoError(t, err)
}
func testOAuthUpdateAccessData(t *testing.T, ss store.Store) {
a1 := model.AccessData{}
a1.ClientId = model.NewId()
a1.UserId = model.NewId()
a1.Token = model.NewId()
a1.RefreshToken = model.NewId()
a1.ExpiresAt = model.GetMillis()
a1.RedirectUri = "http://example.com"
_, err := ss.OAuth().SaveAccessData(&a1)
require.NoError(t, err)
//Try to update to invalid Refresh Token
refreshToken := a1.RefreshToken
a1.RefreshToken = model.NewId() + "123"
_, err = ss.OAuth().UpdateAccessData(&a1)
require.Error(t, err, "Should have failed with invalid token")
//Try to update to invalid RedirectUri
a1.RefreshToken = model.NewId()
a1.RedirectUri = ""
_, err = ss.OAuth().UpdateAccessData(&a1)
require.Error(t, err, "Should have failed with invalid Redirect URI")
// Should update fine
a1.RedirectUri = "http://example.com"
ra1, err := ss.OAuth().UpdateAccessData(&a1)
require.NoError(t, err)
require.NotEqual(t, ra1.RefreshToken, refreshToken, "refresh tokens didn't match")
}
func testOAuthStoreGetAccessData(t *testing.T, ss store.Store) {
a1 := model.AccessData{}
a1.ClientId = model.NewId()
a1.UserId = model.NewId()
a1.Token = model.NewId()
a1.RefreshToken = model.NewId()
a1.ExpiresAt = model.GetMillis()
a1.RedirectUri = "http://example.com"
_, err := ss.OAuth().SaveAccessData(&a1)
require.NoError(t, err)
_, err = ss.OAuth().GetAccessData("invalidToken")
require.Error(t, err, "Should have failed. There is no data with an invalid token")
ra1, err := ss.OAuth().GetAccessData(a1.Token)
require.NoError(t, err)
assert.Equal(t, a1.Token, ra1.Token, "tokens didn't match")
_, err = ss.OAuth().GetPreviousAccessData(a1.UserId, a1.ClientId)
require.NoError(t, err)
_, err = ss.OAuth().GetPreviousAccessData("user", "junk")
require.NoError(t, err)
// Try to get the Access data using an invalid refresh token
_, err = ss.OAuth().GetAccessDataByRefreshToken(a1.Token)
require.Error(t, err, "Should have failed. There is no data with an invalid token")
// Get the Access Data using the refresh token
ra1, err = ss.OAuth().GetAccessDataByRefreshToken(a1.RefreshToken)
require.NoError(t, err)
assert.Equal(t, a1.RefreshToken, ra1.RefreshToken, "tokens didn't match")
}
func testOAuthStoreRemoveAccessData(t *testing.T, ss store.Store) {
a1 := model.AccessData{}
a1.ClientId = model.NewId()
a1.UserId = model.NewId()
a1.Token = model.NewId()
a1.RefreshToken = model.NewId()
a1.RedirectUri = "http://example.com"
_, err := ss.OAuth().SaveAccessData(&a1)
require.NoError(t, err)
err = ss.OAuth().RemoveAccessData(a1.Token)
require.NoError(t, err)
result, _ := ss.OAuth().GetPreviousAccessData(a1.UserId, a1.ClientId)
require.Nil(t, result, "did not delete access token")
}
func testOAuthStoreRemoveAllAccessData(t *testing.T, ss store.Store) {
a1 := model.AccessData{}
a1.ClientId = model.NewId()
a1.UserId = model.NewId()
a1.Token = model.NewId()
a1.RefreshToken = model.NewId()
a1.RedirectUri = "http://example.com"
_, err := ss.OAuth().SaveAccessData(&a1)
require.NoError(t, err)
err = ss.OAuth().RemoveAllAccessData()
require.NoError(t, err)
result, _ := ss.OAuth().GetPreviousAccessData(a1.UserId, a1.ClientId)
require.Nil(t, result, "did not delete access token")
}
func testOAuthStoreSaveAuthData(t *testing.T, ss store.Store) {
a1 := model.AuthData{}
a1.ClientId = model.NewId()
a1.UserId = model.NewId()
a1.Code = model.NewId()
a1.RedirectUri = "http://example.com"
_, err := ss.OAuth().SaveAuthData(&a1)
require.NoError(t, err)
}
func testOAuthStoreGetAuthData(t *testing.T, ss store.Store) {
a1 := model.AuthData{}
a1.ClientId = model.NewId()
a1.UserId = model.NewId()
a1.Code = model.NewId()
a1.RedirectUri = "http://example.com"
_, err := ss.OAuth().SaveAuthData(&a1)
require.NoError(t, err)
_, err = ss.OAuth().GetAuthData(a1.Code)
require.NoError(t, err)
}
func testOAuthStoreRemoveAuthData(t *testing.T, ss store.Store) {
a1 := model.AuthData{}
a1.ClientId = model.NewId()
a1.UserId = model.NewId()
a1.Code = model.NewId()
a1.RedirectUri = "http://example.com"
_, err := ss.OAuth().SaveAuthData(&a1)
require.NoError(t, err)
err = ss.OAuth().RemoveAuthData(a1.Code)
require.NoError(t, err)
_, err = ss.OAuth().GetAuthData(a1.Code)
require.Error(t, err, "should have errored - auth code removed")
}
func testOAuthStoreRemoveAuthDataByUser(t *testing.T, ss store.Store) {
a1 := model.AuthData{}
a1.ClientId = model.NewId()
a1.UserId = model.NewId()
a1.Code = model.NewId()
a1.RedirectUri = "http://example.com"
_, err := ss.OAuth().SaveAuthData(&a1)
require.NoError(t, err)
err = ss.OAuth().PermanentDeleteAuthDataByUser(a1.UserId)
require.NoError(t, err)
}
func testOAuthGetAuthorizedApps(t *testing.T, ss store.Store) {
a1 := model.OAuthApp{}
a1.CreatorId = model.NewId()
a1.Name = "TestApp" + model.NewId()
a1.CallbackUrls = []string{"https://nowhere.com"}
a1.Homepage = "https://nowhere.com"
_, err := ss.OAuth().SaveApp(&a1)
require.NoError(t, err)
// Lets try and get an Authorized app for a user who hasn't authorized it
apps, err := ss.OAuth().GetAuthorizedApps("fake0123456789abcderfgret1", 0, 1000)
require.NoError(t, err)
assert.Empty(t, apps, "Should have failed. Fake user hasn't authorized the app")
// allow the app
p := model.Preference{}
p.UserId = a1.CreatorId
p.Category = model.PreferenceCategoryAuthorizedOAuthApp
p.Name = a1.Id
p.Value = "true"
nErr := ss.Preference().Save(model.Preferences{p})
require.NoError(t, nErr)
apps, err = ss.OAuth().GetAuthorizedApps(a1.CreatorId, 0, 1000)
require.NoError(t, err)
assert.NotEqual(t, len(apps), 0, "It should have return apps")
}
func testOAuthGetAccessDataByUserForApp(t *testing.T, ss store.Store) {
a1 := model.OAuthApp{}
a1.CreatorId = model.NewId()
a1.Name = "TestApp" + model.NewId()
a1.CallbackUrls = []string{"https://nowhere.com"}
a1.Homepage = "https://nowhere.com"
_, err := ss.OAuth().SaveApp(&a1)
require.NoError(t, err)
// allow the app
p := model.Preference{}
p.UserId = a1.CreatorId
p.Category = model.PreferenceCategoryAuthorizedOAuthApp
p.Name = a1.Id
p.Value = "true"
nErr := ss.Preference().Save(model.Preferences{p})
require.NoError(t, nErr)
apps, err := ss.OAuth().GetAuthorizedApps(a1.CreatorId, 0, 1000)
require.NoError(t, err)
assert.NotEqual(t, len(apps), 0, "It should have return apps")
// save the token
ad1 := model.AccessData{}
ad1.ClientId = a1.Id
ad1.UserId = a1.CreatorId
ad1.Token = model.NewId()
ad1.RefreshToken = model.NewId()
ad1.RedirectUri = "http://example.com"
_, err = ss.OAuth().SaveAccessData(&ad1)
require.NoError(t, err)
accessData, err := ss.OAuth().GetAccessDataByUserForApp(a1.CreatorId, a1.Id)
require.NoError(t, err)
assert.NotEqual(t, len(accessData), 0, "It should have return access data")
}
func testOAuthStoreDeleteApp(t *testing.T, ss store.Store) {
a1 := model.OAuthApp{}
a1.CreatorId = model.NewId()
a1.Name = "TestApp" + model.NewId()
a1.CallbackUrls = []string{"https://nowhere.com"}
a1.Homepage = "https://nowhere.com"
_, err := ss.OAuth().SaveApp(&a1)
require.NoError(t, err)
// delete a non-existent app
err = ss.OAuth().DeleteApp("fakeclientId")
require.NoError(t, err)
s1 := &model.Session{}
s1.UserId = model.NewId()
s1.Token = model.NewId()
s1.IsOAuth = true
s1, nErr := ss.Session().Save(s1)
require.NoError(t, nErr)
ad1 := model.AccessData{}
ad1.ClientId = a1.Id
ad1.UserId = a1.CreatorId
ad1.Token = s1.Token
ad1.RefreshToken = model.NewId()
ad1.RedirectUri = "http://example.com"
_, err = ss.OAuth().SaveAccessData(&ad1)
require.NoError(t, err)
err = ss.OAuth().DeleteApp(a1.Id)
require.NoError(t, err)
_, nErr = ss.Session().Get(context.Background(), s1.Token)
require.Error(t, nErr, "should error - session should be deleted")
_, err = ss.OAuth().GetAccessData(s1.Token)
require.Error(t, err, "should error - access data should be deleted")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestPluginStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("SaveOrUpdate", func(t *testing.T) { testPluginSaveOrUpdate(t, ss) })
t.Run("CompareAndSet", func(t *testing.T) { testPluginCompareAndSet(t, ss) })
t.Run("CompareAndDelete", func(t *testing.T) { testPluginCompareAndDelete(t, ss) })
t.Run("SetWithOptions", func(t *testing.T) { testPluginSetWithOptions(t, ss) })
t.Run("Get", func(t *testing.T) { testPluginGet(t, ss) })
t.Run("Delete", func(t *testing.T) { testPluginDelete(t, ss) })
t.Run("DeleteAllForPlugin", func(t *testing.T) { testPluginDeleteAllForPlugin(t, ss) })
t.Run("DeleteAllExpired", func(t *testing.T) { testPluginDeleteAllExpired(t, ss) })
t.Run("List", func(t *testing.T) { testPluginList(t, ss) })
}
func setupKVs(t *testing.T, ss store.Store) (string, func()) {
pluginId := model.NewId()
otherPluginId := model.NewId()
// otherKV is another key value for the current plugin, and used to verify other keys
// aren't modified unintentionally.
otherKV := &model.PluginKeyValue{
PluginId: pluginId,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(otherKV)
require.NoError(t, err)
// otherPluginKV is a key value for another plugin, and used to verify other plugins' keys
// aren't modified unintentionally.
otherPluginKV := &model.PluginKeyValue{
PluginId: otherPluginId,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err = ss.Plugin().SaveOrUpdate(otherPluginKV)
require.NoError(t, err)
return pluginId, func() {
actualOtherKV, err := ss.Plugin().Get(otherKV.PluginId, otherKV.Key)
require.NoError(t, err, "failed to find other key value for same plugin")
assert.Equal(t, otherKV, actualOtherKV)
actualOtherPluginKV, err := ss.Plugin().Get(otherPluginKV.PluginId, otherPluginKV.Key)
require.NoError(t, err, "failed to find other key value from different plugin")
assert.Equal(t, otherPluginKV, actualOtherPluginKV)
}
}
func doTestPluginSaveOrUpdate(t *testing.T, ss store.Store, doer func(kv *model.PluginKeyValue) (*model.PluginKeyValue, error)) {
t.Run("invalid kv", func(t *testing.T) {
_, tearDown := setupKVs(t, ss)
defer tearDown()
kv := &model.PluginKeyValue{
PluginId: "",
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
kv, err := doer(kv)
require.Error(t, err)
appErr, ok := err.(*model.AppError)
require.True(t, ok)
require.Equal(t, "model.plugin_key_value.is_valid.plugin_id.app_error", appErr.Id)
assert.Nil(t, kv)
})
t.Run("new key", func(t *testing.T) {
pluginId, tearDown := setupKVs(t, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
retKV, err := doer(kv)
require.NoError(t, err)
assert.Equal(t, kv, retKV)
// SaveOrUpdate returns the kv passed in, so test each field individually for
// completeness. It should probably be changed to not bother doing that.
assert.Equal(t, pluginId, kv.PluginId)
assert.Equal(t, key, kv.Key)
assert.Equal(t, []byte(value), kv.Value)
assert.Equal(t, expireAt, kv.ExpireAt)
actualKV, nErr := ss.Plugin().Get(pluginId, key)
require.NoError(t, nErr)
assert.Equal(t, kv, actualKV)
})
t.Run("nil value for new key", func(t *testing.T) {
pluginId, tearDown := setupKVs(t, ss)
defer tearDown()
key := model.NewId()
var value []byte
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: value,
ExpireAt: expireAt,
}
retKV, err := doer(kv)
require.NoError(t, err)
assert.Equal(t, kv, retKV)
// SaveOrUpdate returns the kv passed in, so test each field individually for
// completeness. It should probably be changed to not bother doing that.
assert.Equal(t, pluginId, kv.PluginId)
assert.Equal(t, key, kv.Key)
assert.Nil(t, kv.Value)
assert.Equal(t, expireAt, kv.ExpireAt)
actualKV, nErr := ss.Plugin().Get(pluginId, key)
_, ok := nErr.(*store.ErrNotFound)
require.Error(t, nErr)
assert.True(t, ok)
assert.Nil(t, actualKV)
})
t.Run("existing key", func(t *testing.T) {
pluginId, tearDown := setupKVs(t, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := doer(kv)
require.NoError(t, err)
newValue := model.NewId()
kv.Value = []byte(newValue)
retKV, err := doer(kv)
require.NoError(t, err)
assert.Equal(t, kv, retKV)
// SaveOrUpdate returns the kv passed in, so test each field individually for
// completeness. It should probably be changed to not bother doing that.
assert.Equal(t, pluginId, kv.PluginId)
assert.Equal(t, key, kv.Key)
assert.Equal(t, []byte(newValue), kv.Value)
assert.Equal(t, expireAt, kv.ExpireAt)
actualKV, nErr := ss.Plugin().Get(pluginId, key)
require.NoError(t, nErr)
assert.Equal(t, kv, actualKV)
})
t.Run("nil value for existing key", func(t *testing.T) {
pluginId, tearDown := setupKVs(t, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := doer(kv)
require.NoError(t, err)
kv.Value = nil
retKV, err := doer(kv)
require.NoError(t, err)
assert.Equal(t, kv, retKV)
// SaveOrUpdate returns the kv passed in, so test each field individually for
// completeness. It should probably be changed to not bother doing that.
assert.Equal(t, pluginId, kv.PluginId)
assert.Equal(t, key, kv.Key)
assert.Nil(t, kv.Value)
assert.Equal(t, expireAt, kv.ExpireAt)
actualKV, nErr := ss.Plugin().Get(pluginId, key)
_, ok := nErr.(*store.ErrNotFound)
require.Error(t, nErr)
assert.True(t, ok)
assert.Nil(t, actualKV)
})
}
func testPluginSaveOrUpdate(t *testing.T, ss store.Store) {
doTestPluginSaveOrUpdate(t, ss, func(kv *model.PluginKeyValue) (*model.PluginKeyValue, error) {
return ss.Plugin().SaveOrUpdate(kv)
})
}
// doTestPluginCompareAndSet exercises the CompareAndSet functionality, but abstracts the actual
// call to same to allow reuse with SetWithOptions
func doTestPluginCompareAndSet(t *testing.T, ss store.Store, compareAndSet func(kv *model.PluginKeyValue, oldValue []byte) (bool, error)) {
t.Run("invalid kv", func(t *testing.T) {
_, tearDown := setupKVs(t, ss)
defer tearDown()
kv := &model.PluginKeyValue{
PluginId: "",
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
ok, err := compareAndSet(kv, nil)
require.Error(t, err)
assert.False(t, ok)
appErr, ok := err.(*model.AppError)
require.True(t, ok)
assert.Equal(t, "model.plugin_key_value.is_valid.plugin_id.app_error", appErr.Id)
})
// assertChanged verifies that CompareAndSet successfully changes to the given value.
assertChanged := func(t *testing.T, kv *model.PluginKeyValue, oldValue []byte) {
t.Helper()
ok, err := compareAndSet(kv, oldValue)
require.NoError(t, err)
require.True(t, ok, "should have succeeded to CompareAndSet")
actualKV, nErr := ss.Plugin().Get(kv.PluginId, kv.Key)
require.NoError(t, nErr)
// When tested with KVSetWithOptions, a strict comparison can fail because that
// function accepts a relative time and makes its own call to model.GetMillis(),
// leading to off-by-one issues. All these tests are written with 15+ second
// differences, so allow for an off-by-1000ms in either direction.
require.NotNil(t, actualKV)
expiryDelta := actualKV.ExpireAt - kv.ExpireAt
if expiryDelta > -1000 && expiryDelta < 1000 {
actualKV.ExpireAt = kv.ExpireAt
}
assert.Equal(t, kv, actualKV)
}
// assertUnchanged verifies that CompareAndSet fails, leaving the existing value.
assertUnchanged := func(t *testing.T, kv, existingKV *model.PluginKeyValue, oldValue []byte) {
t.Helper()
ok, err := compareAndSet(kv, oldValue)
require.NoError(t, err)
require.False(t, ok, "should have failed to CompareAndSet")
actualKV, nErr := ss.Plugin().Get(kv.PluginId, kv.Key)
if existingKV == nil {
require.Error(t, nErr)
_, ok := nErr.(*store.ErrNotFound)
assert.True(t, ok)
assert.Nil(t, actualKV)
} else {
require.NoError(t, nErr)
assert.Equal(t, existingKV, actualKV)
}
}
// assertRemoved verifies that CompareAndSet successfully removes the given value.
assertRemoved := func(t *testing.T, kv *model.PluginKeyValue, oldValue []byte) {
t.Helper()
ok, err := compareAndSet(kv, oldValue)
require.NoError(t, err)
require.True(t, ok, "should have succeeded to CompareAndSet")
actualKV, nErr := ss.Plugin().Get(kv.PluginId, kv.Key)
_, ok = nErr.(*store.ErrNotFound)
require.Error(t, nErr)
assert.True(t, ok)
assert.Nil(t, actualKV)
}
// Non-existent keys and expired keys should behave identically.
for description, setup := range map[string]func(t *testing.T) (*model.PluginKeyValue, func()){
"non-existent key": func(t *testing.T) (*model.PluginKeyValue, func()) {
pluginId, tearDown := setupKVs(t, ss)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
return kv, tearDown
},
"expired key": func(t *testing.T) (*model.PluginKeyValue, func()) {
pluginId, tearDown := setupKVs(t, ss)
expiredKV := &model.PluginKeyValue{
PluginId: pluginId,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 1,
}
_, err := ss.Plugin().SaveOrUpdate(expiredKV)
require.NoError(t, err)
return expiredKV, tearDown
},
} {
t.Run(description, func(t *testing.T) {
t.Run("setting a nil value should fail", func(t *testing.T) {
testCases := map[string][]byte{
"given nil old value": nil,
"given non-nil old value": []byte(model.NewId()),
}
for description, oldValue := range testCases {
t.Run(description, func(t *testing.T) {
kv, tearDown := setup(t)
defer tearDown()
kv.Value = nil
assertUnchanged(t, kv, nil, oldValue)
})
}
})
t.Run("setting a non-nil value", func(t *testing.T) {
t.Run("should succeed given non-expiring, nil old value", func(t *testing.T) {
kv, tearDown := setup(t)
defer tearDown()
kv.ExpireAt = 0
assertChanged(t, kv, []byte(nil))
})
t.Run("should succeed given not-yet-expired, nil old value", func(t *testing.T) {
kv, tearDown := setup(t)
defer tearDown()
kv.ExpireAt = model.GetMillis() + 15*1000
assertChanged(t, kv, []byte(nil))
})
t.Run("should fail given expired, nil old value", func(t *testing.T) {
kv, tearDown := setup(t)
defer tearDown()
kv.ExpireAt = 1
assertRemoved(t, kv, []byte(nil))
})
t.Run("should fail given 'different' old value", func(t *testing.T) {
kv, tearDown := setup(t)
defer tearDown()
assertUnchanged(t, kv, nil, []byte(model.NewId()))
})
t.Run("should fail given 'same' old value", func(t *testing.T) {
kv, tearDown := setup(t)
defer tearDown()
assertUnchanged(t, kv, nil, kv.Value)
})
})
})
}
t.Run("existing key", func(t *testing.T) {
setup := func(t *testing.T) (*model.PluginKeyValue, func()) {
pluginId, tearDown := setupKVs(t, ss)
existingKV := &model.PluginKeyValue{
PluginId: pluginId,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(existingKV)
require.NoError(t, err)
return existingKV, tearDown
}
testCases := map[string]bool{
// CompareAndSet should succeed even if the value isn't changing.
"setting the same value": true,
"setting a different value": false,
}
for description, setToSameValue := range testCases {
makeKV := func(existingKV *model.PluginKeyValue) *model.PluginKeyValue {
kv := &model.PluginKeyValue{
PluginId: existingKV.PluginId,
Key: existingKV.Key,
ExpireAt: existingKV.ExpireAt,
}
if setToSameValue {
kv.Value = existingKV.Value
} else {
kv.Value = []byte(model.NewId())
}
return kv
}
t.Run(description, func(t *testing.T) {
t.Run("should fail", func(t *testing.T) {
testCases := map[string][]byte{
"given nil old value": nil,
"given different old value": []byte(model.NewId()),
}
for description, oldValue := range testCases {
t.Run(description, func(t *testing.T) {
existingKV, tearDown := setup(t)
defer tearDown()
kv := makeKV(existingKV)
assertUnchanged(t, kv, existingKV, oldValue)
})
}
})
t.Run("should succeed given same old value", func(t *testing.T) {
existingKV, tearDown := setup(t)
defer tearDown()
kv := makeKV(existingKV)
assertChanged(t, kv, existingKV.Value)
})
t.Run("and future expiry should succeed given same old value", func(t *testing.T) {
existingKV, tearDown := setup(t)
defer tearDown()
kv := makeKV(existingKV)
kv.ExpireAt = model.GetMillis() + 15*1000
assertChanged(t, kv, existingKV.Value)
})
t.Run("and past expiry should succeed given same old value", func(t *testing.T) {
existingKV, tearDown := setup(t)
defer tearDown()
kv := makeKV(existingKV)
kv.ExpireAt = model.GetMillis() - 15*1000
assertRemoved(t, kv, existingKV.Value)
})
})
}
t.Run("setting a nil value", func(t *testing.T) {
makeKV := func(existingKV *model.PluginKeyValue) *model.PluginKeyValue {
kv := &model.PluginKeyValue{
PluginId: existingKV.PluginId,
Key: existingKV.Key,
Value: existingKV.Value,
ExpireAt: existingKV.ExpireAt,
}
kv.Value = nil
return kv
}
t.Run("should fail", func(t *testing.T) {
testCases := map[string][]byte{
"given nil old value": nil,
"given different old value": []byte(model.NewId()),
}
for description, oldValue := range testCases {
t.Run(description, func(t *testing.T) {
existingKV, tearDown := setup(t)
defer tearDown()
kv := makeKV(existingKV)
assertUnchanged(t, kv, existingKV, oldValue)
})
}
})
t.Run("should succeed, deleting, given same old value", func(t *testing.T) {
existingKV, tearDown := setup(t)
defer tearDown()
kv := makeKV(existingKV)
assertRemoved(t, kv, existingKV.Value)
})
})
})
}
func testPluginCompareAndSet(t *testing.T, ss store.Store) {
doTestPluginCompareAndSet(t, ss, func(kv *model.PluginKeyValue, oldValue []byte) (bool, error) {
return ss.Plugin().CompareAndSet(kv, oldValue)
})
}
func testPluginCompareAndDelete(t *testing.T, ss store.Store) {
t.Run("invalid kv", func(t *testing.T) {
_, tearDown := setupKVs(t, ss)
defer tearDown()
kv := &model.PluginKeyValue{
PluginId: "",
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
ok, err := ss.Plugin().CompareAndDelete(kv, nil)
require.Error(t, err)
assert.False(t, ok)
appErr, ok := err.(*model.AppError)
require.True(t, ok)
assert.Equal(t, "model.plugin_key_value.is_valid.plugin_id.app_error", appErr.Id)
})
t.Run("non-existent key should fail", func(t *testing.T) {
pluginId, tearDown := setupKVs(t, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
testCases := map[string][]byte{
"given nil old value": nil,
"given non-nil old value": []byte(model.NewId()),
}
for description, oldValue := range testCases {
t.Run(description, func(t *testing.T) {
ok, err := ss.Plugin().CompareAndDelete(kv, oldValue)
require.NoError(t, err)
assert.False(t, ok)
})
}
})
t.Run("expired key should fail", func(t *testing.T) {
pluginId, tearDown := setupKVs(t, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := int64(1)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
testCases := map[string][]byte{
"given nil old value": nil,
"given different old value": []byte(model.NewId()),
"given same old value": []byte(value),
}
for description, oldValue := range testCases {
t.Run(description, func(t *testing.T) {
ok, err := ss.Plugin().CompareAndDelete(kv, oldValue)
require.NoError(t, err)
assert.False(t, ok)
})
}
})
t.Run("existing key should fail given different old value", func(t *testing.T) {
pluginId, tearDown := setupKVs(t, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
oldValue := []byte(model.NewId())
ok, err := ss.Plugin().CompareAndDelete(kv, oldValue)
require.NoError(t, err)
assert.False(t, ok)
})
t.Run("existing key should succeed given same old value", func(t *testing.T) {
pluginId, tearDown := setupKVs(t, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
oldValue := []byte(value)
ok, err := ss.Plugin().CompareAndDelete(kv, oldValue)
require.NoError(t, err)
assert.True(t, ok)
})
}
func testPluginSetWithOptions(t *testing.T, ss store.Store) {
t.Run("invalid options", func(t *testing.T) {
_, tearDown := setupKVs(t, ss)
defer tearDown()
pluginId := ""
key := model.NewId()
value := model.NewId()
options := model.PluginKVSetOptions{
Atomic: false,
OldValue: []byte("not-nil"),
}
ok, err := ss.Plugin().SetWithOptions(pluginId, key, []byte(value), options)
require.Error(t, err)
assert.False(t, ok)
appErr, ok := err.(*model.AppError)
require.True(t, ok)
require.Equal(t, "model.plugin_kvset_options.is_valid.old_value.app_error", appErr.Id)
})
t.Run("invalid kv", func(t *testing.T) {
_, tearDown := setupKVs(t, ss)
defer tearDown()
pluginId := ""
key := model.NewId()
value := model.NewId()
options := model.PluginKVSetOptions{}
ok, err := ss.Plugin().SetWithOptions(pluginId, key, []byte(value), options)
require.Error(t, err)
assert.False(t, ok)
appErr, ok := err.(*model.AppError)
require.True(t, ok)
require.Equal(t, "model.plugin_key_value.is_valid.plugin_id.app_error", appErr.Id)
})
t.Run("atomic", func(t *testing.T) {
doTestPluginCompareAndSet(t, ss, func(kv *model.PluginKeyValue, oldValue []byte) (bool, error) {
now := model.GetMillis()
options := model.PluginKVSetOptions{
Atomic: true,
OldValue: oldValue,
}
if kv.ExpireAt != 0 {
options.ExpireInSeconds = (kv.ExpireAt - now) / 1000
}
return ss.Plugin().SetWithOptions(kv.PluginId, kv.Key, kv.Value, options)
})
})
t.Run("non-atomic", func(t *testing.T) {
doTestPluginSaveOrUpdate(t, ss, func(kv *model.PluginKeyValue) (*model.PluginKeyValue, error) {
now := model.GetMillis()
options := model.PluginKVSetOptions{
Atomic: false,
}
if kv.ExpireAt != 0 {
options.ExpireInSeconds = (kv.ExpireAt - now) / 1000
}
ok, err := ss.Plugin().SetWithOptions(kv.PluginId, kv.Key, kv.Value, options)
if !ok {
return nil, err
}
return kv, err
})
})
}
func testPluginGet(t *testing.T, ss store.Store) {
t.Run("no matching key value", func(t *testing.T) {
pluginId := model.NewId()
key := model.NewId()
kv, nErr := ss.Plugin().Get(pluginId, key)
_, ok := nErr.(*store.ErrNotFound)
require.Error(t, nErr)
assert.True(t, ok)
assert.Nil(t, kv)
})
t.Run("no-matching key value for plugin id", func(t *testing.T) {
pluginId := model.NewId()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
kv, err = ss.Plugin().Get(model.NewId(), key)
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, kv)
})
t.Run("no-matching key value for key", func(t *testing.T) {
pluginId := model.NewId()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
kv, err = ss.Plugin().Get(pluginId, model.NewId())
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, kv)
})
t.Run("old expired key value", func(t *testing.T) {
pluginId := model.NewId()
key := model.NewId()
value := model.NewId()
expireAt := int64(1)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
kv, err = ss.Plugin().Get(pluginId, model.NewId())
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, kv)
})
t.Run("recently expired key value", func(t *testing.T) {
pluginId := model.NewId()
key := model.NewId()
value := model.NewId()
expireAt := model.GetMillis() - 15*1000
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
kv, err = ss.Plugin().Get(pluginId, model.NewId())
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, kv)
})
t.Run("matching key value, non-expiring", func(t *testing.T) {
pluginId := model.NewId()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
actualKV, err := ss.Plugin().Get(pluginId, key)
require.NoError(t, err)
require.Equal(t, kv, actualKV)
})
t.Run("matching key value, not yet expired", func(t *testing.T) {
pluginId := model.NewId()
key := model.NewId()
value := model.NewId()
expireAt := model.GetMillis() + 15*1000
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
actualKV, err := ss.Plugin().Get(pluginId, key)
require.NoError(t, err)
require.Equal(t, kv, actualKV)
})
}
func testPluginDelete(t *testing.T, ss store.Store) {
t.Run("no matching key value", func(t *testing.T) {
pluginId, tearDown := setupKVs(t, ss)
defer tearDown()
key := model.NewId()
err := ss.Plugin().Delete(pluginId, key)
require.NoError(t, err)
kv, err := ss.Plugin().Get(pluginId, key)
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, kv)
})
testCases := []struct {
description string
expireAt int64
}{
{
"expired key value",
model.GetMillis() - 15*1000,
},
{
"never expiring value",
0,
},
{
"not yet expired value",
model.GetMillis() + 15*1000,
},
}
for _, testCase := range testCases {
t.Run(testCase.description, func(t *testing.T) {
pluginId, tearDown := setupKVs(t, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := testCase.expireAt
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
err = ss.Plugin().Delete(pluginId, key)
require.NoError(t, err)
kv, err = ss.Plugin().Get(pluginId, key)
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, kv)
})
}
}
func testPluginDeleteAllForPlugin(t *testing.T, ss store.Store) {
setupKVsForDeleteAll := func(t *testing.T) (string, func()) {
pluginId := model.NewId()
otherPluginId := model.NewId()
// otherPluginKV is another key value for another plugin, and used to verify other
// keys aren't modified unintentionally.
otherPluginKV := &model.PluginKeyValue{
PluginId: otherPluginId,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(otherPluginKV)
require.NoError(t, err)
return pluginId, func() {
actualOtherPluginKV, err := ss.Plugin().Get(otherPluginKV.PluginId, otherPluginKV.Key)
require.NoError(t, err, "failed to find other key value from different plugin")
assert.Equal(t, otherPluginKV, actualOtherPluginKV)
}
}
t.Run("no keys to delete", func(t *testing.T) {
pluginId, tearDown := setupKVsForDeleteAll(t)
defer tearDown()
err := ss.Plugin().DeleteAllForPlugin(pluginId)
require.NoError(t, err)
})
t.Run("multiple keys to delete", func(t *testing.T) {
pluginId, tearDown := setupKVsForDeleteAll(t)
defer tearDown()
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
kv2 := &model.PluginKeyValue{
PluginId: pluginId,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err = ss.Plugin().SaveOrUpdate(kv2)
require.NoError(t, err)
err = ss.Plugin().DeleteAllForPlugin(pluginId)
require.NoError(t, err)
_, err = ss.Plugin().Get(kv.PluginId, kv.Key)
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
_, err = ss.Plugin().Get(kv.PluginId, kv2.Key)
_, ok = err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
})
}
func testPluginDeleteAllExpired(t *testing.T, ss store.Store) {
t.Run("no keys", func(t *testing.T) {
err := ss.Plugin().DeleteAllExpired()
require.NoError(t, err)
})
t.Run("no expiring keys to delete", func(t *testing.T) {
pluginIdA := model.NewId()
pluginIdB := model.NewId()
kvA1 := &model.PluginKeyValue{
PluginId: pluginIdA,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(kvA1)
require.NoError(t, err)
kvA2 := &model.PluginKeyValue{
PluginId: pluginIdA,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err = ss.Plugin().SaveOrUpdate(kvA2)
require.NoError(t, err)
kvB1 := &model.PluginKeyValue{
PluginId: pluginIdB,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err = ss.Plugin().SaveOrUpdate(kvB1)
require.NoError(t, err)
kvB2 := &model.PluginKeyValue{
PluginId: pluginIdB,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err = ss.Plugin().SaveOrUpdate(kvB2)
require.NoError(t, err)
err = ss.Plugin().DeleteAllExpired()
require.NoError(t, err)
actualKVA1, err := ss.Plugin().Get(pluginIdA, kvA1.Key)
require.NoError(t, err)
assert.Equal(t, kvA1, actualKVA1)
actualKVA2, err := ss.Plugin().Get(pluginIdA, kvA2.Key)
require.NoError(t, err)
assert.Equal(t, kvA2, actualKVA2)
actualKVB1, err := ss.Plugin().Get(pluginIdB, kvB1.Key)
require.NoError(t, err)
assert.Equal(t, kvB1, actualKVB1)
actualKVB2, err := ss.Plugin().Get(pluginIdB, kvB2.Key)
require.NoError(t, err)
assert.Equal(t, kvB2, actualKVB2)
})
t.Run("no expired keys to delete", func(t *testing.T) {
pluginIdA := model.NewId()
pluginIdB := model.NewId()
kvA1 := &model.PluginKeyValue{
PluginId: pluginIdA,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() + 15*1000,
}
_, err := ss.Plugin().SaveOrUpdate(kvA1)
require.NoError(t, err)
kvA2 := &model.PluginKeyValue{
PluginId: pluginIdA,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() + 15*1000,
}
_, err = ss.Plugin().SaveOrUpdate(kvA2)
require.NoError(t, err)
kvB1 := &model.PluginKeyValue{
PluginId: pluginIdB,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() + 15*1000,
}
_, err = ss.Plugin().SaveOrUpdate(kvB1)
require.NoError(t, err)
kvB2 := &model.PluginKeyValue{
PluginId: pluginIdB,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() + 15*1000,
}
_, err = ss.Plugin().SaveOrUpdate(kvB2)
require.NoError(t, err)
err = ss.Plugin().DeleteAllExpired()
require.NoError(t, err)
actualKVA1, err := ss.Plugin().Get(pluginIdA, kvA1.Key)
require.NoError(t, err)
assert.Equal(t, kvA1, actualKVA1)
actualKVA2, err := ss.Plugin().Get(pluginIdA, kvA2.Key)
require.NoError(t, err)
assert.Equal(t, kvA2, actualKVA2)
actualKVB1, err := ss.Plugin().Get(pluginIdB, kvB1.Key)
require.NoError(t, err)
assert.Equal(t, kvB1, actualKVB1)
actualKVB2, err := ss.Plugin().Get(pluginIdB, kvB2.Key)
require.NoError(t, err)
assert.Equal(t, kvB2, actualKVB2)
})
t.Run("some expired keys to delete", func(t *testing.T) {
pluginIdA := model.NewId()
pluginIdB := model.NewId()
kvA1 := &model.PluginKeyValue{
PluginId: pluginIdA,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() + 15*1000,
}
_, err := ss.Plugin().SaveOrUpdate(kvA1)
require.NoError(t, err)
expiredKVA2 := &model.PluginKeyValue{
PluginId: pluginIdA,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() - 15*1000,
}
_, err = ss.Plugin().SaveOrUpdate(expiredKVA2)
require.NoError(t, err)
kvB1 := &model.PluginKeyValue{
PluginId: pluginIdB,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() + 15*1000,
}
_, err = ss.Plugin().SaveOrUpdate(kvB1)
require.NoError(t, err)
expiredKVB2 := &model.PluginKeyValue{
PluginId: pluginIdB,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() - 15*1000,
}
_, err = ss.Plugin().SaveOrUpdate(expiredKVB2)
require.NoError(t, err)
err = ss.Plugin().DeleteAllExpired()
require.NoError(t, err)
actualKVA1, err := ss.Plugin().Get(pluginIdA, kvA1.Key)
require.NoError(t, err)
assert.Equal(t, kvA1, actualKVA1)
actualKVA2, err := ss.Plugin().Get(pluginIdA, expiredKVA2.Key)
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, actualKVA2)
actualKVB1, err := ss.Plugin().Get(pluginIdB, kvB1.Key)
require.NoError(t, err)
assert.Equal(t, kvB1, actualKVB1)
actualKVB2, err := ss.Plugin().Get(pluginIdB, expiredKVB2.Key)
_, ok = err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, actualKVB2)
})
}
func testPluginList(t *testing.T, ss store.Store) {
t.Run("no key values", func(t *testing.T) {
_, tearDown := setupKVs(t, ss)
defer tearDown()
// Ignore the pluginId setup by setupKVs
pluginId := model.NewId()
keys, err := ss.Plugin().List(pluginId, 0, 100)
require.NoError(t, err)
assert.Empty(t, keys)
})
t.Run("single key", func(t *testing.T) {
_, tearDown := setupKVs(t, ss)
defer tearDown()
// Ignore the pluginId setup by setupKVs
pluginId := model.NewId()
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
keys, err := ss.Plugin().List(pluginId, 0, 100)
require.NoError(t, err)
require.Len(t, keys, 1)
assert.Equal(t, kv.Key, keys[0])
})
t.Run("multiple keys", func(t *testing.T) {
_, tearDown := setupKVs(t, ss)
defer tearDown()
// Ignore the pluginId setup by setupKVs
pluginId := model.NewId()
var keys []string
for i := 0; i < 150; i++ {
key := model.NewId()
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
keys = append(keys, key)
}
sort.Strings(keys)
keys1, err := ss.Plugin().List(pluginId, 0, 100)
require.NoError(t, err)
require.Len(t, keys1, 100)
keys2, err := ss.Plugin().List(pluginId, 100, 100)
require.NoError(t, err)
require.Len(t, keys2, 50)
actualKeys := append(keys1, keys2...)
sort.Strings(actualKeys)
assert.Equal(t, keys, actualKeys)
})
t.Run("multiple keys, some expiring", func(t *testing.T) {
_, tearDown := setupKVs(t, ss)
defer tearDown()
// Ignore the pluginId setup by setupKVs
pluginId := model.NewId()
var keys []string
now := model.GetMillis()
for i := 0; i < 150; i++ {
key := model.NewId()
var expireAt int64
if i%10 == 0 {
// Expire keys 0, 10, 20, ...
expireAt = 1
} else if (i+5)%10 == 0 {
// Mark for future expiry keys 5, 15, 25, ...
expireAt = now + 5*60*1000
}
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(model.NewId()),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
if expireAt == 0 || expireAt > now {
keys = append(keys, key)
}
}
sort.Strings(keys)
keys1, err := ss.Plugin().List(pluginId, 0, 100)
require.NoError(t, err)
require.Len(t, keys1, 100)
keys2, err := ss.Plugin().List(pluginId, 100, 100)
require.NoError(t, err)
require.Len(t, keys2, 35)
actualKeys := append(keys1, keys2...)
sort.Strings(actualKeys)
assert.Equal(t, keys, actualKeys)
})
t.Run("offsets and limits", func(t *testing.T) {
_, tearDown := setupKVs(t, ss)
defer tearDown()
// Ignore the pluginId setup by setupKVs
pluginId := model.NewId()
var keys []string
for i := 0; i < 150; i++ {
key := model.NewId()
kv := &model.PluginKeyValue{
PluginId: pluginId,
Key: key,
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
keys = append(keys, key)
}
sort.Strings(keys)
t.Run("default limit", func(t *testing.T) {
keys1, err := ss.Plugin().List(pluginId, 0, 0)
require.NoError(t, err)
require.Len(t, keys1, 10)
})
t.Run("offset 0, limit 1", func(t *testing.T) {
keys2, err := ss.Plugin().List(pluginId, 0, 1)
require.NoError(t, err)
require.Len(t, keys2, 1)
})
t.Run("offset 1, limit 1", func(t *testing.T) {
keys2, err := ss.Plugin().List(pluginId, 1, 1)
require.NoError(t, err)
require.Len(t, keys2, 1)
})
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestPostAcknowledgementsStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("Save", func(t *testing.T) { testPostAcknowledgementsStoreSave(t, ss) })
t.Run("GetForPost", func(t *testing.T) { testPostAcknowledgementsStoreGetForPost(t, ss) })
t.Run("GetForPosts", func(t *testing.T) { testPostAcknowledgementsStoreGetForPosts(t, ss) })
}
func testPostAcknowledgementsStoreSave(t *testing.T, ss store.Store) {
userId1 := model.NewId()
p1 := model.Post{}
p1.ChannelId = model.NewId()
p1.UserId = model.NewId()
p1.Message = NewTestId()
p1.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(true),
PersistentNotifications: model.NewBool(false),
},
}
post, err := ss.Post().Save(&p1)
require.NoError(t, err)
t.Run("consecutive saves should just update the acknowledged at", func(t *testing.T) {
_, err := ss.PostAcknowledgement().Save(post.Id, userId1, 0)
require.NoError(t, err)
_, err = ss.PostAcknowledgement().Save(post.Id, userId1, 0)
require.NoError(t, err)
ack1, err := ss.PostAcknowledgement().Save(post.Id, userId1, 0)
require.NoError(t, err)
acknowledgements, err := ss.PostAcknowledgement().GetForPost(post.Id)
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack1})
})
t.Run("saving should update the update at of the post", func(t *testing.T) {
oldUpdateAt := post.UpdateAt
_, err := ss.PostAcknowledgement().Save(post.Id, userId1, 0)
require.NoError(t, err)
post, err = ss.Post().GetSingle(post.Id, false)
require.NoError(t, err)
require.Greater(t, post.UpdateAt, oldUpdateAt)
})
}
func testPostAcknowledgementsStoreGetForPost(t *testing.T, ss store.Store) {
userId1 := model.NewId()
userId2 := model.NewId()
userId3 := model.NewId()
p1 := model.Post{}
p1.ChannelId = model.NewId()
p1.UserId = model.NewId()
p1.Message = NewTestId()
p1.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(true),
PersistentNotifications: model.NewBool(false),
},
}
_, err := ss.Post().Save(&p1)
require.NoError(t, err)
t.Run("get acknowledgements for post", func(t *testing.T) {
ack1, err := ss.PostAcknowledgement().Save(p1.Id, userId1, 0)
require.NoError(t, err)
ack2, err := ss.PostAcknowledgement().Save(p1.Id, userId2, 0)
require.NoError(t, err)
ack3, err := ss.PostAcknowledgement().Save(p1.Id, userId3, 0)
require.NoError(t, err)
acknowledgements, err := ss.PostAcknowledgement().GetForPost(p1.Id)
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack1, ack2, ack3})
err = ss.PostAcknowledgement().Delete(ack1)
require.NoError(t, err)
acknowledgements, err = ss.PostAcknowledgement().GetForPost(p1.Id)
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack2, ack3})
err = ss.PostAcknowledgement().Delete(ack2)
require.NoError(t, err)
acknowledgements, err = ss.PostAcknowledgement().GetForPost(p1.Id)
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack3})
err = ss.PostAcknowledgement().Delete(ack3)
require.NoError(t, err)
acknowledgements, err = ss.PostAcknowledgement().GetForPost(p1.Id)
require.NoError(t, err)
require.Empty(t, acknowledgements)
})
}
func testPostAcknowledgementsStoreGetForPosts(t *testing.T, ss store.Store) {
userId1 := model.NewId()
userId2 := model.NewId()
userId3 := model.NewId()
p1 := model.Post{}
p1.ChannelId = model.NewId()
p1.UserId = model.NewId()
p1.Message = NewTestId()
p1.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(true),
PersistentNotifications: model.NewBool(false),
},
}
p2 := model.Post{}
p2.ChannelId = model.NewId()
p2.UserId = model.NewId()
p2.Message = NewTestId()
p2.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(""),
RequestedAck: model.NewBool(true),
PersistentNotifications: model.NewBool(false),
},
}
_, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1, &p2})
require.NoError(t, err)
require.Equal(t, -1, errIdx)
t.Run("get acknowledgements for post", func(t *testing.T) {
ack1, err := ss.PostAcknowledgement().Save(p1.Id, userId1, 0)
require.NoError(t, err)
ack2, err := ss.PostAcknowledgement().Save(p1.Id, userId2, 0)
require.NoError(t, err)
ack3, err := ss.PostAcknowledgement().Save(p2.Id, userId2, 0)
require.NoError(t, err)
ack4, err := ss.PostAcknowledgement().Save(p2.Id, userId3, 0)
require.NoError(t, err)
acknowledgements, err := ss.PostAcknowledgement().GetForPosts([]string{p1.Id})
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack1, ack2})
acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p2.Id})
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack3, ack4})
acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p1.Id, p2.Id})
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack1, ack2, ack3, ack4})
err = ss.PostAcknowledgement().Delete(ack1)
require.NoError(t, err)
acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p1.Id, p2.Id})
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack2, ack3, ack4})
err = ss.PostAcknowledgement().Delete(ack2)
require.NoError(t, err)
acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p1.Id, p2.Id})
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack3, ack4})
err = ss.PostAcknowledgement().Delete(ack3)
require.NoError(t, err)
acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p1.Id, p2.Id})
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack4})
err = ss.PostAcknowledgement().Delete(ack4)
require.NoError(t, err)
acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p1.Id, p2.Id})
require.NoError(t, err)
require.Empty(t, acknowledgements)
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"database/sql"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestPostPriorityStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("GetForPost", func(t *testing.T) { testPostPriorityStoreGetForPost(t, ss) })
}
func testPostPriorityStoreGetForPost(t *testing.T, ss store.Store) {
t.Run("Save post priority when in post's metadata", func(t *testing.T) {
p1 := model.Post{}
p1.ChannelId = model.NewId()
p1.UserId = model.NewId()
p1.Message = NewTestId()
p1.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(true),
PersistentNotifications: model.NewBool(false),
},
}
p2 := model.Post{}
p2.ChannelId = model.NewId()
p2.UserId = model.NewId()
p2.Message = NewTestId()
p2.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(true),
},
}
p3 := model.Post{}
p3.ChannelId = model.NewId()
p3.UserId = model.NewId()
p3.Message = NewTestId()
_, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1, &p2, &p3})
require.NoError(t, err)
require.Equal(t, -1, errIdx)
pp1, err := ss.PostPriority().GetForPost(p1.Id)
require.NoError(t, err)
assert.Equal(t, "important", *pp1.Priority)
assert.Equal(t, true, *pp1.RequestedAck)
assert.Equal(t, false, *pp1.PersistentNotifications)
pp2, err := ss.PostPriority().GetForPost(p2.Id)
require.NoError(t, err)
assert.Equal(t, model.PostPriorityUrgent, *pp2.Priority)
assert.Equal(t, false, *pp2.RequestedAck)
assert.Equal(t, true, *pp2.PersistentNotifications)
_, err = ss.PostPriority().GetForPost(p3.Id)
assert.True(t, errors.Is(err, sql.ErrNoRows))
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
func TestPostStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("SaveMultiple", func(t *testing.T) { testPostStoreSaveMultiple(t, ss) })
t.Run("Save", func(t *testing.T) { testPostStoreSave(t, ss) })
t.Run("SaveAndUpdateChannelMsgCounts", func(t *testing.T) { testPostStoreSaveChannelMsgCounts(t, ss) })
t.Run("Get", func(t *testing.T) { testPostStoreGet(t, ss) })
t.Run("GetSingle", func(t *testing.T) { testPostStoreGetSingle(t, ss) })
t.Run("Update", func(t *testing.T) { testPostStoreUpdate(t, ss) })
t.Run("Delete", func(t *testing.T) { testPostStoreDelete(t, ss) })
t.Run("PermDelete1Level", func(t *testing.T) { testPostStorePermDelete1Level(t, ss) })
t.Run("PermDelete1Level2", func(t *testing.T) { testPostStorePermDelete1Level2(t, ss) })
t.Run("GetWithChildren", func(t *testing.T) { testPostStoreGetWithChildren(t, ss) })
t.Run("GetPostsWithDetails", func(t *testing.T) { testPostStoreGetPostsWithDetails(t, ss) })
t.Run("GetPostsBeforeAfter", func(t *testing.T) { testPostStoreGetPostsBeforeAfter(t, ss) })
t.Run("GetPostsSince", func(t *testing.T) { testPostStoreGetPostsSince(t, ss) })
t.Run("GetPosts", func(t *testing.T) { testPostStoreGetPosts(t, ss) })
t.Run("GetPostBeforeAfter", func(t *testing.T) { testPostStoreGetPostBeforeAfter(t, ss) })
t.Run("UserCountsWithPostsByDay", func(t *testing.T) { testUserCountsWithPostsByDay(t, ss) })
t.Run("PostCountsByDuration", func(t *testing.T) { testPostCountsByDay(t, ss) })
t.Run("PostCounts", func(t *testing.T) { testPostCounts(t, ss) })
t.Run("GetFlaggedPostsForTeam", func(t *testing.T) { testPostStoreGetFlaggedPostsForTeam(t, ss, s) })
t.Run("GetFlaggedPosts", func(t *testing.T) { testPostStoreGetFlaggedPosts(t, ss) })
t.Run("GetFlaggedPostsForChannel", func(t *testing.T) { testPostStoreGetFlaggedPostsForChannel(t, ss) })
t.Run("GetPostsCreatedAt", func(t *testing.T) { testPostStoreGetPostsCreatedAt(t, ss) })
t.Run("Overwrite", func(t *testing.T) { testPostStoreOverwrite(t, ss) })
t.Run("OverwriteMultiple", func(t *testing.T) { testPostStoreOverwriteMultiple(t, ss) })
t.Run("GetPostsByIds", func(t *testing.T) { testPostStoreGetPostsByIds(t, ss) })
t.Run("GetPostsBatchForIndexing", func(t *testing.T) { testPostStoreGetPostsBatchForIndexing(t, ss) })
t.Run("PermanentDeleteBatch", func(t *testing.T) { testPostStorePermanentDeleteBatch(t, ss) })
t.Run("GetOldest", func(t *testing.T) { testPostStoreGetOldest(t, ss) })
t.Run("TestGetMaxPostSize", func(t *testing.T) { testGetMaxPostSize(t, ss) })
t.Run("GetParentsForExportAfter", func(t *testing.T) { testPostStoreGetParentsForExportAfter(t, ss) })
t.Run("GetRepliesForExport", func(t *testing.T) { testPostStoreGetRepliesForExport(t, ss) })
t.Run("GetDirectPostParentsForExportAfter", func(t *testing.T) { testPostStoreGetDirectPostParentsForExportAfter(t, ss, s) })
t.Run("GetDirectPostParentsForExportAfterDeleted", func(t *testing.T) { testPostStoreGetDirectPostParentsForExportAfterDeleted(t, ss, s) })
t.Run("GetDirectPostParentsForExportAfterBatched", func(t *testing.T) { testPostStoreGetDirectPostParentsForExportAfterBatched(t, ss, s) })
t.Run("GetForThread", func(t *testing.T) { testPostStoreGetForThread(t, ss) })
t.Run("HasAutoResponsePostByUserSince", func(t *testing.T) { testHasAutoResponsePostByUserSince(t, ss) })
t.Run("GetPostsSinceForSync", func(t *testing.T) { testGetPostsSinceForSync(t, ss, s) })
t.Run("SetPostReminder", func(t *testing.T) { testSetPostReminder(t, ss, s) })
t.Run("GetPostReminders", func(t *testing.T) { testGetPostReminders(t, ss, s) })
t.Run("GetPostReminderMetadata", func(t *testing.T) { testGetPostReminderMetadata(t, ss, s) })
t.Run("GetNthRecentPostTime", func(t *testing.T) { testGetNthRecentPostTime(t, ss) })
t.Run("GetTopDMsForUserSince", func(t *testing.T) { testGetTopDMsForUserSince(t, ss, s) })
t.Run("GetEditHistoryForPost", func(t *testing.T) { testGetEditHistoryForPost(t, ss) })
}
func testPostStoreSave(t *testing.T, ss store.Store) {
t.Run("Save post", func(t *testing.T) {
o1 := model.Post{}
o1.ChannelId = model.NewId()
o1.UserId = model.NewId()
o1.Message = NewTestId()
p, err := ss.Post().Save(&o1)
require.NoError(t, err, "couldn't save item")
assert.Equal(t, int64(0), p.ReplyCount)
})
t.Run("Save replies", func(t *testing.T) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.RootId = model.NewId()
o1.Message = NewTestId()
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o2 := model.Post{}
o2.ChannelId = channel2.Id
o2.UserId = model.NewId()
o2.RootId = o1.RootId
o2.Message = NewTestId()
channel3, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName3",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o3 := model.Post{}
o3.ChannelId = channel3.Id
o3.UserId = model.NewId()
o3.RootId = model.NewId()
o3.Message = NewTestId()
p1, err := ss.Post().Save(&o1)
require.NoError(t, err, "couldn't save item")
assert.Equal(t, int64(1), p1.ReplyCount)
p2, err := ss.Post().Save(&o2)
require.NoError(t, err, "couldn't save item")
assert.Equal(t, int64(2), p2.ReplyCount)
p3, err := ss.Post().Save(&o3)
require.NoError(t, err, "couldn't save item")
assert.Equal(t, int64(1), p3.ReplyCount)
})
t.Run("Try to save existing post", func(t *testing.T) {
o1 := model.Post{}
o1.ChannelId = model.NewId()
o1.UserId = model.NewId()
o1.Message = NewTestId()
_, err := ss.Post().Save(&o1)
require.NoError(t, err, "couldn't save item")
_, err = ss.Post().Save(&o1)
require.Error(t, err, "shouldn't be able to update from save")
})
t.Run("Update reply should update the UpdateAt of the root post", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
rootPost := model.Post{}
rootPost.ChannelId = channel.Id
rootPost.UserId = model.NewId()
rootPost.Message = NewTestId()
_, err = ss.Post().Save(&rootPost)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
replyPost := model.Post{}
replyPost.ChannelId = rootPost.ChannelId
replyPost.UserId = model.NewId()
replyPost.Message = NewTestId()
replyPost.RootId = rootPost.Id
// We need to sleep here to be sure the post is not created during the same millisecond
time.Sleep(time.Millisecond)
_, err = ss.Post().Save(&replyPost)
require.NoError(t, err)
rrootPost, err := ss.Post().GetSingle(rootPost.Id, false)
require.NoError(t, err)
assert.Greater(t, rrootPost.UpdateAt, rootPost.UpdateAt)
})
t.Run("Create a post should update the channel LastPostAt and the total messages count by one", func(t *testing.T) {
channel := model.Channel{}
channel.Name = NewTestId()
channel.DisplayName = NewTestId()
channel.Type = model.ChannelTypeOpen
_, err := ss.Channel().Save(&channel, 100)
require.NoError(t, err)
post := model.Post{}
post.ChannelId = channel.Id
post.UserId = model.NewId()
post.Message = NewTestId()
// We need to sleep here to be sure the post is not created during the same millisecond
time.Sleep(time.Millisecond)
_, err = ss.Post().Save(&post)
require.NoError(t, err)
rchannel, err := ss.Channel().Get(channel.Id, false)
require.NoError(t, err)
assert.Greater(t, rchannel.LastPostAt, channel.LastPostAt)
assert.Equal(t, int64(1), rchannel.TotalMsgCount)
post = model.Post{}
post.ChannelId = channel.Id
post.UserId = model.NewId()
post.Message = NewTestId()
post.CreateAt = 5
// We need to sleep here to be sure the post is not created during the same millisecond
time.Sleep(time.Millisecond)
_, err = ss.Post().Save(&post)
require.NoError(t, err)
rchannel2, err := ss.Channel().Get(channel.Id, false)
require.NoError(t, err)
assert.Equal(t, rchannel.LastPostAt, rchannel2.LastPostAt)
assert.Equal(t, int64(2), rchannel2.TotalMsgCount)
post = model.Post{}
post.ChannelId = channel.Id
post.UserId = model.NewId()
post.Message = NewTestId()
// We need to sleep here to be sure the post is not created during the same millisecond
time.Sleep(time.Millisecond)
_, err = ss.Post().Save(&post)
require.NoError(t, err)
rchannel3, err := ss.Channel().Get(channel.Id, false)
require.NoError(t, err)
assert.Greater(t, rchannel3.LastPostAt, rchannel2.LastPostAt)
assert.Equal(t, int64(3), rchannel3.TotalMsgCount)
})
t.Run("Save post with priority metadata set", func(t *testing.T) {
o1 := model.Post{}
o1.ChannelId = model.NewId()
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(true),
PersistentNotifications: model.NewBool(false),
},
}
p, err := ss.Post().Save(&o1)
require.NoError(t, err, "couldn't save item")
assert.Equal(t, int64(0), p.ReplyCount)
pp, err := ss.PostPriority().GetForPost(p.Id)
require.NoError(t, err, "couldn't save item")
assert.Equal(t, "important", *pp.Priority)
assert.Equal(t, true, *pp.RequestedAck)
assert.Equal(t, false, *pp.PersistentNotifications)
})
}
func testPostStoreSaveMultiple(t *testing.T, ss store.Store) {
p1 := model.Post{}
p1.ChannelId = model.NewId()
p1.UserId = model.NewId()
p1.Message = NewTestId()
p2 := model.Post{}
p2.ChannelId = model.NewId()
p2.UserId = model.NewId()
p2.Message = NewTestId()
p3 := model.Post{}
p3.ChannelId = model.NewId()
p3.UserId = model.NewId()
p3.Message = NewTestId()
p4 := model.Post{}
p4.ChannelId = model.NewId()
p4.UserId = model.NewId()
p4.Message = NewTestId()
t.Run("Save correctly a new set of posts", func(t *testing.T) {
newPosts, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1, &p2, &p3})
require.NoError(t, err)
require.Equal(t, -1, errIdx)
for _, post := range newPosts {
storedPost, err := ss.Post().GetSingle(post.Id, false)
assert.NoError(t, err)
assert.Equal(t, post.ChannelId, storedPost.ChannelId)
assert.Equal(t, post.Message, storedPost.Message)
assert.Equal(t, post.UserId, storedPost.UserId)
}
})
t.Run("Save replies", func(t *testing.T) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel3, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName3",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel4, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName4",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.RootId = model.NewId()
o1.Message = NewTestId()
o2 := model.Post{}
o2.ChannelId = channel2.Id
o2.UserId = model.NewId()
o2.RootId = o1.RootId
o2.Message = NewTestId()
o3 := model.Post{}
o3.ChannelId = channel3.Id
o3.UserId = model.NewId()
o3.RootId = model.NewId()
o3.Message = NewTestId()
o4 := model.Post{}
o4.ChannelId = channel4.Id
o4.UserId = model.NewId()
o4.Message = NewTestId()
newPosts, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&o1, &o2, &o3, &o4})
require.NoError(t, err, "couldn't save item")
require.Equal(t, -1, errIdx)
assert.Len(t, newPosts, 4)
assert.Equal(t, int64(2), newPosts[0].ReplyCount)
assert.Equal(t, int64(2), newPosts[1].ReplyCount)
assert.Equal(t, int64(1), newPosts[2].ReplyCount)
assert.Equal(t, int64(0), newPosts[3].ReplyCount)
})
t.Run("Try to save mixed, already saved and not saved posts", func(t *testing.T) {
newPosts, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p4, &p3})
require.Error(t, err)
require.Equal(t, 1, errIdx)
require.Nil(t, newPosts)
storedPost, err := ss.Post().GetSingle(p3.Id, false)
assert.NoError(t, err)
assert.Equal(t, p3.ChannelId, storedPost.ChannelId)
assert.Equal(t, p3.Message, storedPost.Message)
assert.Equal(t, p3.UserId, storedPost.UserId)
storedPost, err = ss.Post().GetSingle(p4.Id, false)
assert.Error(t, err)
assert.Nil(t, storedPost)
})
t.Run("Update reply should update the UpdateAt of the root post", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
rootPost := model.Post{}
rootPost.ChannelId = channel.Id
rootPost.UserId = model.NewId()
rootPost.Message = NewTestId()
replyPost := model.Post{}
replyPost.ChannelId = rootPost.ChannelId
replyPost.UserId = model.NewId()
replyPost.Message = NewTestId()
replyPost.RootId = rootPost.Id
_, _, err = ss.Post().SaveMultiple([]*model.Post{&rootPost, &replyPost})
require.NoError(t, err)
rrootPost, err := ss.Post().GetSingle(rootPost.Id, false)
require.NoError(t, err)
assert.Equal(t, rrootPost.UpdateAt, rootPost.UpdateAt)
replyPost2 := model.Post{}
replyPost2.ChannelId = rootPost.ChannelId
replyPost2.UserId = model.NewId()
replyPost2.Message = NewTestId()
replyPost2.RootId = rootPost.Id
replyPost3 := model.Post{}
replyPost3.ChannelId = rootPost.ChannelId
replyPost3.UserId = model.NewId()
replyPost3.Message = NewTestId()
replyPost3.RootId = rootPost.Id
// Ensure update does not occur in the same timestamp as creation
time.Sleep(time.Millisecond)
_, _, err = ss.Post().SaveMultiple([]*model.Post{&replyPost2, &replyPost3})
require.NoError(t, err)
rrootPost2, err := ss.Post().GetSingle(rootPost.Id, false)
require.NoError(t, err)
assert.Greater(t, rrootPost2.UpdateAt, rrootPost.UpdateAt)
})
t.Run("Create a post should update the channel LastPostAt and the total messages count by one", func(t *testing.T) {
channel := model.Channel{}
channel.Name = NewTestId()
channel.DisplayName = NewTestId()
channel.Type = model.ChannelTypeOpen
_, err := ss.Channel().Save(&channel, 100)
require.NoError(t, err)
post1 := model.Post{}
post1.ChannelId = channel.Id
post1.UserId = model.NewId()
post1.Message = NewTestId()
post2 := model.Post{}
post2.ChannelId = channel.Id
post2.UserId = model.NewId()
post2.Message = NewTestId()
post2.CreateAt = 5
post3 := model.Post{}
post3.ChannelId = channel.Id
post3.UserId = model.NewId()
post3.Message = NewTestId()
_, _, err = ss.Post().SaveMultiple([]*model.Post{&post1, &post2, &post3})
require.NoError(t, err)
rchannel, err := ss.Channel().Get(channel.Id, false)
require.NoError(t, err)
assert.Greater(t, rchannel.LastPostAt, channel.LastPostAt)
assert.Equal(t, int64(3), rchannel.TotalMsgCount)
})
t.Run("Thread participants", func(t *testing.T) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.Message = "jessica hyde" + model.NewId() + "b"
root, err := ss.Post().Save(&o1)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel3, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName3",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel4, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName4",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel5, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName5",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o2 := model.Post{}
o2.ChannelId = channel2.Id
o2.UserId = model.NewId()
o2.RootId = root.Id
o2.Message = "zz" + model.NewId() + "b"
o3 := model.Post{}
o3.ChannelId = channel3.Id
o3.UserId = model.NewId()
o3.RootId = root.Id
o3.Message = "zz" + model.NewId() + "b"
o4 := model.Post{}
o4.ChannelId = channel4.Id
o4.UserId = o2.UserId
o4.RootId = root.Id
o4.Message = "zz" + model.NewId() + "b"
o5 := model.Post{}
o5.ChannelId = channel5.Id
o5.UserId = o1.UserId
o5.RootId = root.Id
o5.Message = "zz" + model.NewId() + "b"
_, err = ss.Post().Save(&o2)
require.NoError(t, err)
thread, errT := ss.Thread().Get(root.Id)
require.NoError(t, errT)
assert.Equal(t, int64(1), thread.ReplyCount)
assert.Equal(t, int(1), len(thread.Participants))
assert.Equal(t, model.StringArray{o2.UserId}, thread.Participants)
_, err = ss.Post().Save(&o3)
require.NoError(t, err)
thread, errT = ss.Thread().Get(root.Id)
require.NoError(t, errT)
assert.Equal(t, int64(2), thread.ReplyCount)
assert.Equal(t, int(2), len(thread.Participants))
assert.Equal(t, model.StringArray{o2.UserId, o3.UserId}, thread.Participants)
_, err = ss.Post().Save(&o4)
require.NoError(t, err)
thread, errT = ss.Thread().Get(root.Id)
require.NoError(t, errT)
assert.Equal(t, int64(3), thread.ReplyCount)
assert.Equal(t, int(2), len(thread.Participants))
assert.Equal(t, model.StringArray{o3.UserId, o2.UserId}, thread.Participants)
_, err = ss.Post().Save(&o5)
require.NoError(t, err)
thread, errT = ss.Thread().Get(root.Id)
require.NoError(t, errT)
assert.Equal(t, int64(4), thread.ReplyCount)
assert.Equal(t, int(3), len(thread.Participants))
assert.Equal(t, model.StringArray{o3.UserId, o2.UserId, o1.UserId}, thread.Participants)
})
}
func testPostStoreSaveChannelMsgCounts(t *testing.T, ss store.Store) {
c1 := &model.Channel{Name: model.NewId(), DisplayName: "posttestchannel", Type: model.ChannelTypeOpen, TeamId: model.NewId()}
_, err := ss.Channel().Save(c1, 1000000)
require.NoError(t, err)
o1 := model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
_, err = ss.Post().Save(&o1)
require.NoError(t, err)
c1, err = ss.Channel().Get(c1.Id, false)
require.NoError(t, err)
assert.Equal(t, int64(1), c1.TotalMsgCount, "Message count should update by 1")
o1.Id = ""
o1.Type = model.PostTypeAddToTeam
_, err = ss.Post().Save(&o1)
require.NoError(t, err)
o1.Id = ""
o1.Type = model.PostTypeRemoveFromTeam
_, err = ss.Post().Save(&o1)
require.NoError(t, err)
c1, err = ss.Channel().Get(c1.Id, false)
require.NoError(t, err)
assert.Equal(t, int64(1), c1.TotalMsgCount, "Message count should not update for team add/removed message")
oldLastPostAt := c1.LastPostAt
o2 := model.Post{}
o2.ChannelId = c1.Id
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.CreateAt = int64(7)
_, err = ss.Post().Save(&o2)
require.NoError(t, err)
c1, err = ss.Channel().Get(c1.Id, false)
require.NoError(t, err)
assert.Equal(t, oldLastPostAt, c1.LastPostAt, "LastPostAt should not update for old message save")
}
func testPostStoreGet(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
etag1 := ss.Post().GetEtag(o1.ChannelId, false, false)
require.Equal(t, 0, strings.Index(etag1, model.CurrentVersion+"."), "Invalid Etag")
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
etag2 := ss.Post().GetEtag(o1.ChannelId, false, false)
require.Equal(t, 0, strings.Index(etag2, fmt.Sprintf("%v.%v", model.CurrentVersion, o1.UpdateAt)), "Invalid Etag")
r1, err := ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
require.Equal(t, r1.Posts[o1.Id].CreateAt, o1.CreateAt, "invalid returned post")
_, err = ss.Post().Get(context.Background(), "123", model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Missing id should have failed")
_, err = ss.Post().Get(context.Background(), "", model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "should fail for blank post ids")
}
func testPostStoreGetForThread(t *testing.T, ss store.Store) {
t.Run("Post thread is followed", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestId()}
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id})
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(o1.UserId, o1.Id, store.ThreadMembershipOpts{
Following: true,
UpdateFollowing: true,
})
require.NoError(t, err)
opts := model.GetPostsOptions{
CollapsedThreads: true,
}
r1, err := ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
require.Equal(t, r1.Posts[o1.Id].CreateAt, o1.CreateAt, "invalid returned post")
require.True(t, *r1.Posts[o1.Id].IsFollowing)
})
t.Run("Post thread is explicitly not followed", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestId()}
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id})
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(o1.UserId, o1.Id, store.ThreadMembershipOpts{
Following: false,
UpdateFollowing: true,
})
require.NoError(t, err)
opts := model.GetPostsOptions{
CollapsedThreads: true,
}
r1, err := ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
require.Equal(t, r1.Posts[o1.Id].CreateAt, o1.CreateAt, "invalid returned post")
require.False(t, *r1.Posts[o1.Id].IsFollowing)
})
t.Run("Post threadmembership does not exist", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestId()}
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id})
require.NoError(t, err)
opts := model.GetPostsOptions{
CollapsedThreads: true,
}
r1, err := ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
require.Equal(t, r1.Posts[o1.Id].CreateAt, o1.CreateAt, "invalid returned post")
require.Nil(t, r1.Posts[o1.Id].IsFollowing)
})
t.Run("Pagination", func(t *testing.T) {
t.Skip("MM-46134")
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1, err := ss.Post().Save(&model.Post{ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestId()})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id})
require.NoError(t, err)
m1, err := ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id})
require.NoError(t, err)
opts := model.GetPostsOptions{
CollapsedThreads: true,
PerPage: 2,
Direction: "down",
}
r1, err := ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
assert.Len(t, r1.Order, 3) // including the root post
assert.True(t, r1.HasNext)
lastPostID := r1.Order[len(r1.Order)-1]
lastPostCreateAt := r1.Posts[lastPostID].CreateAt
opts = model.GetPostsOptions{
CollapsedThreads: true,
PerPage: 2,
Direction: "down",
FromPost: lastPostID,
FromCreateAt: lastPostCreateAt,
}
r1, err = ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
assert.Len(t, r1.Order, 3) // including the root post
assert.GreaterOrEqual(t, r1.Posts[r1.Order[len(r1.Order)-1]].CreateAt, lastPostCreateAt)
assert.False(t, r1.HasNext)
// Going from bottom to top now.
firstPostCreateAt := r1.Posts[r1.Order[1]].CreateAt
opts = model.GetPostsOptions{
CollapsedThreads: true,
PerPage: 2,
Direction: "up",
FromPost: r1.Order[1],
FromCreateAt: firstPostCreateAt,
}
r1, err = ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
assert.Len(t, r1.Order, 3) // including the root post
assert.LessOrEqual(t, r1.Posts[r1.Order[1]].CreateAt, firstPostCreateAt)
assert.False(t, r1.HasNext)
// Only with CreateAt
opts = model.GetPostsOptions{
CollapsedThreads: false,
PerPage: 1,
Direction: "up",
FromCreateAt: m1.CreateAt,
SkipFetchThreads: false,
}
r1, err = ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
assert.Len(t, r1.Order, 2) // including the root post
assert.LessOrEqual(t, r1.Posts[r1.Order[1]].CreateAt, m1.CreateAt)
assert.True(t, r1.HasNext)
// Non-CRT mode
opts = model.GetPostsOptions{
CollapsedThreads: false,
PerPage: 2,
Direction: "down",
SkipFetchThreads: false,
}
r1, err = ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
assert.Len(t, r1.Order, 2) // including the root post
assert.True(t, r1.HasNext)
lastPostID = r1.Order[len(r1.Order)-1]
lastPostCreateAt = r1.Posts[lastPostID].CreateAt
opts = model.GetPostsOptions{
CollapsedThreads: false,
PerPage: 3,
Direction: "down",
FromPost: lastPostID,
FromCreateAt: lastPostCreateAt,
SkipFetchThreads: false,
}
r1, err = ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
assert.Len(t, r1.Order, 4) // including the root post
assert.GreaterOrEqual(t, r1.Posts[r1.Order[len(r1.Order)-1]].CreateAt, lastPostCreateAt)
assert.False(t, r1.HasNext)
// Going from bottom to top now.
firstPostCreateAt = r1.Posts[r1.Order[1]].CreateAt
opts = model.GetPostsOptions{
CollapsedThreads: false,
PerPage: 2,
Direction: "up",
FromPost: r1.Order[1],
FromCreateAt: firstPostCreateAt,
SkipFetchThreads: false,
}
r1, err = ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
assert.Len(t, r1.Order, 2) // including the root post
assert.LessOrEqual(t, r1.Posts[r1.Order[1]].CreateAt, firstPostCreateAt)
assert.False(t, r1.HasNext)
// Only with CreateAt
opts = model.GetPostsOptions{
CollapsedThreads: false,
PerPage: 1,
Direction: "down",
FromCreateAt: m1.CreateAt,
SkipFetchThreads: false,
}
r1, err = ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
assert.Len(t, r1.Order, 2) // including the root post
assert.GreaterOrEqual(t, r1.Posts[r1.Order[1]].CreateAt, m1.CreateAt)
assert.True(t, r1.HasNext)
})
}
func testPostStoreGetSingle(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = o1.UserId
o2.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = o1.UserId
o3.Message = model.NewRandomString(10)
o3.RootId = o1.Id
o4 := &model.Post{}
o4.ChannelId = o1.ChannelId
o4.UserId = o1.UserId
o4.Message = model.NewRandomString(10)
o4.RootId = o1.Id
_, err = ss.Post().Save(o3)
require.NoError(t, err)
o4, err = ss.Post().Save(o4)
require.NoError(t, err)
err = ss.Post().Delete(o2.Id, model.GetMillis(), o2.UserId)
require.NoError(t, err)
err = ss.Post().Delete(o4.Id, model.GetMillis(), o4.UserId)
require.NoError(t, err)
post, err := ss.Post().GetSingle(o1.Id, false)
require.NoError(t, err)
require.Equal(t, post.CreateAt, o1.CreateAt, "invalid returned post")
require.Equal(t, int64(1), post.ReplyCount, "wrong replyCount computed")
_, err = ss.Post().GetSingle(o2.Id, false)
require.Error(t, err, "should not return deleted post")
post, err = ss.Post().GetSingle(o2.Id, true)
require.NoError(t, err)
require.Equal(t, post.CreateAt, o2.CreateAt, "invalid returned post")
require.NotZero(t, post.DeleteAt, "DeleteAt should be non-zero")
require.Zero(t, post.ReplyCount, "Post without replies should return zero ReplyCount")
_, err = ss.Post().GetSingle("123", false)
require.Error(t, err, "Missing id should have failed")
}
func testPostStoreUpdate(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.RootId = o1.Id
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
r1, err := ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro1 := r1.Posts[o1.Id]
r2, err := ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro2 := r2.Posts[o2.Id]
r3, err := ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro3 := r3.Posts[o3.Id]
require.Equal(t, ro1.Message, o1.Message, "Failed to save/get")
o1a := ro1.Clone()
o1a.Message = ro1.Message + "BBBBBBBBBB"
_, err = ss.Post().Update(o1a, ro1)
require.NoError(t, err)
r1, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro1a := r1.Posts[o1.Id]
require.Equal(t, ro1a.Message, o1a.Message, "Failed to update/get")
o2a := ro2.Clone()
o2a.Message = ro2.Message + "DDDDDDD"
_, err = ss.Post().Update(o2a, ro2)
require.NoError(t, err)
r2, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro2a := r2.Posts[o2.Id]
require.Equal(t, ro2a.Message, o2a.Message, "Failed to update/get")
o3a := ro3.Clone()
o3a.Message = ro3.Message + "WWWWWWW"
_, err = ss.Post().Update(o3a, ro3)
require.NoError(t, err)
r3, err = ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro3a := r3.Posts[o3.Id]
if ro3a.Message != o3a.Message {
require.Equal(t, ro3a.Hashtags, o3a.Hashtags, "Failed to update/get")
}
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o4, err := ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: model.NewId(),
Message: model.NewId(),
Filenames: []string{"test"},
})
require.NoError(t, err)
r4, err := ss.Post().Get(context.Background(), o4.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro4 := r4.Posts[o4.Id]
o4a := ro4.Clone()
o4a.Filenames = []string{}
o4a.FileIds = []string{model.NewId()}
_, err = ss.Post().Update(o4a, ro4)
require.NoError(t, err)
r4, err = ss.Post().Get(context.Background(), o4.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro4a := r4.Posts[o4.Id]
require.Empty(t, ro4a.Filenames, "Failed to clear Filenames")
require.Len(t, ro4a.FileIds, 1, "Failed to set FileIds")
}
func testPostStoreDelete(t *testing.T, ss store.Store) {
t.Run("single post, no replies", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
// Create a post
rootPost, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: model.NewRandomString(10),
})
require.NoError(t, err)
// Verify etag generation for the channel containing the post.
etag1 := ss.Post().GetEtag(rootPost.ChannelId, false, false)
require.Equal(t, 0, strings.Index(etag1, model.CurrentVersion+"."), "Invalid Etag")
// Verify the created post.
r1, err := ss.Post().Get(context.Background(), rootPost.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
require.NotNil(t, r1.Posts[rootPost.Id])
require.Equal(t, rootPost, r1.Posts[rootPost.Id])
// Mark the post as deleted by the user identified with deleteByID.
deleteByID := model.NewId()
err = ss.Post().Delete(rootPost.Id, model.GetMillis(), deleteByID)
require.NoError(t, err)
// Ensure the appropriate posts prop reflects the user deleting the post.
posts, err := ss.Post().GetPostsCreatedAt(rootPost.ChannelId, rootPost.CreateAt)
require.NoError(t, err)
require.NotEmpty(t, posts)
assert.Equal(t, deleteByID, posts[0].GetProp(model.PostPropsDeleteBy), "unexpected Props[model.PostPropsDeleteBy]")
// Verify that the post is no longer fetched by default.
_, err = ss.Post().Get(context.Background(), rootPost.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "fetching deleted post should have failed")
require.IsType(t, &store.ErrNotFound{}, err)
// Verify etag generation for the channel containing the now deleted post.
etag2 := ss.Post().GetEtag(rootPost.ChannelId, false, false)
require.Equal(t, 0, strings.Index(etag2, model.CurrentVersion+"."), "Invalid Etag")
})
t.Run("thread with one reply", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
// Create a root post
rootPost, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: NewTestId(),
})
require.NoError(t, err)
// Reply to that root post
replyPost, err := ss.Post().Save(&model.Post{
ChannelId: rootPost.ChannelId,
UserId: model.NewId(),
Message: NewTestId(),
RootId: rootPost.Id,
})
require.NoError(t, err)
// Delete the root post
err = ss.Post().Delete(rootPost.Id, model.GetMillis(), "")
require.NoError(t, err)
// Verify the root post deleted
_, err = ss.Post().Get(context.Background(), rootPost.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
require.IsType(t, &store.ErrNotFound{}, err)
// Verify the reply post deleted
_, err = ss.Post().Get(context.Background(), replyPost.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
require.IsType(t, &store.ErrNotFound{}, err)
})
t.Run("thread with multiple replies", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
// Create a root post
rootPost1, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: NewTestId(),
})
require.NoError(t, err)
// Reply to that root post
replyPost1, err := ss.Post().Save(&model.Post{
ChannelId: rootPost1.ChannelId,
UserId: model.NewId(),
Message: NewTestId(),
RootId: rootPost1.Id,
})
require.NoError(t, err)
// Reply to that root post a second time
replyPost2, err := ss.Post().Save(&model.Post{
ChannelId: rootPost1.ChannelId,
UserId: model.NewId(),
Message: NewTestId(),
RootId: rootPost1.Id,
})
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
// Create another root post in a separate channel
rootPost2, err := ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: model.NewId(),
Message: NewTestId(),
})
require.NoError(t, err)
// Delete the root post
err = ss.Post().Delete(rootPost1.Id, model.GetMillis(), "")
require.NoError(t, err)
// Verify the root post and replies deleted
_, err = ss.Post().Get(context.Background(), rootPost1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(context.Background(), replyPost1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(context.Background(), replyPost2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
// Verify other root posts remain undeleted.
_, err = ss.Post().Get(context.Background(), rootPost2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
})
t.Run("thread with multiple replies, update thread last reply at", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
// Create a root post
rootPost1, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: NewTestId(),
})
require.NoError(t, err)
// Reply to that root post
replyPost1, err := ss.Post().Save(&model.Post{
ChannelId: rootPost1.ChannelId,
UserId: model.NewId(),
Message: NewTestId(),
RootId: rootPost1.Id,
})
require.NoError(t, err)
// Reply to that root post a second time
replyPost2, err := ss.Post().Save(&model.Post{
ChannelId: rootPost1.ChannelId,
UserId: model.NewId(),
Message: NewTestId(),
RootId: rootPost1.Id,
})
require.NoError(t, err)
// Reply to that root post a third time
replyPost3, err := ss.Post().Save(&model.Post{
ChannelId: rootPost1.ChannelId,
UserId: model.NewId(),
Message: NewTestId(),
RootId: rootPost1.Id,
})
require.NoError(t, err)
thread, err := ss.Thread().Get(rootPost1.Id)
require.NoError(t, err)
require.Equal(t, replyPost3.CreateAt, thread.LastReplyAt)
// Delete the reply previous to last
err = ss.Post().Delete(replyPost2.Id, model.GetMillis(), "")
require.NoError(t, err)
thread, err = ss.Thread().Get(rootPost1.Id)
require.NoError(t, err)
// last reply at should be unchanged
require.Equal(t, replyPost3.CreateAt, thread.LastReplyAt)
// Delete the last reply
err = ss.Post().Delete(replyPost3.Id, model.GetMillis(), "")
require.NoError(t, err)
thread, err = ss.Thread().Get(rootPost1.Id)
require.NoError(t, err)
// last reply at should have changed
require.Equal(t, replyPost1.CreateAt, thread.LastReplyAt)
// Delete the last reply
err = ss.Post().Delete(replyPost1.Id, model.GetMillis(), "")
require.NoError(t, err)
thread, err = ss.Thread().Get(rootPost1.Id)
require.NoError(t, err)
// last reply at should be 0
require.Equal(t, int64(0), thread.LastReplyAt)
})
}
func testPostStorePermDelete1Level(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.RootId = o1.Id
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = channel2.Id
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
channel3, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName3",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o4 := &model.Post{}
o4.ChannelId = channel3.Id
o4.RootId = o1.Id
o4.UserId = o2.UserId
o4.Message = NewTestId()
o4, err = ss.Post().Save(o4)
require.NoError(t, err)
o5 := &model.Post{}
o5.ChannelId = o3.ChannelId
o5.UserId = model.NewId()
o5.Message = NewTestId()
o5, err = ss.Post().Save(o5)
require.NoError(t, err)
o6 := &model.Post{}
o6.ChannelId = o3.ChannelId
o6.RootId = o5.Id
o6.UserId = model.NewId()
o6.Message = NewTestId()
o6, err = ss.Post().Save(o6)
require.NoError(t, err)
var thread *model.Thread
thread, err = ss.Thread().Get(o1.Id)
require.NoError(t, err)
require.EqualValues(t, 2, thread.ReplyCount)
require.EqualValues(t, model.StringArray{o2.UserId}, thread.Participants)
err2 := ss.Post().PermanentDeleteByUser(o2.UserId)
require.NoError(t, err2)
thread, err = ss.Thread().Get(o1.Id)
require.NoError(t, err)
require.EqualValues(t, 0, thread.ReplyCount)
require.EqualValues(t, model.StringArray{}, thread.Participants)
_, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err, "Deleted id shouldn't have failed")
_, err = ss.Post().Get(context.Background(), o2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
thread, err = ss.Thread().Get(o5.Id)
require.NoError(t, err)
require.NotEmpty(t, thread)
err = ss.Post().PermanentDeleteByChannel(o3.ChannelId)
require.NoError(t, err)
thread, err = ss.Thread().Get(o5.Id)
require.NoError(t, err)
require.Nil(t, thread)
_, err = ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(context.Background(), o4.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(context.Background(), o5.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(context.Background(), o6.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
}
func testPostStorePermDelete1Level2(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.RootId = o1.Id
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = channel2.Id
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
err2 := ss.Post().PermanentDeleteByUser(o1.UserId)
require.NoError(t, err2)
_, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(context.Background(), o2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err, "Deleted id should have failed")
}
func testPostStoreGetWithChildren(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.RootId = o1.Id
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3.RootId = o1.Id
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
pl, err := ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
require.Len(t, pl.Posts, 3, "invalid returned post")
dErr := ss.Post().Delete(o3.Id, model.GetMillis(), "")
require.NoError(t, dErr)
pl, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
require.Len(t, pl.Posts, 2, "invalid returned post")
dErr = ss.Post().Delete(o2.Id, model.GetMillis(), "")
require.NoError(t, dErr)
pl, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
require.Len(t, pl.Posts, 1, "invalid returned post")
}
func testPostStoreGetPostsWithDetails(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.RootId = o1.Id
_, err = ss.Post().Save(o2)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o2a := &model.Post{}
o2a.ChannelId = o1.ChannelId
o2a.UserId = model.NewId()
o2a.Message = NewTestId()
o2a.RootId = o1.Id
o2a, err = ss.Post().Save(o2a)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3.RootId = o1.Id
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o4 := &model.Post{}
o4.ChannelId = o1.ChannelId
o4.UserId = model.NewId()
o4.Message = NewTestId()
o4, err = ss.Post().Save(o4)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o5 := &model.Post{}
o5.ChannelId = o1.ChannelId
o5.UserId = model.NewId()
o5.Message = NewTestId()
o5.RootId = o4.Id
o5, err = ss.Post().Save(o5)
require.NoError(t, err)
r1, err := ss.Post().GetPosts(model.GetPostsOptions{ChannelId: o1.ChannelId, Page: 0, PerPage: 4}, false, map[string]bool{})
require.NoError(t, err)
require.Equal(t, r1.Order[0], o5.Id, "invalid order")
require.Equal(t, r1.Order[1], o4.Id, "invalid order")
require.Equal(t, r1.Order[2], o3.Id, "invalid order")
require.Equal(t, r1.Order[3], o2a.Id, "invalid order")
//the last 4, + o1 (o2a and o3's parent) + o2 (in same thread as o2a and o3)
require.Len(t, r1.Posts, 6, "wrong size")
require.Equal(t, r1.Posts[o1.Id].Message, o1.Message, "Missing parent")
r2, err := ss.Post().GetPosts(model.GetPostsOptions{ChannelId: o1.ChannelId, Page: 0, PerPage: 4}, false, map[string]bool{})
require.NoError(t, err)
require.Equal(t, r2.Order[0], o5.Id, "invalid order")
require.Equal(t, r2.Order[1], o4.Id, "invalid order")
require.Equal(t, r2.Order[2], o3.Id, "invalid order")
require.Equal(t, r2.Order[3], o2a.Id, "invalid order")
//the last 4, + o1 (o2a and o3's parent) + o2 (in same thread as o2a and o3)
require.Len(t, r2.Posts, 6, "wrong size")
require.Equal(t, r2.Posts[o1.Id].Message, o1.Message, "Missing parent")
// Run once to fill cache
_, err = ss.Post().GetPosts(model.GetPostsOptions{ChannelId: o1.ChannelId, Page: 0, PerPage: 30}, false, map[string]bool{})
require.NoError(t, err)
o6 := &model.Post{}
o6.ChannelId = o1.ChannelId
o6.UserId = model.NewId()
o6.Message = NewTestId()
_, err = ss.Post().Save(o6)
require.NoError(t, err)
r3, err := ss.Post().GetPosts(model.GetPostsOptions{ChannelId: o1.ChannelId, Page: 0, PerPage: 30}, false, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, 7, len(r3.Order))
}
func testPostStoreGetPostsBeforeAfter(t *testing.T, ss store.Store) {
t.Run("without threads", func(t *testing.T) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelId := channel1.Id
userId := model.NewId()
var posts []*model.Post
for i := 0; i < 10; i++ {
post, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
posts = append(posts, post)
time.Sleep(time.Millisecond)
}
t.Run("should return error if negative Page/PerPage options are passed", func(t *testing.T) {
postList, err := ss.Post().GetPostsAfter(model.GetPostsOptions{ChannelId: channelId, PostId: posts[0].Id, Page: 0, PerPage: -1}, map[string]bool{})
assert.Nil(t, postList)
assert.Error(t, err)
assert.IsType(t, &store.ErrInvalidInput{}, err)
postList, err = ss.Post().GetPostsAfter(model.GetPostsOptions{ChannelId: channelId, PostId: posts[0].Id, Page: -1, PerPage: 10}, map[string]bool{})
assert.Nil(t, postList)
assert.Error(t, err)
assert.IsType(t, &store.ErrInvalidInput{}, err)
})
t.Run("should not return anything before the first post", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(model.GetPostsOptions{ChannelId: channelId, PostId: posts[0].Id, Page: 0, PerPage: 10}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{}, postList.Order)
assert.Equal(t, map[string]*model.Post{}, postList.Posts)
})
t.Run("should return posts before a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(model.GetPostsOptions{ChannelId: channelId, PostId: posts[5].Id, Page: 0, PerPage: 10}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{posts[4].Id, posts[3].Id, posts[2].Id, posts[1].Id, posts[0].Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
posts[0].Id: posts[0],
posts[1].Id: posts[1],
posts[2].Id: posts[2],
posts[3].Id: posts[3],
posts[4].Id: posts[4],
}, postList.Posts)
})
t.Run("should limit posts before", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(model.GetPostsOptions{ChannelId: channelId, PostId: posts[5].Id, PerPage: 2}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{posts[4].Id, posts[3].Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
posts[3].Id: posts[3],
posts[4].Id: posts[4],
}, postList.Posts)
})
t.Run("should not return anything after the last post", func(t *testing.T) {
postList, err := ss.Post().GetPostsAfter(model.GetPostsOptions{ChannelId: channelId, PostId: posts[len(posts)-1].Id, PerPage: 10}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{}, postList.Order)
assert.Equal(t, map[string]*model.Post{}, postList.Posts)
})
t.Run("should return posts after a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsAfter(model.GetPostsOptions{ChannelId: channelId, PostId: posts[5].Id, PerPage: 10}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{posts[9].Id, posts[8].Id, posts[7].Id, posts[6].Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
posts[6].Id: posts[6],
posts[7].Id: posts[7],
posts[8].Id: posts[8],
posts[9].Id: posts[9],
}, postList.Posts)
})
t.Run("should limit posts after", func(t *testing.T) {
postList, err := ss.Post().GetPostsAfter(model.GetPostsOptions{ChannelId: channelId, PostId: posts[5].Id, PerPage: 2}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{posts[7].Id, posts[6].Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
posts[6].Id: posts[6],
posts[7].Id: posts[7],
}, postList.Posts)
})
})
t.Run("with threads", func(t *testing.T) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelId := channel1.Id
userId := model.NewId()
// This creates a series of posts that looks like:
// post1
// post2
// post3 (in response to post1)
// post4 (in response to post2)
// post5
// post6 (in response to post2)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
post1.ReplyCount = 1
require.NoError(t, err)
time.Sleep(time.Millisecond)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
post2.ReplyCount = 2
time.Sleep(time.Millisecond)
post3, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
RootId: post1.Id,
Message: "message",
})
require.NoError(t, err)
post3.ReplyCount = 1
time.Sleep(time.Millisecond)
post4, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
RootId: post2.Id,
Message: "message",
})
require.NoError(t, err)
post4.ReplyCount = 2
time.Sleep(time.Millisecond)
post5, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post6, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
RootId: post2.Id,
Message: "message",
})
post6.ReplyCount = 2
require.NoError(t, err)
// Adding a post to a thread changes the UpdateAt timestamp of the parent post
post1.UpdateAt = post3.UpdateAt
post2.UpdateAt = post6.UpdateAt
t.Run("should return each post and thread before a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(model.GetPostsOptions{ChannelId: channelId, PostId: post4.Id, PerPage: 2}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{post3.Id, post2.Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
post1.Id: post1,
post2.Id: post2,
post3.Id: post3,
post4.Id: post4,
post6.Id: post6,
}, postList.Posts)
})
t.Run("should return each post and the root of each thread after a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsAfter(model.GetPostsOptions{ChannelId: channelId, PostId: post4.Id, PerPage: 2}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{post6.Id, post5.Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
post2.Id: post2,
post4.Id: post4,
post5.Id: post5,
post6.Id: post6,
}, postList.Posts)
})
})
t.Run("with threads (skipFetchThreads)", func(t *testing.T) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelId := channel1.Id
userId := model.NewId()
// This creates a series of posts that looks like:
// post1
// post2
// post3 (in response to post1)
// post4 (in response to post2)
// post5
// post6 (in response to post2)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "post1",
})
require.NoError(t, err)
post1.ReplyCount = 1
time.Sleep(time.Millisecond)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "post2",
})
require.NoError(t, err)
post2.ReplyCount = 2
time.Sleep(time.Millisecond)
post3, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
RootId: post1.Id,
Message: "post3",
})
require.NoError(t, err)
post3.ReplyCount = 1
time.Sleep(time.Millisecond)
post4, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
RootId: post2.Id,
Message: "post4",
})
require.NoError(t, err)
post4.ReplyCount = 2
time.Sleep(time.Millisecond)
post5, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "post5",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post6, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
RootId: post2.Id,
Message: "post6",
})
post6.ReplyCount = 2
require.NoError(t, err)
// Adding a post to a thread changes the UpdateAt timestamp of the parent post
post1.UpdateAt = post3.UpdateAt
post2.UpdateAt = post6.UpdateAt
t.Run("should return each post and thread before a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(model.GetPostsOptions{ChannelId: channelId, PostId: post4.Id, PerPage: 2, SkipFetchThreads: true}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{post3.Id, post2.Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
post1.Id: post1,
post2.Id: post2,
post3.Id: post3,
}, postList.Posts)
})
t.Run("should return each post and thread before a post with limit", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(model.GetPostsOptions{ChannelId: channelId, PostId: post4.Id, PerPage: 1, SkipFetchThreads: true}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{post3.Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
post1.Id: post1,
post3.Id: post3,
}, postList.Posts)
})
t.Run("should return each post and the root of each thread after a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsAfter(model.GetPostsOptions{ChannelId: channelId, PostId: post4.Id, PerPage: 2, SkipFetchThreads: true}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{post6.Id, post5.Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
post2.Id: post2,
post5.Id: post5,
post6.Id: post6,
}, postList.Posts)
})
})
t.Run("with threads (collapsedThreads)", func(t *testing.T) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelId := channel1.Id
userId := model.NewId()
// This creates a series of posts that looks like:
// post1
// post2
// post3 (in response to post1)
// post4 (in response to post2)
// post5
// post6 (in response to post2)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "post1",
})
require.NoError(t, err)
post1.ReplyCount = 1
time.Sleep(time.Millisecond)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "post2",
})
require.NoError(t, err)
post2.ReplyCount = 2
time.Sleep(time.Millisecond)
post3, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
RootId: post1.Id,
Message: "post3",
})
require.NoError(t, err)
post3.ReplyCount = 1
time.Sleep(time.Millisecond)
post4, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
RootId: post2.Id,
Message: "post4",
})
require.NoError(t, err)
post4.ReplyCount = 2
time.Sleep(time.Millisecond)
post5, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "post5",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post6, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
RootId: post2.Id,
Message: "post6",
})
post6.ReplyCount = 2
require.NoError(t, err)
// Adding a post to a thread changes the UpdateAt timestamp of the parent post
post1.UpdateAt = post3.UpdateAt
post2.UpdateAt = post6.UpdateAt
t.Run("should return each root post before a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(model.GetPostsOptions{ChannelId: channelId, PostId: post4.Id, PerPage: 2, CollapsedThreads: true}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{post2.Id, post1.Id}, postList.Order)
})
t.Run("should return each root post before a post with limit", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(model.GetPostsOptions{ChannelId: channelId, PostId: post4.Id, PerPage: 1, CollapsedThreads: true}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{post2.Id}, postList.Order)
})
t.Run("should return each root after a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsAfter(model.GetPostsOptions{ChannelId: channelId, PostId: post4.Id, PerPage: 2, CollapsedThreads: true}, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, []string{post5.Id}, postList.Order)
})
})
}
func testPostStoreGetPostsSince(t *testing.T, ss store.Store) {
t.Run("should return posts created after the given time", func(t *testing.T) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelId := channel1.Id
userId := model.NewId()
post1, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
_, err = ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post3, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post4, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post5, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
RootId: post3.Id,
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post6, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
RootId: post1.Id,
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
postList, err := ss.Post().GetPostsSince(model.GetPostsSinceOptions{ChannelId: channelId, Time: post3.CreateAt}, false, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, []string{
post6.Id,
post5.Id,
post4.Id,
post3.Id,
post1.Id,
}, postList.Order)
assert.Len(t, postList.Posts, 5)
assert.NotNil(t, postList.Posts[post1.Id], "should return the parent post")
assert.NotNil(t, postList.Posts[post3.Id])
assert.NotNil(t, postList.Posts[post4.Id])
assert.NotNil(t, postList.Posts[post5.Id])
assert.NotNil(t, postList.Posts[post6.Id])
})
t.Run("should return empty list when nothing has changed", func(t *testing.T) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelId := channel1.Id
userId := model.NewId()
post1, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
postList, err := ss.Post().GetPostsSince(model.GetPostsSinceOptions{ChannelId: channelId, Time: post1.CreateAt}, false, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{}, postList.Order)
assert.Empty(t, postList.Posts)
})
t.Run("should not cache a timestamp of 0 when nothing has changed", func(t *testing.T) {
ss.Post().ClearCaches()
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelId := channel1.Id
userId := model.NewId()
post1, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
// Make a request that returns no results
postList, err := ss.Post().GetPostsSince(model.GetPostsSinceOptions{ChannelId: channelId, Time: post1.CreateAt}, true, map[string]bool{})
require.NoError(t, err)
require.Equal(t, model.NewPostList(), postList)
// And then ensure that it doesn't cause future requests to also return no results
postList, err = ss.Post().GetPostsSince(model.GetPostsSinceOptions{ChannelId: channelId, Time: post1.CreateAt - 1}, true, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, []string{post1.Id}, postList.Order)
assert.Len(t, postList.Posts, 1)
assert.NotNil(t, postList.Posts[post1.Id])
})
}
func testPostStoreGetPosts(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelId := channel1.Id
userId := model.NewId()
post1, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post3, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post4, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post5, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
RootId: post3.Id,
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post6, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
RootId: post1.Id,
})
require.NoError(t, err)
t.Run("should return the last posts created in a channel", func(t *testing.T) {
postList, err := ss.Post().GetPosts(model.GetPostsOptions{ChannelId: channelId, Page: 0, PerPage: 30, SkipFetchThreads: false}, false, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{
post6.Id,
post5.Id,
post4.Id,
post3.Id,
post2.Id,
post1.Id,
}, postList.Order)
assert.Len(t, postList.Posts, 6)
assert.NotNil(t, postList.Posts[post1.Id])
assert.NotNil(t, postList.Posts[post2.Id])
assert.NotNil(t, postList.Posts[post3.Id])
assert.NotNil(t, postList.Posts[post4.Id])
assert.NotNil(t, postList.Posts[post5.Id])
assert.NotNil(t, postList.Posts[post6.Id])
})
t.Run("should return the last posts created in a channel and the threads and the reply count must be 0", func(t *testing.T) {
postList, err := ss.Post().GetPosts(model.GetPostsOptions{ChannelId: channelId, Page: 0, PerPage: 2, SkipFetchThreads: false}, false, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{
post6.Id,
post5.Id,
}, postList.Order)
assert.Len(t, postList.Posts, 4)
require.NotNil(t, postList.Posts[post1.Id])
require.NotNil(t, postList.Posts[post3.Id])
require.NotNil(t, postList.Posts[post5.Id])
require.NotNil(t, postList.Posts[post6.Id])
assert.Equal(t, int64(0), postList.Posts[post1.Id].ReplyCount)
assert.Equal(t, int64(0), postList.Posts[post3.Id].ReplyCount)
assert.Equal(t, int64(0), postList.Posts[post5.Id].ReplyCount)
assert.Equal(t, int64(0), postList.Posts[post6.Id].ReplyCount)
})
t.Run("should return the last posts created in a channel without the threads and the reply count must be correct", func(t *testing.T) {
postList, err := ss.Post().GetPosts(model.GetPostsOptions{ChannelId: channelId, Page: 0, PerPage: 2, SkipFetchThreads: true}, false, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, []string{
post6.Id,
post5.Id,
}, postList.Order)
assert.Len(t, postList.Posts, 4)
assert.NotNil(t, postList.Posts[post5.Id])
assert.NotNil(t, postList.Posts[post6.Id])
assert.Equal(t, int64(1), postList.Posts[post5.Id].ReplyCount)
assert.Equal(t, int64(1), postList.Posts[post6.Id].ReplyCount)
})
t.Run("should return all posts in a channel included deleted posts", func(t *testing.T) {
err := ss.Post().Delete(post1.Id, 1, userId)
require.NoError(t, err)
postList, err := ss.Post().GetPosts(model.GetPostsOptions{ChannelId: channelId, Page: 0, PerPage: 30, SkipFetchThreads: false, IncludeDeleted: true}, false, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, []string{
post6.Id,
post5.Id,
post4.Id,
post3.Id,
post2.Id,
post1.Id,
}, postList.Order)
assert.Len(t, postList.Posts, 6)
assert.NotNil(t, postList.Posts[post1.Id])
assert.NotNil(t, postList.Posts[post2.Id])
assert.NotNil(t, postList.Posts[post3.Id])
assert.NotNil(t, postList.Posts[post4.Id])
assert.NotNil(t, postList.Posts[post5.Id])
assert.NotNil(t, postList.Posts[post6.Id])
})
t.Run("should return all posts in a channel included deleted posts without threads", func(t *testing.T) {
err := ss.Post().Delete(post5.Id, 1, userId)
require.NoError(t, err)
postList, err := ss.Post().GetPosts(model.GetPostsOptions{ChannelId: channelId, Page: 0, PerPage: 30, SkipFetchThreads: true, IncludeDeleted: true}, false, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, []string{
post6.Id,
post5.Id,
post4.Id,
post3.Id,
post2.Id,
post1.Id,
}, postList.Order)
assert.Len(t, postList.Posts, 6)
assert.NotNil(t, postList.Posts[post5.Id])
assert.NotNil(t, postList.Posts[post6.Id])
assert.Equal(t, int64(1), postList.Posts[post5.Id].ReplyCount)
assert.Equal(t, int64(1), postList.Posts[post6.Id].ReplyCount)
})
t.Run("should return the lasts posts created in channel without include deleted posts", func(t *testing.T) {
err := ss.Post().Delete(post6.Id, 1, userId)
require.NoError(t, err)
postList, err := ss.Post().GetPosts(model.GetPostsOptions{ChannelId: channelId, Page: 0, PerPage: 30, SkipFetchThreads: true, IncludeDeleted: false}, false, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, []string{
post4.Id,
post3.Id,
post2.Id,
}, postList.Order)
assert.Len(t, postList.Posts, 3)
assert.NotNil(t, postList.Posts[post2.Id])
assert.NotNil(t, postList.Posts[post3.Id])
assert.NotNil(t, postList.Posts[post4.Id])
})
}
func testPostStoreGetPostBeforeAfter(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelId := channel1.Id
o0 := &model.Post{}
o0.ChannelId = channelId
o0.UserId = model.NewId()
o0.Message = NewTestId()
_, err = ss.Post().Save(o0)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o1 := &model.Post{}
o1.ChannelId = channelId
o1.Type = model.PostTypeJoinChannel
o1.UserId = model.NewId()
o1.Message = "system_join_channel message"
_, err = ss.Post().Save(o1)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o0a := &model.Post{}
o0a.ChannelId = channelId
o0a.UserId = model.NewId()
o0a.Message = NewTestId()
o0a.RootId = o1.Id
_, err = ss.Post().Save(o0a)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o0b := &model.Post{}
o0b.ChannelId = channelId
o0b.UserId = model.NewId()
o0b.Message = "deleted message"
o0b.RootId = o1.Id
o0b.DeleteAt = 1
_, err = ss.Post().Save(o0b)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
otherChannelPost := &model.Post{}
otherChannelPost.ChannelId = channel2.Id
otherChannelPost.UserId = model.NewId()
otherChannelPost.Message = NewTestId()
_, err = ss.Post().Save(otherChannelPost)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o2 := &model.Post{}
o2.ChannelId = channelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
_, err = ss.Post().Save(o2)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o2a := &model.Post{}
o2a.ChannelId = channelId
o2a.UserId = model.NewId()
o2a.Message = NewTestId()
o2a.RootId = o2.Id
_, err = ss.Post().Save(o2a)
require.NoError(t, err)
rPostId1, err := ss.Post().GetPostIdBeforeTime(channelId, o0a.CreateAt, false)
require.Equal(t, rPostId1, o1.Id, "should return before post o1")
require.NoError(t, err)
rPostId1, err = ss.Post().GetPostIdAfterTime(channelId, o0b.CreateAt, false)
require.Equal(t, rPostId1, o2.Id, "should return before post o2")
require.NoError(t, err)
rPost1, err := ss.Post().GetPostAfterTime(channelId, o0b.CreateAt, false)
require.Equal(t, rPost1.Id, o2.Id, "should return before post o2")
require.NoError(t, err)
rPostId2, err := ss.Post().GetPostIdBeforeTime(channelId, o0.CreateAt, false)
require.Empty(t, rPostId2, "should return no post")
require.NoError(t, err)
rPostId2, err = ss.Post().GetPostIdAfterTime(channelId, o0.CreateAt, false)
require.Equal(t, rPostId2, o1.Id, "should return before post o1")
require.NoError(t, err)
rPost2, err := ss.Post().GetPostAfterTime(channelId, o0.CreateAt, false)
require.Equal(t, rPost2.Id, o1.Id, "should return before post o1")
require.NoError(t, err)
rPostId3, err := ss.Post().GetPostIdBeforeTime(channelId, o2a.CreateAt, false)
require.Equal(t, rPostId3, o2.Id, "should return before post o2")
require.NoError(t, err)
rPostId3, err = ss.Post().GetPostIdAfterTime(channelId, o2a.CreateAt, false)
require.Empty(t, rPostId3, "should return no post")
require.NoError(t, err)
rPost3, err := ss.Post().GetPostAfterTime(channelId, o2a.CreateAt, false)
require.Empty(t, rPost3.Id, "should return no post")
require.NoError(t, err)
}
func testUserCountsWithPostsByDay(t *testing.T, ss store.Store) {
t1 := &model.Team{}
t1.DisplayName = "DisplayName"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
t1, err := ss.Team().Save(t1)
require.NoError(t, err)
c1 := &model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel2"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, nErr := ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.CreateAt = utils.MillisFromTime(utils.Yesterday())
o1.Message = NewTestId()
o1, nErr = ss.Post().Save(o1)
require.NoError(t, nErr)
o1a := &model.Post{}
o1a.ChannelId = c1.Id
o1a.UserId = model.NewId()
o1a.CreateAt = o1.CreateAt
o1a.Message = NewTestId()
_, nErr = ss.Post().Save(o1a)
require.NoError(t, nErr)
o2 := &model.Post{}
o2.ChannelId = c1.Id
o2.UserId = model.NewId()
o2.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24)
o2.Message = NewTestId()
o2, nErr = ss.Post().Save(o2)
require.NoError(t, nErr)
o2a := &model.Post{}
o2a.ChannelId = c1.Id
o2a.UserId = o2.UserId
o2a.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24)
o2a.Message = NewTestId()
_, nErr = ss.Post().Save(o2a)
require.NoError(t, nErr)
r1, err := ss.Post().AnalyticsUserCountsWithPostsByDay(t1.Id)
require.NoError(t, err)
row1 := r1[0]
require.Equal(t, float64(2), row1.Value, "wrong value")
row2 := r1[1]
require.Equal(t, float64(1), row2.Value, "wrong value")
}
func testPostCountsByDay(t *testing.T, ss store.Store) {
t1 := &model.Team{}
t1.DisplayName = "DisplayName"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
t1, err := ss.Team().Save(t1)
require.NoError(t, err)
c1 := &model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel2"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, nErr := ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.CreateAt = utils.MillisFromTime(utils.Yesterday())
o1.Message = NewTestId()
o1.Hashtags = "hashtag"
o1, nErr = ss.Post().Save(o1)
require.NoError(t, nErr)
o1a := &model.Post{}
o1a.ChannelId = c1.Id
o1a.UserId = model.NewId()
o1a.CreateAt = o1.CreateAt
o1a.Message = NewTestId()
o1a.FileIds = []string{"fileId1"}
_, nErr = ss.Post().Save(o1a)
require.NoError(t, nErr)
o2 := &model.Post{}
o2.ChannelId = c1.Id
o2.UserId = model.NewId()
o2.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24 * 2)
o2.Message = NewTestId()
o2.Filenames = []string{"filename1"}
o2, nErr = ss.Post().Save(o2)
require.NoError(t, nErr)
o2a := &model.Post{}
o2a.ChannelId = c1.Id
o2a.UserId = o2.UserId
o2a.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24 * 2)
o2a.Message = NewTestId()
o2a.Hashtags = "hashtag"
o2a.FileIds = []string{"fileId2"}
_, nErr = ss.Post().Save(o2a)
require.NoError(t, nErr)
bot1 := &model.Bot{
Username: "username",
Description: "a bot",
OwnerId: model.NewId(),
UserId: model.NewId(),
}
_, nErr = ss.Bot().Save(bot1)
require.NoError(t, nErr)
b1 := &model.Post{}
b1.Message = "bot message one"
b1.ChannelId = c1.Id
b1.UserId = bot1.UserId
b1.CreateAt = utils.MillisFromTime(utils.Yesterday())
_, nErr = ss.Post().Save(b1)
require.NoError(t, nErr)
b1a := &model.Post{}
b1a.Message = "bot message two"
b1a.ChannelId = c1.Id
b1a.UserId = bot1.UserId
b1a.CreateAt = utils.MillisFromTime(utils.Yesterday()) - (1000 * 60 * 60 * 24 * 2)
_, nErr = ss.Post().Save(b1a)
require.NoError(t, nErr)
time.Sleep(1 * time.Second)
// summary of posts
// yesterday - 2 non-bot user posts, 1 bot user post
// 3 days ago - 2 non-bot user posts, 1 bot user post
// last 31 days, all users (including bots)
postCountsOptions := &model.AnalyticsPostCountsOptions{TeamId: t1.Id, BotsOnly: false, YesterdayOnly: false}
r1, err := ss.Post().AnalyticsPostCountsByDay(postCountsOptions)
require.NoError(t, err)
assert.Equal(t, float64(3), r1[0].Value)
assert.Equal(t, float64(3), r1[1].Value)
// last 31 days, bots only
postCountsOptions = &model.AnalyticsPostCountsOptions{TeamId: t1.Id, BotsOnly: true, YesterdayOnly: false}
r1, err = ss.Post().AnalyticsPostCountsByDay(postCountsOptions)
require.NoError(t, err)
assert.Equal(t, float64(1), r1[0].Value)
assert.Equal(t, float64(1), r1[1].Value)
// yesterday only, all users (including bots)
postCountsOptions = &model.AnalyticsPostCountsOptions{TeamId: t1.Id, BotsOnly: false, YesterdayOnly: true}
r1, err = ss.Post().AnalyticsPostCountsByDay(postCountsOptions)
require.NoError(t, err)
assert.Equal(t, float64(3), r1[0].Value)
// yesterday only, bots only
postCountsOptions = &model.AnalyticsPostCountsOptions{TeamId: t1.Id, BotsOnly: true, YesterdayOnly: true}
r1, err = ss.Post().AnalyticsPostCountsByDay(postCountsOptions)
require.NoError(t, err)
assert.Equal(t, float64(1), r1[0].Value)
}
func testPostCounts(t *testing.T, ss store.Store) {
now := time.Now()
twentyMinAgo := now.Add(-20 * time.Minute).UnixMilli()
fifteenMinAgo := now.Add(-15 * time.Minute).UnixMilli()
tenMinAgo := now.Add(-10 * time.Minute).UnixMilli()
t1 := &model.Team{}
t1.DisplayName = "DisplayName"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
t1, err := ss.Team().Save(t1)
require.NoError(t, err)
c1 := &model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel2"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, nErr := ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
// system post
p1 := &model.Post{}
p1.Type = "system_add_to_channel"
p1.ChannelId = c1.Id
p1.UserId = model.NewId()
p1.Message = NewTestId()
p1.CreateAt = twentyMinAgo
p1.UpdateAt = twentyMinAgo
_, nErr = ss.Post().Save(p1)
require.NoError(t, nErr)
p2 := &model.Post{}
p2.ChannelId = c1.Id
p2.UserId = model.NewId()
p2.Message = NewTestId()
p2.Hashtags = "hashtag"
p2.CreateAt = twentyMinAgo
p2.UpdateAt = twentyMinAgo
p2, nErr = ss.Post().Save(p2)
require.NoError(t, nErr)
p3 := &model.Post{}
p3.ChannelId = c1.Id
p3.UserId = model.NewId()
p3.Message = NewTestId()
p3.FileIds = []string{"fileId1"}
p3.CreateAt = twentyMinAgo
p3.UpdateAt = twentyMinAgo
_, nErr = ss.Post().Save(p3)
require.NoError(t, nErr)
p4 := &model.Post{}
p4.ChannelId = c1.Id
p4.UserId = model.NewId()
p4.Message = NewTestId()
p4.Filenames = []string{"filename1"}
p4.CreateAt = tenMinAgo
p4.UpdateAt = tenMinAgo
p4, nErr = ss.Post().Save(p4)
require.NoError(t, nErr)
p5 := &model.Post{}
p5.ChannelId = c1.Id
p5.UserId = p4.UserId
p5.Message = NewTestId()
p5.Hashtags = "hashtag"
p5.FileIds = []string{"fileId2"}
p5.CreateAt = tenMinAgo
p5.UpdateAt = tenMinAgo
_, nErr = ss.Post().Save(p5)
require.NoError(t, nErr)
bot1 := &model.Bot{
Username: "username",
Description: "a bot",
OwnerId: model.NewId(),
UserId: model.NewId(),
}
_, nErr = ss.Bot().Save(bot1)
require.NoError(t, nErr)
p6 := &model.Post{}
p6.Message = "bot message one"
p6.ChannelId = c1.Id
p6.UserId = bot1.UserId
p6.CreateAt = twentyMinAgo
p6.UpdateAt = twentyMinAgo
_, nErr = ss.Post().Save(p6)
require.NoError(t, nErr)
p7 := &model.Post{}
p7.Message = "bot message two"
p7.ChannelId = c1.Id
p7.UserId = bot1.UserId
p7.CreateAt = tenMinAgo
p7.UpdateAt = tenMinAgo
_, nErr = ss.Post().Save(p7)
require.NoError(t, nErr)
// total across all teams
c, err := ss.Post().AnalyticsPostCount(&model.PostCountOptions{})
require.NoError(t, err)
assert.GreaterOrEqual(t, c, int64(7))
// total for single team
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id})
require.NoError(t, err)
assert.Equal(t, int64(7), c)
// with files
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, MustHaveFile: true})
require.NoError(t, err)
assert.Equal(t, int64(3), c)
// with hashtags
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, MustHaveHashtag: true})
require.NoError(t, err)
assert.Equal(t, int64(2), c)
// with hashtags and files
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, MustHaveFile: true, MustHaveHashtag: true})
require.NoError(t, err)
assert.Equal(t, int64(1), c)
// excluding system posts
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, ExcludeSystemPosts: true})
require.NoError(t, err)
assert.Equal(t, int64(6), c)
// before update_at time
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, SinceUpdateAt: fifteenMinAgo})
require.NoError(t, err)
assert.Equal(t, int64(3), c)
// equal to update_at time
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, SinceUpdateAt: tenMinAgo})
require.NoError(t, err)
assert.Equal(t, int64(3), c)
// since update_at and since post id
tenMinAgoIDs := []string{p4.Id, p5.Id, p7.Id}
sort.Strings(tenMinAgoIDs)
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, SinceUpdateAt: tenMinAgo, SincePostID: tenMinAgoIDs[0]})
require.NoError(t, err)
assert.Equal(t, int64(2), c)
// delete 1 post
err = ss.Post().Delete(p2.Id, 1, p2.UserId)
require.NoError(t, err)
// total for single team with the deleted post excluded
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, ExcludeDeleted: true})
require.NoError(t, err)
assert.Equal(t, int64(6), c)
// total users only posts for single team with the deleted post excluded
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, ExcludeDeleted: true, UsersPostsOnly: true})
require.NoError(t, err)
assert.Equal(t, int64(3), c)
}
func testPostStoreGetFlaggedPostsForTeam(t *testing.T, ss store.Store, s SqlStore) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, err := ss.Channel().Save(c1, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3.DeleteAt = 1
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
m0 := &model.ChannelMember{}
m0.ChannelId = c1.Id
m0.UserId = o1.UserId
m0.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(m0)
require.NoError(t, err)
teamId := model.NewId()
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o4 := &model.Post{}
o4.ChannelId = channel2.Id
o4.UserId = model.NewId()
o4.Message = NewTestId()
o4, err = ss.Post().Save(o4)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
c2 := &model.Channel{}
c2.DisplayName = "DMChannel1"
c2.Name = NewTestId()
c2.Type = model.ChannelTypeDirect
m1 := &model.ChannelMember{}
m1.ChannelId = c2.Id
m1.UserId = o1.UserId
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := &model.ChannelMember{}
m2.ChannelId = c2.Id
m2.UserId = model.NewId()
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
c2, err = ss.Channel().SaveDirectChannel(c2, m1, m2)
require.NoError(t, err)
o5 := &model.Post{}
o5.ChannelId = c2.Id
o5.UserId = m2.UserId
o5.Message = NewTestId()
o5, err = ss.Post().Save(o5)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
// Post on channel where user is not a member
channel3, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName3",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o6 := &model.Post{}
o6.ChannelId = channel3.Id
o6.UserId = m2.UserId
o6.Message = NewTestId()
o6, err = ss.Post().Save(o6)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
r1, err := ss.Post().GetFlaggedPosts(o1.ChannelId, 0, 2)
require.NoError(t, err)
require.Empty(t, r1.Order, "should be empty")
preferences := model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o1.Id,
Value: "true",
},
}
err = ss.Preference().Save(preferences)
require.NoError(t, err)
r2, err := ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 0, 2)
require.NoError(t, err)
require.Len(t, r2.Order, 1, "should have 1 post")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o2.Id,
Value: "true",
},
}
err = ss.Preference().Save(preferences)
require.NoError(t, err)
r3, err := ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 0, 1)
require.NoError(t, err)
require.Len(t, r3.Order, 1, "should have 1 post")
r3, err = ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 1, 1)
require.NoError(t, err)
require.Len(t, r3.Order, 1, "should have 1 post")
r3, err = ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 1000, 10)
require.NoError(t, err)
require.Empty(t, r3.Order, "should be empty")
r4, err := ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 0, 2)
require.NoError(t, err)
require.Len(t, r4.Order, 2, "should have 2 posts")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o3.Id,
Value: "true",
},
}
err = ss.Preference().Save(preferences)
require.NoError(t, err)
r4, err = ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 0, 2)
require.NoError(t, err)
require.Len(t, r4.Order, 2, "should have 2 posts")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o4.Id,
Value: "true",
},
}
err = ss.Preference().Save(preferences)
require.NoError(t, err)
r4, err = ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 0, 2)
require.NoError(t, err)
require.Len(t, r4.Order, 2, "should have 2 posts")
r4, err = ss.Post().GetFlaggedPostsForTeam(o1.UserId, model.NewId(), 0, 2)
require.NoError(t, err)
require.Empty(t, r4.Order, "should have 0 posts")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o5.Id,
Value: "true",
},
}
err = ss.Preference().Save(preferences)
require.NoError(t, err)
r4, err = ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 0, 10)
require.NoError(t, err)
require.Len(t, r4.Order, 3, "should have 3 posts")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o6.Id,
Value: "true",
},
}
err = ss.Preference().Save(preferences)
require.NoError(t, err)
r4, err = ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 0, 10)
require.NoError(t, err)
require.Len(t, r4.Order, 3, "should have 3 posts")
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testPostStoreGetFlaggedPosts(t *testing.T, ss store.Store) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, err := ss.Channel().Save(c1, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3.DeleteAt = 1
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
// Post on channel where user is not a member
teamId := model.NewId()
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o4 := &model.Post{}
o4.ChannelId = channel2.Id
o4.UserId = model.NewId()
o4.Message = NewTestId()
o4, err = ss.Post().Save(o4)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
m0 := &model.ChannelMember{}
m0.ChannelId = o1.ChannelId
m0.UserId = o1.UserId
m0.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(m0)
require.NoError(t, err)
r1, err := ss.Post().GetFlaggedPosts(o1.UserId, 0, 2)
require.NoError(t, err)
require.Empty(t, r1.Order, "should be empty")
preferences := model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o1.Id,
Value: "true",
},
}
nErr := ss.Preference().Save(preferences)
require.NoError(t, nErr)
r2, err := ss.Post().GetFlaggedPosts(o1.UserId, 0, 2)
require.NoError(t, err)
require.Len(t, r2.Order, 1, "should have 1 post")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o2.Id,
Value: "true",
},
}
nErr = ss.Preference().Save(preferences)
require.NoError(t, nErr)
r3, err := ss.Post().GetFlaggedPosts(o1.UserId, 0, 1)
require.NoError(t, err)
require.Len(t, r3.Order, 1, "should have 1 post")
r3, err = ss.Post().GetFlaggedPosts(o1.UserId, 1, 1)
require.NoError(t, err)
require.Len(t, r3.Order, 1, "should have 1 post")
r3, err = ss.Post().GetFlaggedPosts(o1.UserId, 1000, 10)
require.NoError(t, err)
require.Empty(t, r3.Order, "should be empty")
r4, err := ss.Post().GetFlaggedPosts(o1.UserId, 0, 2)
require.NoError(t, err)
require.Len(t, r4.Order, 2, "should have 2 posts")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o3.Id,
Value: "true",
},
}
nErr = ss.Preference().Save(preferences)
require.NoError(t, nErr)
r4, err = ss.Post().GetFlaggedPosts(o1.UserId, 0, 2)
require.NoError(t, err)
require.Len(t, r4.Order, 2, "should have 2 posts")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o4.Id,
Value: "true",
},
}
nErr = ss.Preference().Save(preferences)
require.NoError(t, nErr)
r4, err = ss.Post().GetFlaggedPosts(o1.UserId, 0, 2)
require.NoError(t, err)
require.Len(t, r4.Order, 2, "should have 2 posts")
}
func testPostStoreGetFlaggedPostsForChannel(t *testing.T, ss store.Store) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, err := ss.Channel().Save(c1, -1)
require.NoError(t, err)
c2 := &model.Channel{}
c2.TeamId = model.NewId()
c2.DisplayName = "Channel2"
c2.Name = NewTestId()
c2.Type = model.ChannelTypeOpen
c2, err = ss.Channel().Save(c2, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
// deleted post
teamId := model.NewId()
channel3, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName3",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = channel3.Id
o3.UserId = o1.ChannelId
o3.Message = NewTestId()
o3.DeleteAt = 1
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o4 := &model.Post{}
o4.ChannelId = c2.Id
o4.UserId = model.NewId()
o4.Message = NewTestId()
o4, err = ss.Post().Save(o4)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
// Post on channel where user is not a member
channel4, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName4",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o5 := &model.Post{}
o5.ChannelId = channel4.Id
o5.UserId = model.NewId()
o5.Message = NewTestId()
o5, err = ss.Post().Save(o5)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
m1 := &model.ChannelMember{}
m1.ChannelId = o1.ChannelId
m1.UserId = o1.UserId
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(m1)
require.NoError(t, err)
m2 := &model.ChannelMember{}
m2.ChannelId = o4.ChannelId
m2.UserId = o1.UserId
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(m2)
require.NoError(t, err)
r, err := ss.Post().GetFlaggedPostsForChannel(o1.UserId, o1.ChannelId, 0, 10)
require.NoError(t, err)
require.Empty(t, r.Order, "should be empty")
preference := model.Preference{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o1.Id,
Value: "true",
}
nErr := ss.Preference().Save(model.Preferences{preference})
require.NoError(t, nErr)
r, err = ss.Post().GetFlaggedPostsForChannel(o1.UserId, o1.ChannelId, 0, 10)
require.NoError(t, err)
require.Len(t, r.Order, 1, "should have 1 post")
preference.Name = o2.Id
nErr = ss.Preference().Save(model.Preferences{preference})
require.NoError(t, nErr)
preference.Name = o3.Id
nErr = ss.Preference().Save(model.Preferences{preference})
require.NoError(t, nErr)
r, err = ss.Post().GetFlaggedPostsForChannel(o1.UserId, o1.ChannelId, 0, 1)
require.NoError(t, err)
require.Len(t, r.Order, 1, "should have 1 post")
r, err = ss.Post().GetFlaggedPostsForChannel(o1.UserId, o1.ChannelId, 1, 1)
require.NoError(t, err)
require.Len(t, r.Order, 1, "should have 1 post")
r, err = ss.Post().GetFlaggedPostsForChannel(o1.UserId, o1.ChannelId, 1000, 10)
require.NoError(t, err)
require.Empty(t, r.Order, "should be empty")
r, err = ss.Post().GetFlaggedPostsForChannel(o1.UserId, o1.ChannelId, 0, 10)
require.NoError(t, err)
require.Len(t, r.Order, 2, "should have 2 posts")
preference.Name = o4.Id
nErr = ss.Preference().Save(model.Preferences{preference})
require.NoError(t, nErr)
r, err = ss.Post().GetFlaggedPostsForChannel(o1.UserId, o4.ChannelId, 0, 10)
require.NoError(t, err)
require.Len(t, r.Order, 1, "should have 1 posts")
preference.Name = o5.Id
nErr = ss.Preference().Save(model.Preferences{preference})
require.NoError(t, nErr)
r, err = ss.Post().GetFlaggedPostsForChannel(o1.UserId, o5.ChannelId, 0, 10)
require.NoError(t, err)
require.Len(t, r.Order, 0, "should have 0 posts")
}
func testPostStoreGetPostsCreatedAt(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
createTime := model.GetMillis() + 1
o0 := &model.Post{}
o0.ChannelId = channel1.Id
o0.UserId = model.NewId()
o0.Message = NewTestId()
o0.CreateAt = createTime
o0, err = ss.Post().Save(o0)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = o0.ChannelId
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1.CreateAt = createTime
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.RootId = o1.Id
o2.CreateAt = createTime + 1
_, err = ss.Post().Save(o2)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = channel2.Id
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3.CreateAt = createTime
_, err = ss.Post().Save(o3)
require.NoError(t, err)
r1, _ := ss.Post().GetPostsCreatedAt(o1.ChannelId, createTime)
assert.Equal(t, 2, len(r1))
}
func testPostStoreOverwriteMultiple(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.RootId = o1.Id
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o4, err := ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: model.NewId(),
Message: model.NewId(),
Filenames: []string{"test"},
})
require.NoError(t, err)
channel3, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName3",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o5, err := ss.Post().Save(&model.Post{
ChannelId: channel3.Id,
UserId: model.NewId(),
Message: model.NewId(),
Filenames: []string{"test2", "test3"},
})
require.NoError(t, err)
r1, err := ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro1 := r1.Posts[o1.Id]
r2, err := ss.Post().Get(context.Background(), o2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro2 := r2.Posts[o2.Id]
r3, err := ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro3 := r3.Posts[o3.Id]
r4, err := ss.Post().Get(context.Background(), o4.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro4 := r4.Posts[o4.Id]
r5, err := ss.Post().Get(context.Background(), o5.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro5 := r5.Posts[o5.Id]
require.Equal(t, ro1.Message, o1.Message, "Failed to save/get")
require.Equal(t, ro2.Message, o2.Message, "Failed to save/get")
require.Equal(t, ro3.Message, o3.Message, "Failed to save/get")
require.Equal(t, ro4.Message, o4.Message, "Failed to save/get")
require.Equal(t, ro4.Filenames, o4.Filenames, "Failed to save/get")
require.Equal(t, ro5.Message, o5.Message, "Failed to save/get")
require.Equal(t, ro5.Filenames, o5.Filenames, "Failed to save/get")
t.Run("overwrite changing message", func(t *testing.T) {
o1a := ro1.Clone()
o1a.Message = ro1.Message + "BBBBBBBBBB"
o2a := ro2.Clone()
o2a.Message = ro2.Message + "DDDDDDD"
o3a := ro3.Clone()
o3a.Message = ro3.Message + "WWWWWWW"
_, errIdx, err := ss.Post().OverwriteMultiple([]*model.Post{o1a, o2a, o3a})
require.NoError(t, err)
require.Equal(t, -1, errIdx)
r1, nErr := ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, nErr)
ro1a := r1.Posts[o1.Id]
r2, nErr = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, nErr)
ro2a := r2.Posts[o2.Id]
r3, nErr = ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, nErr)
ro3a := r3.Posts[o3.Id]
assert.Equal(t, ro1a.Message, o1a.Message, "Failed to overwrite/get")
assert.Equal(t, ro2a.Message, o2a.Message, "Failed to overwrite/get")
assert.Equal(t, ro3a.Message, o3a.Message, "Failed to overwrite/get")
})
t.Run("overwrite clearing filenames", func(t *testing.T) {
o4a := ro4.Clone()
o4a.Filenames = []string{}
o4a.FileIds = []string{model.NewId()}
o5a := ro5.Clone()
o5a.Filenames = []string{}
o5a.FileIds = []string{}
_, errIdx, err := ss.Post().OverwriteMultiple([]*model.Post{o4a, o5a})
require.NoError(t, err)
require.Equal(t, -1, errIdx)
r4, nErr := ss.Post().Get(context.Background(), o4.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, nErr)
ro4a := r4.Posts[o4.Id]
r5, nErr = ss.Post().Get(context.Background(), o5.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, nErr)
ro5a := r5.Posts[o5.Id]
require.Empty(t, ro4a.Filenames, "Failed to clear Filenames")
require.Len(t, ro4a.FileIds, 1, "Failed to set FileIds")
require.Empty(t, ro5a.Filenames, "Failed to clear Filenames")
require.Empty(t, ro5a.FileIds, "Failed to set FileIds")
})
}
func testPostStoreOverwrite(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.RootId = o1.Id
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o4, err := ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: model.NewId(),
Message: model.NewId(),
Filenames: []string{"test"},
})
require.NoError(t, err)
r1, err := ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro1 := r1.Posts[o1.Id]
r2, err := ss.Post().Get(context.Background(), o2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro2 := r2.Posts[o2.Id]
r3, err := ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro3 := r3.Posts[o3.Id]
r4, err := ss.Post().Get(context.Background(), o4.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro4 := r4.Posts[o4.Id]
require.Equal(t, ro1.Message, o1.Message, "Failed to save/get")
require.Equal(t, ro2.Message, o2.Message, "Failed to save/get")
require.Equal(t, ro3.Message, o3.Message, "Failed to save/get")
require.Equal(t, ro4.Message, o4.Message, "Failed to save/get")
t.Run("overwrite changing message", func(t *testing.T) {
o1a := ro1.Clone()
o1a.Message = ro1.Message + "BBBBBBBBBB"
_, err = ss.Post().Overwrite(o1a)
require.NoError(t, err)
o2a := ro2.Clone()
o2a.Message = ro2.Message + "DDDDDDD"
_, err = ss.Post().Overwrite(o2a)
require.NoError(t, err)
o3a := ro3.Clone()
o3a.Message = ro3.Message + "WWWWWWW"
_, err = ss.Post().Overwrite(o3a)
require.NoError(t, err)
r1, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro1a := r1.Posts[o1.Id]
r2, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro2a := r2.Posts[o2.Id]
r3, err = ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro3a := r3.Posts[o3.Id]
assert.Equal(t, ro1a.Message, o1a.Message, "Failed to overwrite/get")
assert.Equal(t, ro2a.Message, o2a.Message, "Failed to overwrite/get")
assert.Equal(t, ro3a.Message, o3a.Message, "Failed to overwrite/get")
})
t.Run("overwrite clearing filenames", func(t *testing.T) {
o4a := ro4.Clone()
o4a.Filenames = []string{}
o4a.FileIds = []string{model.NewId()}
_, err = ss.Post().Overwrite(o4a)
require.NoError(t, err)
r4, err = ss.Post().Get(context.Background(), o4.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro4a := r4.Posts[o4.Id]
require.Empty(t, ro4a.Filenames, "Failed to clear Filenames")
require.Len(t, ro4a.FileIds, 1, "Failed to set FileIds")
})
}
func testPostStoreGetPostsByIds(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
r1, err := ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro1 := r1.Posts[o1.Id]
r2, err := ss.Post().Get(context.Background(), o2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro2 := r2.Posts[o2.Id]
r3, err := ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro3 := r3.Posts[o3.Id]
postIds := []string{
ro1.Id,
ro2.Id,
ro3.Id,
}
posts, err := ss.Post().GetPostsByIds(postIds)
require.NoError(t, err)
require.Len(t, posts, 3, "Expected 3 posts in results. Got %v", len(posts))
err = ss.Post().Delete(ro1.Id, model.GetMillis(), "")
require.NoError(t, err)
posts, err = ss.Post().GetPostsByIds(postIds)
require.NoError(t, err)
require.Len(t, posts, 3, "Expected 3 posts in results. Got %v", len(posts))
}
func testPostStoreGetPostsBatchForIndexing(t *testing.T, ss store.Store) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, _ = ss.Channel().Save(c1, -1)
c2 := &model.Channel{}
c2.TeamId = model.NewId()
c2.DisplayName = "Channel2"
c2.Name = NewTestId()
c2.Type = model.ChannelTypeOpen
c2, _ = ss.Channel().Save(c2, -1)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err := ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = c2.Id
o2.UserId = model.NewId()
o2.Message = NewTestId()
_, err = ss.Post().Save(o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = c1.Id
o3.UserId = model.NewId()
o3.RootId = o1.Id
o3.Message = NewTestId()
_, err = ss.Post().Save(o3)
require.NoError(t, err)
// Getting all
r, err := ss.Post().GetPostsBatchForIndexing(o1.CreateAt-1, "", 100)
require.NoError(t, err)
require.Len(t, r, 3, "Expected 3 posts in results. Got %v", len(r))
// Testing pagination
r, err = ss.Post().GetPostsBatchForIndexing(o1.CreateAt-1, "", 1)
require.NoError(t, err)
require.Len(t, r, 1, "Expected 1 post in results. Got %v", len(r))
r, err = ss.Post().GetPostsBatchForIndexing(r[0].CreateAt, r[0].Id, 1)
require.NoError(t, err)
require.Len(t, r, 1, "Expected 1 post in results. Got %v", len(r))
r, err = ss.Post().GetPostsBatchForIndexing(r[0].CreateAt, r[0].Id, 1)
require.NoError(t, err)
require.Len(t, r, 1, "Expected 1 post in results. Got %v", len(r))
r, err = ss.Post().GetPostsBatchForIndexing(r[0].CreateAt, r[0].Id, 1)
require.NoError(t, err)
require.Len(t, r, 0, "Expected 0 post in results. Got %v", len(r))
}
func testPostStorePermanentDeleteBatch(t *testing.T, ss store.Store) {
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1.CreateAt = 1000
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = channel.Id
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.CreateAt = 1000
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = channel.Id
o3.UserId = model.NewId()
o3.Message = NewTestId()
o3.CreateAt = 100000
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
_, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, 1000, model.RetentionPolicyCursor{})
require.NoError(t, err)
_, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Should have not found post 1 after purge")
_, err = ss.Post().Get(context.Background(), o2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Should have not found post 2 after purge")
_, err = ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err, "Should have found post 3 after purge")
t.Run("with pagination", func(t *testing.T) {
for i := 0; i < 3; i++ {
_, err = ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: "message",
CreateAt: 1,
})
require.NoError(t, err)
}
cursor := model.RetentionPolicyCursor{}
deleted, cursor, err := ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2, 2, cursor)
require.NoError(t, err)
require.Equal(t, int64(2), deleted)
deleted, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2, 2, cursor)
require.NoError(t, err)
require.Equal(t, int64(1), deleted)
})
t.Run("with data retention policies", func(t *testing.T) {
channelPolicy, err2 := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewInt64(30),
},
ChannelIDs: []string{channel.Id},
})
require.NoError(t, err2)
post := &model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: "message",
CreateAt: 1,
}
post, err2 = ss.Post().Save(post)
require.NoError(t, err2)
_, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, 1000, model.RetentionPolicyCursor{})
require.NoError(t, err2)
_, err2 = ss.Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err2, "global policy should have been ignored due to granular policy")
nowMillis := post.CreateAt + *channelPolicy.PostDurationDays*model.DayInMilliseconds + 1
_, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, 1000, model.RetentionPolicyCursor{})
require.NoError(t, err2)
_, err2 = ss.Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err2, "post should have been deleted by channel policy")
// Create a team policy which is stricter than the channel policy
teamPolicy, err2 := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewInt64(20),
},
TeamIDs: []string{team.Id},
})
require.NoError(t, err2)
post.Id = ""
post, err2 = ss.Post().Save(post)
require.NoError(t, err2)
nowMillis = post.CreateAt + *teamPolicy.PostDurationDays*model.DayInMilliseconds + 1
_, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, 1000, model.RetentionPolicyCursor{})
require.NoError(t, err2)
_, err2 = ss.Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err2, "channel policy should have overridden team policy")
// Delete channel policy and re-run team policy
err2 = ss.RetentionPolicy().RemoveChannels(channelPolicy.ID, []string{channel.Id})
require.NoError(t, err2)
err2 = ss.RetentionPolicy().Delete(channelPolicy.ID)
require.NoError(t, err2)
_, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, 1000, model.RetentionPolicyCursor{})
require.NoError(t, err2)
_, err2 = ss.Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err2, "post should have been deleted by team policy")
err2 = ss.RetentionPolicy().RemoveTeams(teamPolicy.ID, []string{team.Id})
require.NoError(t, err2)
err2 = ss.RetentionPolicy().Delete(teamPolicy.ID)
require.NoError(t, err2)
})
t.Run("with channel, team and global policies", func(t *testing.T) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
c1, _ = ss.Channel().Save(c1, -1)
c2 := &model.Channel{}
c2.TeamId = model.NewId()
c2.DisplayName = "Channel2"
c2.Name = NewTestId()
c2.Type = model.ChannelTypeOpen
c2, _ = ss.Channel().Save(c2, -1)
channelPolicy, err2 := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewInt64(30),
},
ChannelIDs: []string{c1.Id},
})
require.NoError(t, err2)
defer ss.RetentionPolicy().Delete(channelPolicy.ID)
teamPolicy, err2 := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewInt64(30),
},
TeamIDs: []string{team.Id},
})
require.NoError(t, err2)
defer ss.RetentionPolicy().Delete(teamPolicy.ID)
// This one should be deleted by the channel policy
_, err2 = ss.Post().Save(&model.Post{
ChannelId: c1.Id,
UserId: model.NewId(),
Message: "message",
CreateAt: 1,
})
require.NoError(t, err2)
// This one, by the team policy
_, err2 = ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: "message",
CreateAt: 1,
})
require.NoError(t, err2)
// This one, by the global policy
_, err2 = ss.Post().Save(&model.Post{
ChannelId: c2.Id,
UserId: model.NewId(),
Message: "message",
CreateAt: 1,
})
require.NoError(t, err2)
nowMillis := int64(1 + 30*model.DayInMilliseconds + 1)
deleted, _, err2 := ss.Post().PermanentDeleteBatchForRetentionPolicies(nowMillis, 2, 1000, model.RetentionPolicyCursor{})
require.NoError(t, err2)
require.Equal(t, int64(3), deleted)
})
}
func testPostStoreGetOldest(t *testing.T, ss store.Store) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o0 := &model.Post{}
o0.ChannelId = channel1.Id
o0.UserId = model.NewId()
o0.Message = NewTestId()
o0.CreateAt = 3
o0, err = ss.Post().Save(o0)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = o0.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1.CreateAt = 2
o1, err = ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
o2.CreateAt = 1
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
r1, err := ss.Post().GetOldest()
require.NoError(t, err)
assert.EqualValues(t, o2.Id, r1.Id)
}
func testGetMaxPostSize(t *testing.T, ss store.Store) {
assert.Equal(t, model.PostMessageMaxRunesV2, ss.Post().GetMaxPostSize())
assert.Equal(t, model.PostMessageMaxRunesV2, ss.Post().GetMaxPostSize())
}
func testPostStoreGetParentsForExportAfter(t *testing.T, ss store.Store) {
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
c1 := model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&c1, -1)
require.NoError(t, nErr)
u1 := model.User{}
u1.Username = model.NewId()
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err = ss.User().Save(&u1)
require.NoError(t, err)
p1 := &model.Post{}
p1.ChannelId = c1.Id
p1.UserId = u1.Id
p1.Message = NewTestId()
p1.CreateAt = 1000
p1, nErr = ss.Post().Save(p1)
require.NoError(t, nErr)
posts, err := ss.Post().GetParentsForExportAfter(10000, strings.Repeat("0", 26))
assert.NoError(t, err)
found := false
for _, p := range posts {
if p.Id == p1.Id {
found = true
assert.Equal(t, p.Id, p1.Id)
assert.Equal(t, p.Message, p1.Message)
assert.Equal(t, p.Username, u1.Username)
assert.Equal(t, p.TeamName, t1.Name)
assert.Equal(t, p.ChannelName, c1.Name)
}
}
assert.True(t, found)
}
func testPostStoreGetRepliesForExport(t *testing.T, ss store.Store) {
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
c1 := model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel1"
c1.Name = NewTestId()
c1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(&c1, -1)
require.NoError(t, nErr)
u1 := model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err = ss.User().Save(&u1)
require.NoError(t, err)
p1 := &model.Post{}
p1.ChannelId = c1.Id
p1.UserId = u1.Id
p1.Message = NewTestId()
p1.CreateAt = 1000
p1, nErr = ss.Post().Save(p1)
require.NoError(t, nErr)
p2 := &model.Post{}
p2.ChannelId = c1.Id
p2.UserId = u1.Id
p2.Message = NewTestId()
p2.CreateAt = 1001
p2.RootId = p1.Id
p2, nErr = ss.Post().Save(p2)
require.NoError(t, nErr)
r1, err := ss.Post().GetRepliesForExport(p1.Id)
assert.NoError(t, err)
assert.Len(t, r1, 1)
reply1 := r1[0]
assert.Equal(t, reply1.Id, p2.Id)
assert.Equal(t, reply1.Message, p2.Message)
assert.Equal(t, reply1.Username, u1.Username)
// Checking whether replies by deleted user are exported
u1.DeleteAt = 1002
_, err = ss.User().Update(&u1, false)
require.NoError(t, err)
r1, err = ss.Post().GetRepliesForExport(p1.Id)
assert.NoError(t, err)
assert.Len(t, r1, 1)
reply1 = r1[0]
assert.Equal(t, reply1.Id, p2.Id)
assert.Equal(t, reply1.Message, p2.Message)
assert.Equal(t, reply1.Username, u1.Username)
}
func testPostStoreGetDirectPostParentsForExportAfter(t *testing.T, ss store.Store, s SqlStore) {
teamId := model.NewId()
o1 := model.Channel{}
o1.TeamId = teamId
o1.DisplayName = "Name"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeDirect
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
ss.Channel().SaveDirectChannel(&o1, &m1, &m2)
p1 := &model.Post{}
p1.ChannelId = o1.Id
p1.UserId = u1.Id
p1.Message = NewTestId()
p1.CreateAt = 1000
p1, nErr = ss.Post().Save(p1)
require.NoError(t, nErr)
r1, nErr := ss.Post().GetDirectPostParentsForExportAfter(10000, strings.Repeat("0", 26))
assert.NoError(t, nErr)
assert.Equal(t, p1.Message, r1[0].Message)
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testPostStoreGetDirectPostParentsForExportAfterDeleted(t *testing.T, ss store.Store, s SqlStore) {
teamId := model.NewId()
o1 := model.Channel{}
o1.TeamId = teamId
o1.DisplayName = "Name"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeDirect
u1 := &model.User{}
u1.DeleteAt = 1
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.DeleteAt = 1
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
ss.Channel().SaveDirectChannel(&o1, &m1, &m2)
o1.DeleteAt = 1
nErr = ss.Channel().SetDeleteAt(o1.Id, 1, 1)
assert.NoError(t, nErr)
p1 := &model.Post{}
p1.ChannelId = o1.Id
p1.UserId = u1.Id
p1.Message = NewTestId()
p1.CreateAt = 1000
p1, nErr = ss.Post().Save(p1)
require.NoError(t, nErr)
o1a := p1.Clone()
o1a.DeleteAt = 1
o1a.Message = p1.Message + "BBBBBBBBBB"
_, nErr = ss.Post().Update(o1a, p1)
require.NoError(t, nErr)
r1, nErr := ss.Post().GetDirectPostParentsForExportAfter(10000, strings.Repeat("0", 26))
assert.NoError(t, nErr)
assert.Equal(t, 0, len(r1))
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testPostStoreGetDirectPostParentsForExportAfterBatched(t *testing.T, ss store.Store, s SqlStore) {
teamId := model.NewId()
o1 := model.Channel{}
o1.TeamId = teamId
o1.DisplayName = "Name"
o1.Name = NewTestId()
o1.Type = model.ChannelTypeDirect
var postIds []string
for i := 0; i < 150; i++ {
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(u1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
ss.Channel().SaveDirectChannel(&o1, &m1, &m2)
p1 := &model.Post{}
p1.ChannelId = o1.Id
p1.UserId = u1.Id
p1.Message = NewTestId()
p1.CreateAt = 1000
p1, nErr = ss.Post().Save(p1)
require.NoError(t, nErr)
postIds = append(postIds, p1.Id)
}
sort.Slice(postIds, func(i, j int) bool { return postIds[i] < postIds[j] })
// Get all posts
r1, err := ss.Post().GetDirectPostParentsForExportAfter(10000, strings.Repeat("0", 26))
assert.NoError(t, err)
assert.Equal(t, len(postIds), len(r1))
var exportedPostIds []string
for i := range r1 {
exportedPostIds = append(exportedPostIds, r1[i].Id)
}
sort.Slice(exportedPostIds, func(i, j int) bool { return exportedPostIds[i] < exportedPostIds[j] })
assert.ElementsMatch(t, postIds, exportedPostIds)
// Get 100
r1, err = ss.Post().GetDirectPostParentsForExportAfter(100, strings.Repeat("0", 26))
assert.NoError(t, err)
assert.Equal(t, 100, len(r1))
exportedPostIds = []string{}
for i := range r1 {
exportedPostIds = append(exportedPostIds, r1[i].Id)
}
sort.Slice(exportedPostIds, func(i, j int) bool { return exportedPostIds[i] < exportedPostIds[j] })
assert.ElementsMatch(t, postIds[:100], exportedPostIds)
// Manually truncate Channels table until testlib can handle cleanups
s.GetMasterX().Exec("TRUNCATE Channels")
}
func testHasAutoResponsePostByUserSince(t *testing.T, ss store.Store) {
t.Run("should return posts created after the given time", func(t *testing.T) {
teamId := model.NewId()
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelId := channel1.Id
userId := model.NewId()
_, err = ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
// We need to sleep because SendAutoResponseIfNecessary
// runs in a goroutine.
time.Sleep(time.Millisecond)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post3, err := ss.Post().Save(&model.Post{
ChannelId: channelId,
UserId: userId,
Message: "auto response message",
Type: model.PostTypeAutoResponder,
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
exists, err := ss.Post().HasAutoResponsePostByUserSince(model.GetPostsSinceOptions{ChannelId: channelId, Time: post2.CreateAt}, userId)
require.NoError(t, err)
assert.True(t, exists)
err = ss.Post().Delete(post3.Id, time.Now().Unix(), userId)
require.NoError(t, err)
exists, err = ss.Post().HasAutoResponsePostByUserSince(model.GetPostsSinceOptions{ChannelId: channelId, Time: post2.CreateAt}, userId)
require.NoError(t, err)
assert.False(t, exists)
})
}
func testGetPostsSinceForSync(t *testing.T, ss store.Store, s SqlStore) {
// create some posts.
channelID := model.NewId()
remoteID := model.NewString(model.NewId())
first := model.GetMillis()
data := []*model.Post{
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 0"},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 1"},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 2"},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 3", RemoteId: remoteID},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 4", RemoteId: remoteID},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 5", RemoteId: remoteID},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 6", RemoteId: remoteID},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 7"},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 8", DeleteAt: model.GetMillis()},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 9", DeleteAt: model.GetMillis()},
}
for i, p := range data {
p.UpdateAt = first + (int64(i) * 300000)
if p.RemoteId == nil {
p.RemoteId = model.NewString(model.NewId())
}
_, err := ss.Post().Save(p)
require.NoError(t, err, "couldn't save post")
}
t.Run("Invalid channel id", func(t *testing.T) {
opt := model.GetPostsSinceForSyncOptions{
ChannelId: model.NewId(),
}
cursor := model.GetPostsSinceForSyncCursor{}
posts, cursorOut, err := ss.Post().GetPostsSinceForSync(opt, cursor, 100)
require.NoError(t, err)
require.Empty(t, posts, "should return zero posts")
require.Equal(t, cursor, cursorOut)
})
t.Run("Get by channel, exclude remotes, exclude deleted", func(t *testing.T) {
opt := model.GetPostsSinceForSyncOptions{
ChannelId: channelID,
ExcludeRemoteId: *remoteID,
}
cursor := model.GetPostsSinceForSyncCursor{}
posts, _, err := ss.Post().GetPostsSinceForSync(opt, cursor, 100)
require.NoError(t, err)
require.ElementsMatch(t, getPostIds(data[0:3], data[7]), getPostIds(posts))
})
t.Run("Include deleted", func(t *testing.T) {
opt := model.GetPostsSinceForSyncOptions{
ChannelId: channelID,
IncludeDeleted: true,
}
cursor := model.GetPostsSinceForSyncCursor{}
posts, _, err := ss.Post().GetPostsSinceForSync(opt, cursor, 100)
require.NoError(t, err)
require.ElementsMatch(t, getPostIds(data), getPostIds(posts))
})
t.Run("Limit and cursor", func(t *testing.T) {
opt := model.GetPostsSinceForSyncOptions{
ChannelId: channelID,
}
cursor := model.GetPostsSinceForSyncCursor{}
posts1, cursor, err := ss.Post().GetPostsSinceForSync(opt, cursor, 5)
require.NoError(t, err)
require.Len(t, posts1, 5, "should get 5 posts")
posts2, _, err := ss.Post().GetPostsSinceForSync(opt, cursor, 5)
require.NoError(t, err)
require.Len(t, posts2, 3, "should get 3 posts")
require.ElementsMatch(t, getPostIds(data[0:8]), getPostIds(posts1, posts2...))
})
t.Run("UpdateAt collisions", func(t *testing.T) {
// this test requires all the UpdateAt timestamps to be the same.
result, err := s.GetMasterX().Exec("UPDATE Posts SET UpdateAt = ?", model.GetMillis())
require.NoError(t, err)
rows, err := result.RowsAffected()
require.NoError(t, err)
require.Greater(t, rows, int64(0))
opt := model.GetPostsSinceForSyncOptions{
ChannelId: channelID,
}
cursor := model.GetPostsSinceForSyncCursor{}
posts1, cursor, err := ss.Post().GetPostsSinceForSync(opt, cursor, 5)
require.NoError(t, err)
require.Len(t, posts1, 5, "should get 5 posts")
posts2, _, err := ss.Post().GetPostsSinceForSync(opt, cursor, 5)
require.NoError(t, err)
require.Len(t, posts2, 3, "should get 3 posts")
require.ElementsMatch(t, getPostIds(data[0:8]), getPostIds(posts1, posts2...))
})
}
func testSetPostReminder(t *testing.T, ss store.Store, s SqlStore) {
// Basic
userID := NewTestId()
p1 := &model.Post{
UserId: userID,
ChannelId: NewTestId(),
Message: "hi there",
Type: model.PostTypeDefault,
}
p1, err := ss.Post().Save(p1)
require.NoError(t, err)
reminder := &model.PostReminder{
TargetTime: 1234,
PostId: p1.Id,
UserId: userID,
}
require.NoError(t, ss.Post().SetPostReminder(reminder))
out := model.PostReminder{}
require.NoError(t, s.GetMasterX().Get(&out, `SELECT PostId, UserId, TargetTime FROM PostReminders WHERE PostId=? AND UserId=?`, reminder.PostId, reminder.UserId))
assert.Equal(t, reminder, &out)
reminder.PostId = "notfound"
err = ss.Post().SetPostReminder(reminder)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
// Upsert
reminder = &model.PostReminder{
TargetTime: 12345,
PostId: p1.Id,
UserId: userID,
}
require.NoError(t, ss.Post().SetPostReminder(reminder))
require.NoError(t, s.GetMasterX().Get(&out, `SELECT PostId, UserId, TargetTime FROM PostReminders WHERE PostId=? AND UserId=?`, reminder.PostId, reminder.UserId))
assert.Equal(t, reminder, &out)
}
func testGetPostReminders(t *testing.T, ss store.Store, s SqlStore) {
times := []int64{100, 101, 102}
for _, tt := range times {
userID := NewTestId()
p1 := &model.Post{
UserId: userID,
ChannelId: NewTestId(),
Message: "hi there",
Type: model.PostTypeDefault,
}
p1, err := ss.Post().Save(p1)
require.NoError(t, err)
reminder := &model.PostReminder{
TargetTime: tt,
PostId: p1.Id,
UserId: userID,
}
require.NoError(t, ss.Post().SetPostReminder(reminder))
}
reminders, err := ss.Post().GetPostReminders(102)
require.NoError(t, err)
require.Len(t, reminders, 2)
// assert one reminder is left
reminders, err = ss.Post().GetPostReminders(103)
require.NoError(t, err)
require.Len(t, reminders, 1)
// assert everything is deleted.
reminders, err = ss.Post().GetPostReminders(103)
require.NoError(t, err)
require.Len(t, reminders, 0)
}
func testGetPostReminderMetadata(t *testing.T, ss store.Store, s SqlStore) {
team := &model.Team{
Name: "teamname",
DisplayName: "display",
Type: model.TeamOpen,
}
team, err := ss.Team().Save(team)
require.NoError(t, err)
ch := &model.Channel{
TeamId: team.Id,
DisplayName: "channeldisplay",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
ch, err = ss.Channel().Save(ch, -1)
require.NoError(t, err)
ch2 := &model.Channel{
TeamId: "",
DisplayName: "GM_display",
Name: NewTestId(),
Type: model.ChannelTypeGroup,
}
ch2, err = ss.Channel().Save(ch2, -1)
require.NoError(t, err)
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
Locale: "es",
}
u1, err = ss.User().Save(u1)
require.NoError(t, err)
p1 := &model.Post{
UserId: u1.Id,
ChannelId: ch.Id,
Message: "hi there",
Type: model.PostTypeDefault,
}
p1, err = ss.Post().Save(p1)
require.NoError(t, err)
p2 := &model.Post{
UserId: u1.Id,
ChannelId: ch2.Id,
Message: "hi there 2",
Type: model.PostTypeDefault,
}
p2, err = ss.Post().Save(p2)
require.NoError(t, err)
meta, err := ss.Post().GetPostReminderMetadata(p1.Id)
require.NoError(t, err)
assert.Equal(t, meta.ChannelId, ch.Id)
assert.Equal(t, meta.TeamName, team.Name)
assert.Equal(t, meta.Username, u1.Username)
assert.Equal(t, meta.UserLocale, u1.Locale)
meta, err = ss.Post().GetPostReminderMetadata(p2.Id)
require.NoError(t, err)
assert.Equal(t, meta.ChannelId, ch2.Id)
assert.Equal(t, meta.TeamName, "")
assert.Equal(t, meta.Username, u1.Username)
assert.Equal(t, meta.UserLocale, u1.Locale)
}
func getPostIds(posts []*model.Post, morePosts ...*model.Post) []string {
ids := make([]string, 0, len(posts)+len(morePosts))
for _, p := range posts {
ids = append(ids, p.Id)
}
for _, p := range morePosts {
ids = append(ids, p.Id)
}
return ids
}
func testGetNthRecentPostTime(t *testing.T, ss store.Store) {
_, err := ss.Post().GetNthRecentPostTime(0)
assert.Error(t, err)
_, err = ss.Post().GetNthRecentPostTime(-1)
assert.Error(t, err)
diff := int64(10000)
now := utils.MillisFromTime(time.Now()) + diff
p1 := &model.Post{}
p1.ChannelId = model.NewId()
p1.UserId = model.NewId()
p1.Message = "test"
p1.CreateAt = now
p1, err = ss.Post().Save(p1)
require.NoError(t, err)
p2 := &model.Post{}
p2.ChannelId = p1.ChannelId
p2.UserId = p1.UserId
p2.Message = p1.Message
now = now + diff
p2.CreateAt = now
p2, err = ss.Post().Save(p2)
require.NoError(t, err)
bot1 := &model.Bot{
Username: "username",
Description: "a bot",
OwnerId: model.NewId(),
UserId: model.NewId(),
}
_, err = ss.Bot().Save(bot1)
require.NoError(t, err)
b1 := &model.Post{}
b1.Message = "bot test"
b1.ChannelId = p1.ChannelId
b1.UserId = bot1.UserId
now = now + diff
b1.CreateAt = now
_, err = ss.Post().Save(b1)
require.NoError(t, err)
p3 := &model.Post{}
p3.ChannelId = p1.ChannelId
p3.UserId = p1.UserId
p3.Message = p1.Message
now = now + diff
p3.CreateAt = now
p3, err = ss.Post().Save(p3)
require.NoError(t, err)
s1 := &model.Post{}
s1.Type = model.PostTypeJoinChannel
s1.ChannelId = p1.ChannelId
s1.UserId = model.NewId()
s1.Message = "system_join_channel message"
now = now + diff
s1.CreateAt = now
_, err = ss.Post().Save(s1)
require.NoError(t, err)
p4 := &model.Post{}
p4.ChannelId = p1.ChannelId
p4.UserId = p1.UserId
p4.Message = p1.Message
now = now + diff
p4.CreateAt = now
p4, err = ss.Post().Save(p4)
require.NoError(t, err)
r, err := ss.Post().GetNthRecentPostTime(1)
assert.NoError(t, err)
assert.Equal(t, p4.CreateAt, r)
// Skip system post
r, err = ss.Post().GetNthRecentPostTime(2)
assert.NoError(t, err)
assert.Equal(t, p3.CreateAt, r)
// Skip system & bot post
r, err = ss.Post().GetNthRecentPostTime(3)
assert.NoError(t, err)
assert.Equal(t, p2.CreateAt, r)
r, err = ss.Post().GetNthRecentPostTime(4)
assert.NoError(t, err)
assert.Equal(t, p1.CreateAt, r)
_, err = ss.Post().GetNthRecentPostTime(10000)
assert.Error(t, err)
assert.IsType(t, &store.ErrNotFound{}, err)
}
func testGetTopDMsForUserSince(t *testing.T, ss store.Store, s SqlStore) {
// users
user := model.User{Email: MakeEmail(), Username: model.NewId()}
u1 := model.User{Email: MakeEmail(), Username: model.NewId()}
u2 := model.User{Email: MakeEmail(), Username: model.NewId()}
u3 := model.User{Email: MakeEmail(), Username: model.NewId()}
u4 := model.User{Email: MakeEmail(), Username: model.NewId()}
u5 := model.User{Email: MakeEmail(), Username: model.NewId()}
_, err := ss.User().Save(&user)
require.NoError(t, err)
_, err = ss.User().Save(&u1)
require.NoError(t, err)
_, err = ss.User().Save(&u2)
require.NoError(t, err)
_, err = ss.User().Save(&u3)
require.NoError(t, err)
_, err = ss.User().Save(&u4)
require.NoError(t, err)
_, err = ss.User().Save(&u5)
require.NoError(t, err)
bot := &model.Bot{
Username: "bot_user",
Description: "bot",
OwnerId: model.NewId(),
UserId: u5.Id,
}
savedBot, nErr := ss.Bot().Save(bot)
require.NoError(t, nErr)
// user direct messages
chUser1, nErr := ss.Channel().CreateDirectChannel(&u1, &user)
require.NoError(t, nErr)
chUser2, nErr := ss.Channel().CreateDirectChannel(&u2, &user)
require.NoError(t, nErr)
chUser3, nErr := ss.Channel().CreateDirectChannel(&u3, &user)
require.NoError(t, nErr)
// other user direct message
chUser3User4, nErr := ss.Channel().CreateDirectChannel(&u3, &u4)
require.NoError(t, nErr)
// bot direct message - should be ignored by top DMs
botUser, err := ss.User().Get(context.Background(), savedBot.UserId)
require.NoError(t, err)
chBot, nErr := ss.Channel().CreateDirectChannel(&user, botUser)
require.NoError(t, nErr)
_, err = ss.Post().Save(&model.Post{
ChannelId: chBot.Id,
UserId: botUser.Id,
})
require.NoError(t, err)
// sample post data
// for u1
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser1.Id,
UserId: u1.Id,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser1.Id,
UserId: user.Id,
})
require.NoError(t, err)
// for u2: 1 post
postToDelete, err := ss.Post().Save(&model.Post{
ChannelId: chUser2.Id,
UserId: u2.Id,
})
require.NoError(t, err)
// create second post for u2: modify create at to a very old date to make sure it isn't counted
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser2.Id,
UserId: u2.Id,
CreateAt: 100,
})
require.NoError(t, err)
// for user-u3: 3 posts
for i := 0; i < 3; i++ {
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser3.Id,
UserId: user.Id,
})
require.NoError(t, err)
}
// for u4-u3: 4 posts
u3u4Post1, err := ss.Post().Save(&model.Post{
ChannelId: chUser3User4.Id,
UserId: u3.Id,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser3User4.Id,
UserId: u4.Id,
})
require.NoError(t, err)
u3u4Post2, err := ss.Post().Save(&model.Post{
ChannelId: chUser3User4.Id,
UserId: u3.Id,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser3User4.Id,
UserId: u4.Id,
})
require.NoError(t, err)
t.Run("should return topDMs when userid is specified ", func(t *testing.T) {
topDMs, storeErr := ss.Post().GetTopDMsForUserSince(user.Id, 100, 0, 100)
require.NoError(t, storeErr)
// len of topDMs.Items should be 3
require.Len(t, topDMs.Items, 3)
// check order, magnitude of items
require.Equal(t, topDMs.Items[0].SecondParticipant.Id, u3.Id)
require.Equal(t, topDMs.Items[0].MessageCount, int64(3))
require.Equal(t, topDMs.Items[0].OutgoingMessageCount, int64(3))
require.Equal(t, topDMs.Items[1].SecondParticipant.Id, u1.Id)
require.Equal(t, topDMs.Items[1].MessageCount, int64(2))
require.Equal(t, topDMs.Items[1].OutgoingMessageCount, int64(1))
require.Equal(t, topDMs.Items[2].SecondParticipant.Id, u2.Id)
require.Equal(t, topDMs.Items[2].MessageCount, int64(1))
require.Equal(t, topDMs.Items[2].OutgoingMessageCount, int64(0))
// this also ensures that u3-u4 conversation doesn't show up in others' top DMs.
})
t.Run("topDMs should only consider user's DM channels ", func(t *testing.T) {
// u4 only takes part in one conversation
topDMs, storeErr := ss.Post().GetTopDMsForUserSince(u4.Id, 100, 0, 100)
require.NoError(t, storeErr)
// len of topDMs.Items should be 3
require.Len(t, topDMs.Items, 1)
// check order, magnitude of items
require.Equal(t, topDMs.Items[0].SecondParticipant.Id, u3.Id)
require.Equal(t, topDMs.Items[0].MessageCount, int64(4))
})
t.Run("topDMs will not consider self dms", func(t *testing.T) {
chUser, nErr := ss.Channel().CreateDirectChannel(&user, &user)
require.NoError(t, nErr)
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser.Id,
UserId: user.Id,
})
// delete u2 post
err := ss.Post().Delete(postToDelete.Id, 200, user.Id)
require.NoError(t, err)
// u4 only takes part in one conversation
topDMs, err := ss.Post().GetTopDMsForUserSince(user.Id, 100, 0, 100)
require.NoError(t, err)
// len of topDMs.Items should be 3
require.Len(t, topDMs.Items, 2)
})
t.Run("topDMs will not consider deleted second user", func(t *testing.T) {
// u4 only takes part in one conversation
topDMs, err := ss.Post().GetTopDMsForUserSince(u4.Id, 100, 0, 100)
require.NoError(t, err)
// len of topDMs.Items should be 1
require.Len(t, topDMs.Items, 1)
// delete user3
err = ss.User().PermanentDelete(u3.Id)
require.NoError(t, err)
// delete user3 posts
err = ss.Post().Delete(u3u4Post1.Id, 200, u3.Id)
require.NoError(t, err)
err = ss.Post().Delete(u3u4Post2.Id, 200, u3.Id)
require.NoError(t, err)
// delete channel memberships
err = ss.Channel().PermanentDeleteMembersByUser(u3.Id)
require.NoError(t, err)
topDMs, err = ss.Post().GetTopDMsForUserSince(u4.Id, 100, 0, 100)
require.NoError(t, err)
// len of topDMs.Items should be 0 since u3 is deleted
require.Len(t, topDMs.Items, 0)
})
}
func testGetEditHistoryForPost(t *testing.T, ss store.Store) {
t.Run("should return edit history for post", func(t *testing.T) {
// create a post
post := &model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
Message: "test",
}
originalPost, err := ss.Post().Save(post)
require.NoError(t, err)
// create an edit
updatedPost := originalPost.Clone()
updatedPost.Message = "test edited"
savedUpdatedPost, err := ss.Post().Update(updatedPost, originalPost)
require.NoError(t, err)
// get edit history
edits, err := ss.Post().GetEditHistoryForPost(savedUpdatedPost.Id)
require.NoError(t, err)
require.Len(t, edits, 1)
require.Equal(t, originalPost.Id, edits[0].Id)
require.Equal(t, originalPost.UserId, edits[0].UserId)
require.Equal(t, originalPost.Message, edits[0].Message)
})
t.Run("should return error for not edited posts", func(t *testing.T) {
// create a post
post := &model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
Message: "test",
}
originalPost, err := ss.Post().Save(post)
require.NoError(t, err)
// get edit history
_, err = ss.Post().GetEditHistoryForPost(originalPost.Id)
require.Error(t, err)
})
t.Run("should return error for non-existent post", func(t *testing.T) {
// get edit history
_, err := ss.Post().GetEditHistoryForPost("non-existent")
require.Error(t, err)
})
t.Run("should return error for deleted post", func(t *testing.T) {
// create a post
post := &model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
Message: "test",
}
originalPost, err := ss.Post().Save(post)
require.NoError(t, err)
// delete post
err = ss.Post().Delete(post.Id, 100, post.UserId)
require.NoError(t, err)
// get edit history
_, err = ss.Post().GetEditHistoryForPost(originalPost.Id)
require.Error(t, err)
})
t.Run("should return error for deleted edit", func(t *testing.T) {
// create a post
post := &model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
Message: "test",
}
originalPost, err := ss.Post().Save(post)
require.NoError(t, err)
// create an edit
updatedPost := originalPost.Clone()
updatedPost.Message = "test edited"
savedUpdatedPost, err := ss.Post().Update(updatedPost, originalPost)
require.NoError(t, err)
// delete edit
err = ss.Post().Delete(savedUpdatedPost.Id, 100, savedUpdatedPost.UserId)
require.NoError(t, err)
// get edit history
_, err = ss.Post().GetEditHistoryForPost(savedUpdatedPost.Id)
require.NoError(t, err)
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestPreferenceStore(t *testing.T, ss store.Store) {
t.Run("PreferenceSave", func(t *testing.T) { testPreferenceSave(t, ss) })
t.Run("PreferenceGet", func(t *testing.T) { testPreferenceGet(t, ss) })
t.Run("PreferenceGetCategory", func(t *testing.T) { testPreferenceGetCategory(t, ss) })
t.Run("PreferenceGetAll", func(t *testing.T) { testPreferenceGetAll(t, ss) })
t.Run("PreferenceDeleteByUser", func(t *testing.T) { testPreferenceDeleteByUser(t, ss) })
t.Run("PreferenceDelete", func(t *testing.T) { testPreferenceDelete(t, ss) })
t.Run("PreferenceDeleteCategory", func(t *testing.T) { testPreferenceDeleteCategory(t, ss) })
t.Run("PreferenceDeleteCategoryAndName", func(t *testing.T) { testPreferenceDeleteCategoryAndName(t, ss) })
t.Run("PreferenceDeleteOrphanedRows", func(t *testing.T) { testPreferenceDeleteOrphanedRows(t, ss) })
}
func testPreferenceSave(t *testing.T, ss store.Store) {
id := model.NewId()
preferences := model.Preferences{
{
UserId: id,
Category: model.PreferenceCategoryDirectChannelShow,
Name: model.NewId(),
Value: "value1a",
},
{
UserId: id,
Category: model.PreferenceCategoryDirectChannelShow,
Name: model.NewId(),
Value: "value1b",
},
}
err := ss.Preference().Save(preferences)
require.NoError(t, err, "saving preference returned error")
for _, preference := range preferences {
data, _ := ss.Preference().Get(preference.UserId, preference.Category, preference.Name)
require.Equal(t, data, &preference, "got incorrect preference after first Save")
}
preferences[0].Value = "value2a"
preferences[1].Value = "value2b"
err = ss.Preference().Save(preferences)
require.NoError(t, err, "saving preference returned error")
for _, preference := range preferences {
data, _ := ss.Preference().Get(preference.UserId, preference.Category, preference.Name)
require.Equal(t, data, &preference, "got incorrect preference after second Save")
}
}
func testPreferenceGet(t *testing.T, ss store.Store) {
userId := model.NewId()
category := model.PreferenceCategoryDirectChannelShow
name := model.NewId()
preferences := model.Preferences{
{
UserId: userId,
Category: category,
Name: name,
},
{
UserId: userId,
Category: category,
Name: model.NewId(),
},
{
UserId: userId,
Category: model.NewId(),
Name: name,
},
{
UserId: model.NewId(),
Category: category,
Name: name,
},
}
err := ss.Preference().Save(preferences)
require.NoError(t, err)
data, err := ss.Preference().Get(userId, category, name)
require.NoError(t, err)
require.Equal(t, &preferences[0], data, "got incorrect preference")
// make sure getting a missing preference fails
_, err = ss.Preference().Get(model.NewId(), model.NewId(), model.NewId())
require.Error(t, err, "no error on getting a missing preference")
}
func testPreferenceGetCategory(t *testing.T, ss store.Store) {
userId := model.NewId()
category := model.PreferenceCategoryDirectChannelShow
name := model.NewId()
preferences := model.Preferences{
{
UserId: userId,
Category: category,
Name: name,
},
// same user/category, different name
{
UserId: userId,
Category: category,
Name: model.NewId(),
},
// same user/name, different category
{
UserId: userId,
Category: model.NewId(),
Name: name,
},
// same name/category, different user
{
UserId: model.NewId(),
Category: category,
Name: name,
},
}
err := ss.Preference().Save(preferences)
require.NoError(t, err)
preferencesByCategory, err := ss.Preference().GetCategory(userId, category)
require.NoError(t, err)
require.Equal(t, 2, len(preferencesByCategory), "got the wrong number of preferences")
require.True(
t,
((preferencesByCategory[0] == preferences[0] && preferencesByCategory[1] == preferences[1]) || (preferencesByCategory[0] == preferences[1] && preferencesByCategory[1] == preferences[0])),
"got incorrect preferences",
)
// make sure getting a missing preference category doesn't fail
preferencesByCategory, err = ss.Preference().GetCategory(model.NewId(), model.NewId())
require.NoError(t, err)
require.Equal(t, 0, len(preferencesByCategory), "shouldn't have got any preferences")
}
func testPreferenceGetAll(t *testing.T, ss store.Store) {
userId := model.NewId()
category := model.PreferenceCategoryDirectChannelShow
name := model.NewId()
preferences := model.Preferences{
{
UserId: userId,
Category: category,
Name: name,
},
// same user/category, different name
{
UserId: userId,
Category: category,
Name: model.NewId(),
},
// same user/name, different category
{
UserId: userId,
Category: model.NewId(),
Name: name,
},
// same name/category, different user
{
UserId: model.NewId(),
Category: category,
Name: name,
},
}
err := ss.Preference().Save(preferences)
require.NoError(t, err)
result, err := ss.Preference().GetAll(userId)
require.NoError(t, err)
require.Equal(t, 3, len(result), "got the wrong number of preferences")
for i := 0; i < 3; i++ {
assert.Falsef(t, result[0] != preferences[i] && result[1] != preferences[i] && result[2] != preferences[i], "got incorrect preferences")
}
}
func testPreferenceDeleteByUser(t *testing.T, ss store.Store) {
userId := model.NewId()
category := model.PreferenceCategoryDirectChannelShow
name := model.NewId()
preferences := model.Preferences{
{
UserId: userId,
Category: category,
Name: name,
},
// same user/category, different name
{
UserId: userId,
Category: category,
Name: model.NewId(),
},
// same user/name, different category
{
UserId: userId,
Category: model.NewId(),
Name: name,
},
// same name/category, different user
{
UserId: model.NewId(),
Category: category,
Name: name,
},
}
err := ss.Preference().Save(preferences)
require.NoError(t, err)
err = ss.Preference().PermanentDeleteByUser(userId)
require.NoError(t, err)
}
func testPreferenceDelete(t *testing.T, ss store.Store) {
preference := model.Preference{
UserId: model.NewId(),
Category: model.PreferenceCategoryDirectChannelShow,
Name: model.NewId(),
Value: "value1a",
}
err := ss.Preference().Save(model.Preferences{preference})
require.NoError(t, err)
preferences, err := ss.Preference().GetAll(preference.UserId)
require.NoError(t, err)
assert.Len(t, preferences, 1, "should've returned 1 preference")
err = ss.Preference().Delete(preference.UserId, preference.Category, preference.Name)
require.NoError(t, err)
preferences, err = ss.Preference().GetAll(preference.UserId)
require.NoError(t, err)
assert.Empty(t, preferences, "should've returned no preferences")
}
func testPreferenceDeleteCategory(t *testing.T, ss store.Store) {
category := model.NewId()
userId := model.NewId()
preference1 := model.Preference{
UserId: userId,
Category: category,
Name: model.NewId(),
Value: "value1a",
}
preference2 := model.Preference{
UserId: userId,
Category: category,
Name: model.NewId(),
Value: "value1a",
}
err := ss.Preference().Save(model.Preferences{preference1, preference2})
require.NoError(t, err)
preferences, err := ss.Preference().GetAll(userId)
require.NoError(t, err)
assert.Len(t, preferences, 2, "should've returned 2 preferences")
err = ss.Preference().DeleteCategory(userId, category)
require.NoError(t, err)
preferences, err = ss.Preference().GetAll(userId)
require.NoError(t, err)
assert.Empty(t, preferences, "should've returned no preferences")
}
func testPreferenceDeleteCategoryAndName(t *testing.T, ss store.Store) {
category := model.NewId()
name := model.NewId()
userId := model.NewId()
userId2 := model.NewId()
preference1 := model.Preference{
UserId: userId,
Category: category,
Name: name,
Value: "value1a",
}
preference2 := model.Preference{
UserId: userId2,
Category: category,
Name: name,
Value: "value1a",
}
err := ss.Preference().Save(model.Preferences{preference1, preference2})
require.NoError(t, err)
preferences, err := ss.Preference().GetAll(userId)
require.NoError(t, err)
assert.Len(t, preferences, 1, "should've returned 1 preference")
preferences, err = ss.Preference().GetAll(userId2)
require.NoError(t, err)
assert.Len(t, preferences, 1, "should've returned 1 preference")
err = ss.Preference().DeleteCategoryAndName(category, name)
require.NoError(t, err)
preferences, err = ss.Preference().GetAll(userId)
require.NoError(t, err)
assert.Empty(t, preferences, "should've returned no preference")
preferences, err = ss.Preference().GetAll(userId2)
require.NoError(t, err)
assert.Empty(t, preferences, "should've returned no preference")
}
func testPreferenceDeleteOrphanedRows(t *testing.T, ss store.Store) {
const limit = 1000
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
category := model.PreferenceCategoryFlaggedPost
userId := model.NewId()
olderPost, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: userId,
Message: "message",
CreateAt: 1000,
})
require.NoError(t, err)
newerPost, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: userId,
Message: "message",
CreateAt: 3000,
})
require.NoError(t, err)
preference1 := model.Preference{
UserId: userId,
Category: category,
Name: olderPost.Id,
Value: "true",
}
preference2 := model.Preference{
UserId: userId,
Category: category,
Name: newerPost.Id,
Value: "true",
}
nErr := ss.Preference().Save(model.Preferences{preference1, preference2})
require.NoError(t, nErr)
_, _, nErr = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, limit, model.RetentionPolicyCursor{})
assert.NoError(t, nErr)
_, nErr = ss.Preference().DeleteOrphanedRows(limit)
assert.NoError(t, nErr)
_, nErr = ss.Preference().Get(userId, category, preference1.Name)
assert.Error(t, nErr, "older preference should have been deleted")
_, nErr = ss.Preference().Get(userId, category, preference2.Name)
assert.NoError(t, nErr, "newer preference should not have been deleted")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestProductNoticesStore(t *testing.T, ss store.Store) {
t.Run("TestAddViewed", func(t *testing.T) { testAddViewed(t, ss) })
t.Run("TestUpdateViewed", func(t *testing.T) { testUpdateViewed(t, ss) })
t.Run("TestClearOld", func(t *testing.T) { testClearOld(t, ss) })
}
func testAddViewed(t *testing.T, ss store.Store) {
notices := []string{"noticeA", "noticeB"}
defer ss.ProductNotices().Clear(notices)
err := ss.ProductNotices().View("testuser", notices)
require.NoError(t, err)
err = ss.ProductNotices().View("testuser2", notices)
require.NoError(t, err)
res, err := ss.ProductNotices().GetViews("testuser")
require.NoError(t, err)
require.Len(t, res, 2)
}
func testUpdateViewed(t *testing.T, ss store.Store) {
noticesA := []string{"noticeA", "noticeB"}
noticesB := []string{"noticeB", "noticeC"}
defer ss.ProductNotices().Clear(noticesA)
defer ss.ProductNotices().Clear(noticesB)
// mark two notices
err := ss.ProductNotices().View("testuser", noticesA)
require.NoError(t, err)
// mark one old and one new
err = ss.ProductNotices().View("testuser", noticesB)
require.NoError(t, err)
res, err := ss.ProductNotices().GetViews("testuser")
require.NoError(t, err)
require.Len(t, res, 3)
// make sure that one B has two views
require.Equal(t, res[0].Viewed, int32(1))
require.Equal(t, res[1].Viewed, int32(2))
require.Equal(t, res[2].Viewed, int32(1))
// make sure that B's timestamp was updated
require.GreaterOrEqual(t, res[1].Timestamp, res[0].Timestamp)
}
func testClearOld(t *testing.T, ss store.Store) {
noticesA := []string{"noticeA", "noticeB"}
defer ss.ProductNotices().Clear(noticesA)
// mark two notices
err := ss.ProductNotices().View("testuser", noticesA)
require.NoError(t, err)
err = ss.ProductNotices().ClearOldNotices(model.ProductNotices{
{
ID: "noticeA",
},
{
ID: "noticeC",
},
})
require.NoError(t, err)
res, err := ss.ProductNotices().GetViews("testuser")
require.NoError(t, err)
require.Len(t, res, 1)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"errors"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/retrylayer"
)
func TestReactionStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("ReactionSave", func(t *testing.T) { testReactionSave(t, ss) })
t.Run("ReactionDelete", func(t *testing.T) { testReactionDelete(t, ss) })
t.Run("ReactionGetForPost", func(t *testing.T) { testReactionGetForPost(t, ss) })
t.Run("ReactionGetForPostSince", func(t *testing.T) { testReactionGetForPostSince(t, ss, s) })
t.Run("ReactionDeleteAllWithEmojiName", func(t *testing.T) { testReactionDeleteAllWithEmojiName(t, ss, s) })
t.Run("PermanentDeleteBatch", func(t *testing.T) { testReactionStorePermanentDeleteBatch(t, ss) })
t.Run("ReactionBulkGetForPosts", func(t *testing.T) { testReactionBulkGetForPosts(t, ss) })
t.Run("ReactionDeadlock", func(t *testing.T) { testReactionDeadlock(t, ss) })
}
func testReactionSave(t *testing.T, ss store.Store) {
post, err := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
})
require.NoError(t, err)
firstUpdateAt := post.UpdateAt
reaction1 := &model.Reaction{
UserId: model.NewId(),
PostId: post.Id,
EmojiName: model.NewId(),
}
time.Sleep(time.Millisecond)
reaction, nErr := ss.Reaction().Save(reaction1)
require.NoError(t, nErr)
saved := reaction
assert.Equal(t, saved.UserId, reaction1.UserId, "should've saved reaction user_id and returned it")
assert.Equal(t, saved.PostId, reaction1.PostId, "should've saved reaction post_id and returned it")
assert.Equal(t, saved.EmojiName, reaction1.EmojiName, "should've saved reaction emoji_name and returned it")
assert.NotZero(t, saved.UpdateAt, "should've saved reaction update_at and returned it")
assert.Equal(t, saved.ChannelId, post.ChannelId, "should've saved reaction update_at and returned it")
assert.Zero(t, saved.DeleteAt, "should've saved reaction delete_at with zero value and returned it")
var secondUpdateAt int64
postList, err := ss.Post().Get(context.Background(), reaction1.PostId, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
assert.True(t, postList.Posts[post.Id].HasReactions, "should've set HasReactions = true on post")
assert.NotEqual(t, postList.Posts[post.Id].UpdateAt, firstUpdateAt, "should've marked post as updated when HasReactions changed")
if postList.Posts[post.Id].HasReactions && postList.Posts[post.Id].UpdateAt != firstUpdateAt {
secondUpdateAt = postList.Posts[post.Id].UpdateAt
}
_, nErr = ss.Reaction().Save(reaction1)
assert.NoError(t, nErr, "should've allowed saving a duplicate reaction")
// different user
reaction2 := &model.Reaction{
UserId: model.NewId(),
PostId: reaction1.PostId,
EmojiName: reaction1.EmojiName,
}
time.Sleep(time.Millisecond)
_, nErr = ss.Reaction().Save(reaction2)
require.NoError(t, nErr)
postList, err = ss.Post().Get(context.Background(), reaction2.PostId, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
assert.NotEqual(t, postList.Posts[post.Id].UpdateAt, secondUpdateAt, "should've marked post as updated even if HasReactions doesn't change")
// different post
// create post1
post1, err := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
})
require.NoError(t, err)
reaction3 := &model.Reaction{
UserId: reaction1.UserId,
PostId: post1.Id,
EmojiName: reaction1.EmojiName,
}
_, nErr = ss.Reaction().Save(reaction3)
require.NoError(t, nErr)
// different emoji
reaction4 := &model.Reaction{
UserId: reaction1.UserId,
PostId: reaction1.PostId,
EmojiName: model.NewId(),
}
_, nErr = ss.Reaction().Save(reaction4)
require.NoError(t, nErr)
// invalid reaction
reaction5 := &model.Reaction{
UserId: reaction1.UserId,
PostId: reaction1.PostId,
}
_, nErr = ss.Reaction().Save(reaction5)
require.Error(t, nErr, "should've failed for invalid reaction")
}
func testReactionDelete(t *testing.T, ss store.Store) {
t.Run("Delete", func(t *testing.T) {
post, err := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
})
require.NoError(t, err)
reaction := &model.Reaction{
UserId: model.NewId(),
PostId: post.Id,
EmojiName: model.NewId(),
}
_, nErr := ss.Reaction().Save(reaction)
require.NoError(t, nErr)
result, err := ss.Post().Get(context.Background(), reaction.PostId, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
firstUpdateAt := result.Posts[post.Id].UpdateAt
_, nErr = ss.Reaction().Delete(reaction)
require.NoError(t, nErr)
reactions, rErr := ss.Reaction().GetForPost(post.Id, false)
require.NoError(t, rErr)
assert.Empty(t, reactions, "should've deleted reaction")
postList, err := ss.Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
assert.False(t, postList.Posts[post.Id].HasReactions, "should've set HasReactions = false on post")
assert.NotEqual(t, postList.Posts[post.Id].UpdateAt, firstUpdateAt, "should mark post as updated after deleting reactions")
})
t.Run("Undelete", func(t *testing.T) {
post, err := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
})
require.NoError(t, err)
reaction := &model.Reaction{
UserId: model.NewId(),
PostId: post.Id,
EmojiName: model.NewId(),
}
savedReaction, nErr := ss.Reaction().Save(reaction)
require.NoError(t, nErr)
updateAt := savedReaction.UpdateAt
_, nErr = ss.Reaction().Delete(savedReaction)
require.NoError(t, nErr)
// add same reaction back and ensure update_at is set
_, nErr = ss.Reaction().Save(savedReaction)
require.NoError(t, nErr)
reactions, err := ss.Reaction().GetForPost(post.Id, false)
require.NoError(t, err)
assert.Len(t, reactions, 1)
assert.GreaterOrEqual(t, reactions[0].UpdateAt, updateAt)
})
}
func testReactionGetForPost(t *testing.T, ss store.Store) {
userId := model.NewId()
// create post
post, err := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: userId,
})
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: userId,
})
require.NoError(t, err)
postId := post.Id
post1Id := post1.Id
reactions := []*model.Reaction{
{
UserId: userId,
PostId: postId,
EmojiName: "smile",
},
{
UserId: post1Id,
PostId: postId,
EmojiName: "smile",
},
{
UserId: userId,
PostId: postId,
EmojiName: "sad",
},
{
UserId: userId,
PostId: post1Id,
EmojiName: "angry",
},
}
for _, reaction := range reactions {
_, err = ss.Reaction().Save(reaction)
require.NoError(t, err)
}
// save and delete an additional reaction to test soft deletion
temp := &model.Reaction{
UserId: userId,
PostId: postId,
EmojiName: "grin",
}
savedTmp, err := ss.Reaction().Save(temp)
require.NoError(t, err)
_, err = ss.Reaction().Delete(savedTmp)
require.NoError(t, err)
returned, err := ss.Reaction().GetForPost(postId, false)
require.NoError(t, err)
require.Len(t, returned, 3, "should've returned 3 reactions")
for _, reaction := range reactions {
found := false
for _, returnedReaction := range returned {
if returnedReaction.UserId == reaction.UserId && returnedReaction.PostId == reaction.PostId &&
returnedReaction.EmojiName == reaction.EmojiName && returnedReaction.UpdateAt > 0 {
found = true
break
}
}
if !found {
assert.NotEqual(t, reaction.PostId, postId, "should've returned reaction for post %v", reaction)
} else if found {
assert.Equal(t, reaction.PostId, postId, "shouldn't have returned reaction for another post")
}
}
// Should return cached item
returned, err = ss.Reaction().GetForPost(postId, true)
require.NoError(t, err)
require.Len(t, returned, 3, "should've returned 3 reactions")
for _, reaction := range reactions {
found := false
for _, returnedReaction := range returned {
if returnedReaction.UserId == reaction.UserId && returnedReaction.PostId == reaction.PostId &&
returnedReaction.EmojiName == reaction.EmojiName {
found = true
break
}
}
if !found {
assert.NotEqual(t, reaction.PostId, postId, "should've returned reaction for post %v", reaction)
} else if found {
assert.Equal(t, reaction.PostId, postId, "shouldn't have returned reaction for another post")
}
}
}
func testReactionGetForPostSince(t *testing.T, ss store.Store, s SqlStore) {
now := model.GetMillis()
later := now + 1800000 // add 30 minutes
remoteId := model.NewId()
userId := model.NewId()
// create post
post, _ := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: userId,
})
post1, _ := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: userId,
})
postId := post.Id
post1Id := post1.Id
reactions := []*model.Reaction{
{
UserId: userId,
PostId: postId,
EmojiName: "smile",
UpdateAt: later,
},
{
UserId: model.NewId(),
PostId: postId,
EmojiName: "smile",
},
{
UserId: userId,
PostId: postId,
EmojiName: "sad",
UpdateAt: later,
RemoteId: &remoteId,
},
{
UserId: userId,
PostId: post1Id,
EmojiName: "angry",
},
{
UserId: userId,
PostId: postId,
EmojiName: "angry",
DeleteAt: now + 1,
UpdateAt: later,
},
}
for _, reaction := range reactions {
delete := reaction.DeleteAt
update := reaction.UpdateAt
_, err := ss.Reaction().Save(reaction)
require.NoError(t, err)
if delete > 0 {
_, err = ss.Reaction().Delete(reaction)
require.NoError(t, err)
}
if update > 0 {
err = forceUpdateAt(reaction, update, s)
require.NoError(t, err)
}
err = forceNULL(reaction, s) // test COALESCE
require.NoError(t, err)
}
t.Run("reactions since", func(t *testing.T) {
// should return 2 reactions that are not deleted for post
returned, err := ss.Reaction().GetForPostSince(postId, later-1, "", false)
require.NoError(t, err)
require.Len(t, returned, 2, "should've returned 2 non-deleted reactions")
for _, r := range returned {
assert.Zero(t, r.DeleteAt, "should not have returned deleted reaction")
}
})
t.Run("reactions since, incl deleted", func(t *testing.T) {
// should return 3 reactions for post, including one deleted
returned, err := ss.Reaction().GetForPostSince(postId, later-1, "", true)
require.NoError(t, err)
require.Len(t, returned, 3, "should've returned 3 reactions")
var count int
for _, r := range returned {
if r.DeleteAt > 0 {
count++
}
}
assert.Equal(t, 1, count, "should not have returned 1 deleted reaction")
})
t.Run("reactions since, filter remoteId", func(t *testing.T) {
// should return 1 reactions that are not deleted for post and have no remoteId
returned, err := ss.Reaction().GetForPostSince(postId, later-1, remoteId, false)
require.NoError(t, err)
require.Len(t, returned, 1, "should've returned 1 filtered reactions")
for _, r := range returned {
assert.Zero(t, r.DeleteAt, "should not have returned deleted reaction")
}
})
t.Run("reactions since, invalid post", func(t *testing.T) {
// should return 0 reactions for invalid post
returned, err := ss.Reaction().GetForPostSince(model.NewId(), later-1, "", true)
require.NoError(t, err)
require.Empty(t, returned, "should've returned 0 reactions")
})
t.Run("reactions since, far future", func(t *testing.T) {
// should return 0 reactions for since far in the future
returned, err := ss.Reaction().GetForPostSince(postId, later*2, "", true)
require.NoError(t, err)
require.Empty(t, returned, "should've returned 0 reactions")
})
}
func forceUpdateAt(reaction *model.Reaction, updateAt int64, s SqlStore) error {
params := map[string]any{
"userid": reaction.UserId,
"postid": reaction.PostId,
"emojiname": reaction.EmojiName,
"updateat": updateAt,
}
sqlResult, err := s.GetMasterX().NamedExec(`
UPDATE
Reactions
SET
UpdateAt=:updateat
WHERE
UserId = :userid AND
PostId = :postid AND
EmojiName = :emojiname`, params,
)
if err != nil {
return err
}
rows, err := sqlResult.RowsAffected()
if err != nil {
return err
}
if rows != 1 {
return errors.New("expected one row affected")
}
return nil
}
func forceNULL(reaction *model.Reaction, s SqlStore) error {
if _, err := s.GetMasterX().Exec(`UPDATE Reactions SET UpdateAt = NULL WHERE UpdateAt = 0`); err != nil {
return err
}
if _, err := s.GetMasterX().Exec(`UPDATE Reactions SET DeleteAt = NULL WHERE DeleteAt = 0`); err != nil {
return err
}
return nil
}
func testReactionDeleteAllWithEmojiName(t *testing.T, ss store.Store, s SqlStore) {
emojiToDelete := model.NewId()
post, err1 := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
})
require.NoError(t, err1)
post2, err2 := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
})
require.NoError(t, err2)
post3, err3 := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
})
require.NoError(t, err3)
userId := model.NewId()
reactions := []*model.Reaction{
{
UserId: userId,
PostId: post.Id,
EmojiName: emojiToDelete,
},
{
UserId: model.NewId(),
PostId: post.Id,
EmojiName: emojiToDelete,
},
{
UserId: userId,
PostId: post.Id,
EmojiName: "sad",
},
{
UserId: userId,
PostId: post2.Id,
EmojiName: "angry",
},
{
UserId: userId,
PostId: post3.Id,
EmojiName: emojiToDelete,
},
}
for _, reaction := range reactions {
_, err := ss.Reaction().Save(reaction)
require.NoError(t, err)
// make at least one Reaction record contain NULL for Update and DeleteAt to simulate post schema upgrade case.
if reaction.EmojiName == emojiToDelete {
err = forceNULL(reaction, s)
require.NoError(t, err)
}
}
err := ss.Reaction().DeleteAllWithEmojiName(emojiToDelete)
require.NoError(t, err)
// check that the reactions were deleted
returned, err := ss.Reaction().GetForPost(post.Id, false)
require.NoError(t, err)
require.Len(t, returned, 1, "should've only removed reactions with emoji name")
for _, reaction := range returned {
assert.NotEqual(t, reaction.EmojiName, "smile", "should've removed reaction with emoji name")
}
returned, err = ss.Reaction().GetForPost(post2.Id, false)
require.NoError(t, err)
assert.Len(t, returned, 1, "should've only removed reactions with emoji name")
returned, err = ss.Reaction().GetForPost(post3.Id, false)
require.NoError(t, err)
assert.Empty(t, returned, "should've only removed reactions with emoji name")
// check that the posts are updated
postList, err := ss.Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
assert.True(t, postList.Posts[post.Id].HasReactions, "post should still have reactions")
postList, err = ss.Post().Get(context.Background(), post2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
assert.True(t, postList.Posts[post2.Id].HasReactions, "post should still have reactions")
postList, err = ss.Post().Get(context.Background(), post3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
assert.False(t, postList.Posts[post3.Id].HasReactions, "post shouldn't have reactions any more")
}
func testReactionStorePermanentDeleteBatch(t *testing.T, ss store.Store) {
const limit = 1000
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
olderPost, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
CreateAt: 1000,
})
require.NoError(t, err)
newerPost, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
CreateAt: 3000,
})
require.NoError(t, err)
// Reactions will be deleted based on the timestamp of their post. So the time at
// which a reaction was created doesn't matter.
reactions := []*model.Reaction{
{
UserId: model.NewId(),
PostId: olderPost.Id,
EmojiName: "sad",
},
{
UserId: model.NewId(),
PostId: olderPost.Id,
EmojiName: "sad",
},
{
UserId: model.NewId(),
PostId: newerPost.Id,
EmojiName: "smile",
},
}
for _, reaction := range reactions {
_, err = ss.Reaction().Save(reaction)
require.NoError(t, err)
}
_, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
_, err = ss.Reaction().DeleteOrphanedRows(limit)
require.NoError(t, err)
returned, err := ss.Reaction().GetForPost(olderPost.Id, false)
require.NoError(t, err)
require.Len(t, returned, 0, "reactions for older post should have been deleted")
returned, err = ss.Reaction().GetForPost(newerPost.Id, false)
require.NoError(t, err)
require.Len(t, returned, 1, "reactions for newer post should not have been deleted")
}
func testReactionBulkGetForPosts(t *testing.T, ss store.Store) {
userId := model.NewId()
post, _ := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: userId,
})
postId := post.Id
post, _ = ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: userId,
})
post2Id := post.Id
post, _ = ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: userId,
})
post3Id := post.Id
post, _ = ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: userId,
})
post4Id := post.Id
reactions := []*model.Reaction{
{
UserId: userId,
PostId: postId,
EmojiName: "smile",
},
{
UserId: model.NewId(),
PostId: post2Id,
EmojiName: "smile",
},
{
UserId: userId,
PostId: post3Id,
EmojiName: "sad",
},
{
UserId: userId,
PostId: postId,
EmojiName: "angry",
},
{
UserId: userId,
PostId: post2Id,
EmojiName: "angry",
},
{
UserId: userId,
PostId: post4Id,
EmojiName: "angry",
},
}
for _, reaction := range reactions {
_, err := ss.Reaction().Save(reaction)
require.NoError(t, err)
}
postIds := []string{postId, post2Id, post3Id}
returned, err := ss.Reaction().BulkGetForPosts(postIds)
require.NoError(t, err)
require.Len(t, returned, 5, "should've returned 5 reactions")
post4IdFound := false
for _, reaction := range returned {
if reaction.PostId == post4Id {
post4IdFound = true
break
}
}
require.False(t, post4IdFound, "Wrong reaction returned")
}
// testReactionDeadlock is a best-case attempt to recreate the deadlock scenario.
// It at least deadlocks 2 times out of 5.
func testReactionDeadlock(t *testing.T, ss store.Store) {
ss = retrylayer.New(ss)
post, err := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
})
require.NoError(t, err)
postId := post.Id
post, err = ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
})
require.NoError(t, err)
reaction1 := &model.Reaction{
UserId: model.NewId(),
PostId: post.Id,
EmojiName: model.NewId(),
}
_, nErr := ss.Reaction().Save(reaction1)
require.NoError(t, nErr)
// different user
reaction2 := &model.Reaction{
UserId: model.NewId(),
PostId: reaction1.PostId,
EmojiName: reaction1.EmojiName,
}
_, nErr = ss.Reaction().Save(reaction2)
require.NoError(t, nErr)
// different post
reaction3 := &model.Reaction{
UserId: reaction1.UserId,
PostId: postId,
EmojiName: reaction1.EmojiName,
}
_, nErr = ss.Reaction().Save(reaction3)
require.NoError(t, nErr)
// different emoji
reaction4 := &model.Reaction{
UserId: reaction1.UserId,
PostId: reaction1.PostId,
EmojiName: model.NewId(),
}
_, nErr = ss.Reaction().Save(reaction4)
require.NoError(t, nErr)
var wg sync.WaitGroup
wg.Add(2)
// 1st tx
go func() {
defer wg.Done()
err := ss.Reaction().DeleteAllWithEmojiName(reaction1.EmojiName)
require.NoError(t, err)
}()
// 2nd tx
go func() {
defer wg.Done()
_, err := ss.Reaction().Delete(reaction2)
require.NoError(t, err)
}()
wg.Wait()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"strings"
"testing"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRemoteClusterStore(t *testing.T, ss store.Store) {
t.Run("RemoteClusterGetAllInChannel", func(t *testing.T) { testRemoteClusterGetAllInChannel(t, ss) })
t.Run("RemoteClusterGetAllNotInChannel", func(t *testing.T) { testRemoteClusterGetAllNotInChannel(t, ss) })
t.Run("RemoteClusterSave", func(t *testing.T) { testRemoteClusterSave(t, ss) })
t.Run("RemoteClusterDelete", func(t *testing.T) { testRemoteClusterDelete(t, ss) })
t.Run("RemoteClusterGet", func(t *testing.T) { testRemoteClusterGet(t, ss) })
t.Run("RemoteClusterGetAll", func(t *testing.T) { testRemoteClusterGetAll(t, ss) })
t.Run("RemoteClusterGetByTopic", func(t *testing.T) { testRemoteClusterGetByTopic(t, ss) })
t.Run("RemoteClusterUpdateTopics", func(t *testing.T) { testRemoteClusterUpdateTopics(t, ss) })
}
func testRemoteClusterSave(t *testing.T, ss store.Store) {
t.Run("Save", func(t *testing.T) {
rc := &model.RemoteCluster{
Name: "some_remote",
SiteURL: "somewhere.com",
CreatorId: model.NewId(),
}
rcSaved, err := ss.RemoteCluster().Save(rc)
require.NoError(t, err)
require.Equal(t, rc.Name, rcSaved.Name)
require.Equal(t, rc.SiteURL, rcSaved.SiteURL)
require.Greater(t, rc.CreateAt, int64(0))
require.Equal(t, rc.LastPingAt, int64(0))
})
t.Run("Save missing display name", func(t *testing.T) {
rc := &model.RemoteCluster{
SiteURL: "somewhere.com",
CreatorId: model.NewId(),
}
_, err := ss.RemoteCluster().Save(rc)
require.Error(t, err)
})
t.Run("Save missing creator id", func(t *testing.T) {
rc := &model.RemoteCluster{
Name: "some_remote_2",
SiteURL: "somewhere.com",
}
_, err := ss.RemoteCluster().Save(rc)
require.Error(t, err)
})
}
func testRemoteClusterDelete(t *testing.T, ss store.Store) {
t.Run("Delete", func(t *testing.T) {
rc := &model.RemoteCluster{
Name: "shortlived_remote",
SiteURL: "nowhere.com",
CreatorId: model.NewId(),
}
rcSaved, err := ss.RemoteCluster().Save(rc)
require.NoError(t, err)
deleted, err := ss.RemoteCluster().Delete(rcSaved.RemoteId)
require.NoError(t, err)
require.True(t, deleted)
})
t.Run("Delete nonexistent", func(t *testing.T) {
deleted, err := ss.RemoteCluster().Delete(model.NewId())
require.NoError(t, err)
require.False(t, deleted)
})
}
func testRemoteClusterGet(t *testing.T, ss store.Store) {
t.Run("Get", func(t *testing.T) {
rc := &model.RemoteCluster{
Name: "shortlived_remote_2",
SiteURL: "nowhere.com",
CreatorId: model.NewId(),
}
rcSaved, err := ss.RemoteCluster().Save(rc)
require.NoError(t, err)
rcGet, err := ss.RemoteCluster().Get(rcSaved.RemoteId)
require.NoError(t, err)
require.Equal(t, rcSaved.RemoteId, rcGet.RemoteId)
})
t.Run("Get not found", func(t *testing.T) {
_, err := ss.RemoteCluster().Get(model.NewId())
require.Error(t, err)
})
}
func testRemoteClusterGetAll(t *testing.T, ss store.Store) {
require.NoError(t, clearRemoteClusters(ss))
userId := model.NewId()
now := model.GetMillis()
pingLongAgo := model.GetMillis() - (model.RemoteOfflineAfterMillis * 3)
data := []*model.RemoteCluster{
{Name: "offline_remote", CreatorId: userId, SiteURL: "somewhere.com", LastPingAt: pingLongAgo, Topics: " shared incident "},
{Name: "some_online_remote", CreatorId: userId, SiteURL: "nowhere.com", LastPingAt: now, Topics: " shared incident "},
{Name: "another_online_remote", CreatorId: model.NewId(), SiteURL: "underwhere.com", LastPingAt: now, Topics: ""},
{Name: "another_offline_remote", CreatorId: model.NewId(), SiteURL: "knowhere.com", LastPingAt: pingLongAgo, Topics: " shared "},
{Name: "brand_new_offline_remote", CreatorId: userId, SiteURL: "", LastPingAt: 0, Topics: " bogus shared stuff "},
}
idsAll := make([]string, 0)
idsOnline := make([]string, 0)
idsShareTopic := make([]string, 0)
for _, item := range data {
online := item.LastPingAt == now
saved, err := ss.RemoteCluster().Save(item)
require.NoError(t, err)
idsAll = append(idsAll, saved.RemoteId)
if online {
idsOnline = append(idsOnline, saved.RemoteId)
}
if strings.Contains(saved.Topics, " shared ") {
idsShareTopic = append(idsShareTopic, saved.RemoteId)
}
}
t.Run("GetAll", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{}
remotes, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
// make sure all the test data remotes were returned.
ids := getIds(remotes)
assert.ElementsMatch(t, ids, idsAll)
})
t.Run("GetAll online only", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
ExcludeOffline: true,
}
remotes, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
// make sure all the online remotes were returned.
ids := getIds(remotes)
assert.ElementsMatch(t, ids, idsOnline)
})
t.Run("GetAll by topic", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
Topic: "shared",
}
remotes, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
// make sure only correct topic returned
ids := getIds(remotes)
assert.ElementsMatch(t, ids, idsShareTopic)
})
t.Run("GetAll online by topic", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
ExcludeOffline: true,
Topic: "shared",
}
remotes, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
// make sure only online remotes were returned.
ids := getIds(remotes)
assert.Subset(t, idsOnline, ids)
// make sure correct topic returned
assert.Subset(t, idsShareTopic, ids)
assert.Len(t, ids, 1)
})
t.Run("GetAll by Creator", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
CreatorId: userId,
}
remotes, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
// make sure only correct creator returned
assert.Len(t, remotes, 3)
for _, rc := range remotes {
assert.Equal(t, userId, rc.CreatorId)
}
})
t.Run("GetAll by Confirmed", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
OnlyConfirmed: true,
}
remotes, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
// make sure only confirmed returned
assert.Len(t, remotes, 4)
for _, rc := range remotes {
assert.NotEmpty(t, rc.SiteURL)
}
})
}
func testRemoteClusterGetAllInChannel(t *testing.T, ss store.Store) {
require.NoError(t, clearRemoteClusters(ss))
now := model.GetMillis()
userId := model.NewId()
channel1, err := createTestChannel(ss, "channel_1")
require.NoError(t, err)
channel2, err := createTestChannel(ss, "channel_2")
require.NoError(t, err)
channel3, err := createTestChannel(ss, "channel_3")
require.NoError(t, err)
// Create shared channels
scData := []*model.SharedChannel{
{ChannelId: channel1.Id, TeamId: model.NewId(), Home: true, ShareName: "test_chan_1", CreatorId: model.NewId()},
{ChannelId: channel2.Id, TeamId: model.NewId(), Home: true, ShareName: "test_chan_2", CreatorId: model.NewId()},
{ChannelId: channel3.Id, TeamId: model.NewId(), Home: true, ShareName: "test_chan_3", CreatorId: model.NewId()},
}
for _, item := range scData {
_, err := ss.SharedChannel().Save(item)
require.NoError(t, err)
}
// Create some remote clusters
rcData := []*model.RemoteCluster{
{Name: "AAAA_Inc", CreatorId: userId, SiteURL: "aaaa.com", RemoteId: model.NewId(), LastPingAt: now},
{Name: "BBBB_Inc", CreatorId: userId, SiteURL: "bbbb.com", RemoteId: model.NewId(), LastPingAt: 0},
{Name: "CCCC_Inc", CreatorId: userId, SiteURL: "cccc.com", RemoteId: model.NewId(), LastPingAt: now},
{Name: "DDDD_Inc", CreatorId: userId, SiteURL: "dddd.com", RemoteId: model.NewId(), LastPingAt: now},
{Name: "EEEE_Inc", CreatorId: userId, SiteURL: "eeee.com", RemoteId: model.NewId(), LastPingAt: 0},
}
for _, item := range rcData {
_, err := ss.RemoteCluster().Save(item)
require.NoError(t, err)
}
// Create some shared channel remotes
scrData := []*model.SharedChannelRemote{
{ChannelId: channel1.Id, RemoteId: rcData[0].RemoteId, CreatorId: model.NewId()},
{ChannelId: channel1.Id, RemoteId: rcData[1].RemoteId, CreatorId: model.NewId()},
{ChannelId: channel2.Id, RemoteId: rcData[2].RemoteId, CreatorId: model.NewId()},
{ChannelId: channel2.Id, RemoteId: rcData[3].RemoteId, CreatorId: model.NewId()},
{ChannelId: channel2.Id, RemoteId: rcData[4].RemoteId, CreatorId: model.NewId()},
}
for _, item := range scrData {
_, err := ss.SharedChannel().SaveRemote(item)
require.NoError(t, err)
}
t.Run("Channel 1", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
InChannel: channel1.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
require.Len(t, list, 2, "channel 1 should have 2 remote clusters")
ids := getIds(list)
require.ElementsMatch(t, []string{rcData[0].RemoteId, rcData[1].RemoteId}, ids)
})
t.Run("Channel 1 online only", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
ExcludeOffline: true,
InChannel: channel1.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
require.Len(t, list, 1, "channel 1 should have 1 online remote clusters")
ids := getIds(list)
require.ElementsMatch(t, []string{rcData[0].RemoteId}, ids)
})
t.Run("Channel 2", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
InChannel: channel2.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
require.Len(t, list, 3, "channel 2 should have 3 remote clusters")
ids := getIds(list)
require.ElementsMatch(t, []string{rcData[2].RemoteId, rcData[3].RemoteId, rcData[4].RemoteId}, ids)
})
t.Run("Channel 2 online only", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
ExcludeOffline: true,
InChannel: channel2.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
require.Len(t, list, 2, "channel 2 should have 2 online remote clusters")
ids := getIds(list)
require.ElementsMatch(t, []string{rcData[2].RemoteId, rcData[3].RemoteId}, ids)
})
t.Run("Channel 3", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
InChannel: channel3.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
require.Empty(t, list, "channel 3 should have 0 remote clusters")
})
}
func testRemoteClusterGetAllNotInChannel(t *testing.T, ss store.Store) {
require.NoError(t, clearRemoteClusters(ss))
userId := model.NewId()
channel1, err := createTestChannel(ss, "channel_1")
require.NoError(t, err)
channel2, err := createTestChannel(ss, "channel_2")
require.NoError(t, err)
channel3, err := createTestChannel(ss, "channel_3")
require.NoError(t, err)
// Create shared channels
scData := []*model.SharedChannel{
{ChannelId: channel1.Id, TeamId: model.NewId(), Home: true, ShareName: "test_chan_1", CreatorId: model.NewId()},
{ChannelId: channel2.Id, TeamId: model.NewId(), Home: true, ShareName: "test_chan_2", CreatorId: model.NewId()},
{ChannelId: channel3.Id, TeamId: model.NewId(), Home: true, ShareName: "test_chan_3", CreatorId: model.NewId()},
}
for _, item := range scData {
_, err := ss.SharedChannel().Save(item)
require.NoError(t, err)
}
// Create some remote clusters
rcData := []*model.RemoteCluster{
{Name: "AAAA_Inc", CreatorId: userId, SiteURL: "aaaa.com", RemoteId: model.NewId()},
{Name: "BBBB_Inc", CreatorId: userId, SiteURL: "bbbb.com", RemoteId: model.NewId()},
{Name: "CCCC_Inc", CreatorId: userId, SiteURL: "cccc.com", RemoteId: model.NewId()},
{Name: "DDDD_Inc", CreatorId: userId, SiteURL: "dddd.com", RemoteId: model.NewId()},
{Name: "EEEE_Inc", CreatorId: userId, SiteURL: "eeee.com", RemoteId: model.NewId()},
}
for _, item := range rcData {
_, err := ss.RemoteCluster().Save(item)
require.NoError(t, err)
}
// Create some shared channel remotes
scrData := []*model.SharedChannelRemote{
{ChannelId: channel1.Id, RemoteId: rcData[0].RemoteId, CreatorId: model.NewId()},
{ChannelId: channel1.Id, RemoteId: rcData[1].RemoteId, CreatorId: model.NewId()},
{ChannelId: channel2.Id, RemoteId: rcData[2].RemoteId, CreatorId: model.NewId()},
{ChannelId: channel2.Id, RemoteId: rcData[3].RemoteId, CreatorId: model.NewId()},
{ChannelId: channel3.Id, RemoteId: rcData[4].RemoteId, CreatorId: model.NewId()},
}
for _, item := range scrData {
_, err := ss.SharedChannel().SaveRemote(item)
require.NoError(t, err)
}
t.Run("Channel 1", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
NotInChannel: channel1.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
require.Len(t, list, 3, "channel 1 should have 3 remote clusters that are not already members")
ids := getIds(list)
require.ElementsMatch(t, []string{rcData[2].RemoteId, rcData[3].RemoteId, rcData[4].RemoteId}, ids)
})
t.Run("Channel 2", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
NotInChannel: channel2.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
require.Len(t, list, 3, "channel 2 should have 3 remote clusters that are not already members")
ids := getIds(list)
require.ElementsMatch(t, []string{rcData[0].RemoteId, rcData[1].RemoteId, rcData[4].RemoteId}, ids)
})
t.Run("Channel 3", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
NotInChannel: channel3.Id,
}
list, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
require.Len(t, list, 4, "channel 3 should have 4 remote clusters that are not already members")
ids := getIds(list)
require.ElementsMatch(t, []string{rcData[0].RemoteId, rcData[1].RemoteId, rcData[2].RemoteId, rcData[3].RemoteId}, ids)
})
t.Run("Channel with no share remotes", func(t *testing.T) {
filter := model.RemoteClusterQueryFilter{
NotInChannel: model.NewId(),
}
list, err := ss.RemoteCluster().GetAll(filter)
require.NoError(t, err)
require.Len(t, list, 5, "should have 5 remote clusters that are not already members")
ids := getIds(list)
require.ElementsMatch(t, []string{rcData[0].RemoteId, rcData[1].RemoteId, rcData[2].RemoteId, rcData[3].RemoteId,
rcData[4].RemoteId}, ids)
})
}
func getIds(remotes []*model.RemoteCluster) []string {
ids := make([]string, 0, len(remotes))
for _, r := range remotes {
ids = append(ids, r.RemoteId)
}
return ids
}
func testRemoteClusterGetByTopic(t *testing.T, ss store.Store) {
require.NoError(t, clearRemoteClusters(ss))
rcData := []*model.RemoteCluster{
{Name: "AAAA_Inc", CreatorId: model.NewId(), SiteURL: "aaaa.com", RemoteId: model.NewId(), Topics: ""},
{Name: "BBBB_Inc", CreatorId: model.NewId(), SiteURL: "bbbb.com", RemoteId: model.NewId(), Topics: " share "},
{Name: "CCCC_Inc", CreatorId: model.NewId(), SiteURL: "cccc.com", RemoteId: model.NewId(), Topics: " incident share "},
{Name: "DDDD_Inc", CreatorId: model.NewId(), SiteURL: "dddd.com", RemoteId: model.NewId(), Topics: " bogus "},
{Name: "EEEE_Inc", CreatorId: model.NewId(), SiteURL: "eeee.com", RemoteId: model.NewId(), Topics: " logs share incident "},
{Name: "FFFF_Inc", CreatorId: model.NewId(), SiteURL: "ffff.com", RemoteId: model.NewId(), Topics: " bogus incident "},
{Name: "GGGG_Inc", CreatorId: model.NewId(), SiteURL: "gggg.com", RemoteId: model.NewId(), Topics: "*"},
}
for _, item := range rcData {
_, err := ss.RemoteCluster().Save(item)
require.NoError(t, err)
}
testData := []struct {
topic string
expectedCount int
expectError bool
}{
{topic: "", expectedCount: 7, expectError: false},
{topic: " ", expectedCount: 0, expectError: true},
{topic: "share", expectedCount: 4},
{topic: " share ", expectedCount: 4},
{topic: "bogus", expectedCount: 3},
{topic: "non-existent", expectedCount: 1},
{topic: "*", expectedCount: 0, expectError: true}, // can't query with wildcard
}
for _, tt := range testData {
filter := model.RemoteClusterQueryFilter{
Topic: tt.topic,
}
list, err := ss.RemoteCluster().GetAll(filter)
if tt.expectError {
assert.Errorf(t, err, "expected error for topic=%s", tt.topic)
} else {
assert.NoErrorf(t, err, "expected no error for topic=%s", tt.topic)
}
assert.Lenf(t, list, tt.expectedCount, "topic=%s", tt.topic)
}
}
func testRemoteClusterUpdateTopics(t *testing.T, ss store.Store) {
remoteId := model.NewId()
rc := &model.RemoteCluster{
DisplayName: "Blap Inc",
Name: "blap",
SiteURL: "blap.com",
RemoteId: remoteId,
Topics: "",
CreatorId: model.NewId(),
}
_, err := ss.RemoteCluster().Save(rc)
require.NoError(t, err)
testData := []struct {
topics string
expected string
}{
{topics: "", expected: ""},
{topics: " ", expected: ""},
{topics: "share", expected: " share "},
{topics: " share ", expected: " share "},
{topics: "share incident", expected: " share incident "},
{topics: " share incident ", expected: " share incident "},
}
for _, tt := range testData {
_, err = ss.RemoteCluster().UpdateTopics(remoteId, tt.topics)
require.NoError(t, err)
rcUpdated, err := ss.RemoteCluster().Get(remoteId)
require.NoError(t, err)
require.Equal(t, tt.expected, rcUpdated.Topics)
}
}
func clearRemoteClusters(ss store.Store) error {
list, err := ss.RemoteCluster().GetAll(model.RemoteClusterQueryFilter{})
if err != nil {
return err
}
for _, rc := range list {
if _, err := ss.RemoteCluster().Delete(rc.RemoteId); err != nil {
return err
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"sort"
"strconv"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestRetentionPolicyStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("Save", func(t *testing.T) { testRetentionPolicyStoreSave(t, ss, s) })
t.Run("Patch", func(t *testing.T) { testRetentionPolicyStorePatch(t, ss, s) })
t.Run("Get", func(t *testing.T) { testRetentionPolicyStoreGet(t, ss, s) })
t.Run("GetCount", func(t *testing.T) { testRetentionPolicyStoreGetCount(t, ss, s) })
t.Run("Delete", func(t *testing.T) { testRetentionPolicyStoreDelete(t, ss, s) })
t.Run("GetChannels", func(t *testing.T) { testRetentionPolicyStoreGetChannels(t, ss, s) })
t.Run("AddChannels", func(t *testing.T) { testRetentionPolicyStoreAddChannels(t, ss, s) })
t.Run("RemoveChannels", func(t *testing.T) { testRetentionPolicyStoreRemoveChannels(t, ss, s) })
t.Run("GetTeams", func(t *testing.T) { testRetentionPolicyStoreGetTeams(t, ss, s) })
t.Run("AddTeams", func(t *testing.T) { testRetentionPolicyStoreAddTeams(t, ss, s) })
t.Run("RemoveTeams", func(t *testing.T) { testRetentionPolicyStoreRemoveTeams(t, ss, s) })
t.Run("RemoveOrphanedRows", func(t *testing.T) { testRetentionPolicyStoreRemoveOrphanedRows(t, ss, s) })
t.Run("GetPoliciesForUser", func(t *testing.T) { testRetentionPolicyStoreGetPoliciesForUser(t, ss, s) })
}
func getRetentionPolicyWithTeamAndChannelIds(t *testing.T, ss store.Store, policyID string) *model.RetentionPolicyWithTeamAndChannelIDs {
policyWithCounts, err := ss.RetentionPolicy().Get(policyID)
require.NoError(t, err)
policyWithIds := model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
ID: policyID,
DisplayName: policyWithCounts.DisplayName,
PostDurationDays: policyWithCounts.PostDurationDays,
},
ChannelIDs: make([]string, int(policyWithCounts.ChannelCount)),
TeamIDs: make([]string, int(policyWithCounts.TeamCount)),
}
channels, err := ss.RetentionPolicy().GetChannels(policyID, 0, 1000)
require.NoError(t, err)
for i, channel := range channels {
policyWithIds.ChannelIDs[i] = channel.Id
}
teams, err := ss.RetentionPolicy().GetTeams(policyID, 0, 1000)
require.NoError(t, err)
for i, team := range teams {
policyWithIds.TeamIDs[i] = team.Id
}
return &policyWithIds
}
func CheckRetentionPolicyWithTeamAndChannelIdsAreEqual(t *testing.T, p1, p2 *model.RetentionPolicyWithTeamAndChannelIDs) {
require.Equal(t, p1.ID, p2.ID)
require.Equal(t, p1.DisplayName, p2.DisplayName)
require.Equal(t, p1.PostDurationDays, p2.PostDurationDays)
require.Equal(t, len(p1.ChannelIDs), len(p2.ChannelIDs))
if p1.ChannelIDs == nil || p2.ChannelIDs == nil {
require.Equal(t, p1.ChannelIDs, p2.ChannelIDs)
} else {
sort.Strings(p1.ChannelIDs)
sort.Strings(p2.ChannelIDs)
}
for i := range p1.ChannelIDs {
require.Equal(t, p1.ChannelIDs[i], p2.ChannelIDs[i])
}
if p1.TeamIDs == nil || p2.TeamIDs == nil {
require.Equal(t, p1.TeamIDs, p2.TeamIDs)
} else {
sort.Strings(p1.TeamIDs)
sort.Strings(p2.TeamIDs)
}
require.Equal(t, len(p1.TeamIDs), len(p2.TeamIDs))
for i := range p1.TeamIDs {
require.Equal(t, p1.TeamIDs[i], p2.TeamIDs[i])
}
}
func CheckRetentionPolicyWithTeamAndChannelCountsAreEqual(t *testing.T, p1, p2 *model.RetentionPolicyWithTeamAndChannelCounts) {
require.Equal(t, p1.ID, p2.ID)
require.Equal(t, p1.DisplayName, p2.DisplayName)
require.Equal(t, p1.PostDurationDays, p2.PostDurationDays)
require.Equal(t, p1.ChannelCount, p2.ChannelCount)
require.Equal(t, p1.TeamCount, p2.TeamCount)
}
func checkRetentionPolicyLikeThisExists(t *testing.T, ss store.Store, expected *model.RetentionPolicyWithTeamAndChannelIDs) {
retrieved := getRetentionPolicyWithTeamAndChannelIds(t, ss, expected.ID)
CheckRetentionPolicyWithTeamAndChannelIdsAreEqual(t, expected, retrieved)
}
func copyRetentionPolicyWithTeamAndChannelIds(policy *model.RetentionPolicyWithTeamAndChannelIDs) *model.RetentionPolicyWithTeamAndChannelIDs {
cpy := &model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: policy.RetentionPolicy,
ChannelIDs: make([]string, len(policy.ChannelIDs)),
TeamIDs: make([]string, len(policy.TeamIDs)),
}
copy(cpy.ChannelIDs, policy.ChannelIDs)
copy(cpy.TeamIDs, policy.TeamIDs)
return cpy
}
func createChannelsForRetentionPolicy(t *testing.T, ss store.Store, teamId string, numChannels int) (channelIDs []string) {
channelIDs = make([]string, numChannels)
for i := range channelIDs {
name := "channel" + model.NewId()
channel := &model.Channel{
TeamId: teamId,
DisplayName: "Channel " + name,
Name: name,
Type: model.ChannelTypeOpen,
}
channel, err := ss.Channel().Save(channel, -1)
require.NoError(t, err)
channelIDs[i] = channel.Id
}
return
}
func createTeamsForRetentionPolicy(t *testing.T, ss store.Store, numTeams int) (teamIDs []string) {
teamIDs = make([]string, numTeams)
for i := range teamIDs {
name := "team" + model.NewId()
team := &model.Team{
DisplayName: "Team " + name,
Name: name,
Type: model.TeamOpen,
}
team, err := ss.Team().Save(team)
require.NoError(t, err)
teamIDs[i] = team.Id
}
return
}
func createTeamsAndChannelsForRetentionPolicy(t *testing.T, ss store.Store) (teamIDs, channelIDs []string) {
teamIDs = createTeamsForRetentionPolicy(t, ss, 2)
channels1 := createChannelsForRetentionPolicy(t, ss, teamIDs[0], 1)
channels2 := createChannelsForRetentionPolicy(t, ss, teamIDs[1], 2)
channelIDs = append(channels1, channels2...)
return
}
func cleanupRetentionPolicyTest(s SqlStore) {
// Manually clear tables until testlib can handle cleanups
tables := []string{"RetentionPolicies", "RetentionPoliciesChannels", "RetentionPoliciesTeams"}
for _, table := range tables {
if _, err := s.GetMasterX().Exec("DELETE FROM " + table); err != nil {
panic(err)
}
}
}
func deleteTeamsAndChannels(ss store.Store, teamIDs, channelIDs []string) {
for _, teamID := range teamIDs {
if err := ss.Team().PermanentDelete(teamID); err != nil {
panic(err)
}
}
for _, channelID := range channelIDs {
if err := ss.Channel().PermanentDelete(channelID); err != nil {
panic(err)
}
}
}
func createRetentionPolicyWithTeamAndChannelIds(displayName string, teamIDs, channelIDs []string) *model.RetentionPolicyWithTeamAndChannelIDs {
return &model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: displayName,
PostDurationDays: model.NewInt64(30),
},
TeamIDs: teamIDs,
ChannelIDs: channelIDs,
}
}
// saveRetentionPolicyWithTeamAndChannelIds creates a model.RetentionPolicyWithTeamAndChannelIds struct using
// the display name, team IDs, and channel IDs. The new policy ID will be assigned to the struct and returned.
// The team IDs and channel IDs are kept the same.
func saveRetentionPolicyWithTeamAndChannelIds(t *testing.T, ss store.Store, displayName string, teamIDs, channelIDs []string) *model.RetentionPolicyWithTeamAndChannelIDs {
proposal := createRetentionPolicyWithTeamAndChannelIds(displayName, teamIDs, channelIDs)
policyWithCounts, err := ss.RetentionPolicy().Save(proposal)
require.NoError(t, err)
proposal.ID = policyWithCounts.ID
return proposal
}
func restoreRetentionPolicy(t *testing.T, ss store.Store, policy *model.RetentionPolicyWithTeamAndChannelIDs) {
_, err := ss.RetentionPolicy().Patch(policy)
require.NoError(t, err)
checkRetentionPolicyLikeThisExists(t, ss, policy)
}
func testRetentionPolicyStoreSave(t *testing.T, ss store.Store, s SqlStore) {
defer cleanupRetentionPolicyTest(s)
t.Run("teams and channels are nil", func(t *testing.T) {
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1", nil, nil)
policy.ChannelIDs = []string{}
policy.TeamIDs = []string{}
checkRetentionPolicyLikeThisExists(t, ss, policy)
})
t.Run("teams and channels are empty", func(t *testing.T) {
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 2", []string{}, []string{})
checkRetentionPolicyLikeThisExists(t, ss, policy)
})
t.Run("some teams and channels are specified", func(t *testing.T) {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 3", teamIDs, channelIDs)
checkRetentionPolicyLikeThisExists(t, ss, policy)
})
t.Run("team specified does not exist", func(t *testing.T) {
policy := createRetentionPolicyWithTeamAndChannelIds("Policy 4", []string{"no_such_team"}, []string{})
_, err := ss.RetentionPolicy().Save(policy)
require.Error(t, err)
})
t.Run("channel specified does not exist", func(t *testing.T) {
policy := createRetentionPolicyWithTeamAndChannelIds("Policy 5", []string{}, []string{"no_such_channel"})
_, err := ss.RetentionPolicy().Save(policy)
require.Error(t, err)
})
}
func testRetentionPolicyStorePatch(t *testing.T, ss store.Store, s SqlStore) {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1", teamIDs, channelIDs)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
defer cleanupRetentionPolicyTest(s)
t.Run("modify DisplayName", func(t *testing.T) {
patch := &model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
ID: policy.ID,
DisplayName: "something new",
},
}
_, err := ss.RetentionPolicy().Patch(patch)
require.NoError(t, err)
expected := copyRetentionPolicyWithTeamAndChannelIds(policy)
expected.DisplayName = patch.DisplayName
checkRetentionPolicyLikeThisExists(t, ss, expected)
restoreRetentionPolicy(t, ss, policy)
})
t.Run("modify PostDuration", func(t *testing.T) {
patch := &model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
ID: policy.ID,
PostDurationDays: model.NewInt64(10000),
},
}
_, err := ss.RetentionPolicy().Patch(patch)
require.NoError(t, err)
expected := copyRetentionPolicyWithTeamAndChannelIds(policy)
expected.PostDurationDays = patch.PostDurationDays
checkRetentionPolicyLikeThisExists(t, ss, expected)
// Store a negative value (= infinity)
patch.PostDurationDays = model.NewInt64(-1)
_, err = ss.RetentionPolicy().Patch(patch)
require.NoError(t, err)
expected = copyRetentionPolicyWithTeamAndChannelIds(policy)
expected.PostDurationDays = patch.PostDurationDays
checkRetentionPolicyLikeThisExists(t, ss, expected)
restoreRetentionPolicy(t, ss, policy)
})
t.Run("clear TeamIds", func(t *testing.T) {
patch := &model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
ID: policy.ID,
},
TeamIDs: make([]string, 0),
}
_, err := ss.RetentionPolicy().Patch(patch)
require.NoError(t, err)
expected := copyRetentionPolicyWithTeamAndChannelIds(policy)
expected.TeamIDs = make([]string, 0)
checkRetentionPolicyLikeThisExists(t, ss, expected)
restoreRetentionPolicy(t, ss, policy)
})
t.Run("add team which does not exist", func(t *testing.T) {
patch := &model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
ID: policy.ID,
},
TeamIDs: []string{"no_such_team"},
}
_, err := ss.RetentionPolicy().Patch(patch)
require.Error(t, err)
})
t.Run("clear ChannelIds", func(t *testing.T) {
patch := &model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
ID: policy.ID,
},
ChannelIDs: make([]string, 0),
}
_, err := ss.RetentionPolicy().Patch(patch)
require.NoError(t, err)
expected := copyRetentionPolicyWithTeamAndChannelIds(policy)
expected.ChannelIDs = make([]string, 0)
checkRetentionPolicyLikeThisExists(t, ss, expected)
restoreRetentionPolicy(t, ss, policy)
})
t.Run("add channel which does not exist", func(t *testing.T) {
patch := &model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
ID: policy.ID,
},
ChannelIDs: []string{"no_such_channel"},
}
_, err := ss.RetentionPolicy().Patch(patch)
require.Error(t, err)
})
}
func testRetentionPolicyStoreGet(t *testing.T, ss store.Store, s SqlStore) {
t.Run("get none", func(t *testing.T) {
retrievedPolicies, err := ss.RetentionPolicy().GetAll(0, 10)
require.NoError(t, err)
require.NotNil(t, retrievedPolicies)
require.Equal(t, 0, len(retrievedPolicies))
})
// create multiple policies
policiesWithCounts := make([]*model.RetentionPolicyWithTeamAndChannelCounts, 0)
for i := 0; i < 3; i++ {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
policyWithIds := createRetentionPolicyWithTeamAndChannelIds(
"Policy "+strconv.Itoa(i+1), teamIDs, channelIDs)
policyWithCounts, err := ss.RetentionPolicy().Save(policyWithIds)
require.NoError(t, err)
policiesWithCounts = append(policiesWithCounts, policyWithCounts)
}
defer cleanupRetentionPolicyTest(s)
t.Run("get all", func(t *testing.T) {
retrievedPolicies, err := ss.RetentionPolicy().GetAll(0, 60)
require.NoError(t, err)
require.Equal(t, len(policiesWithCounts), len(retrievedPolicies))
for i := range policiesWithCounts {
CheckRetentionPolicyWithTeamAndChannelCountsAreEqual(t, policiesWithCounts[i], retrievedPolicies[i])
}
})
t.Run("get all with limit", func(t *testing.T) {
for i := range policiesWithCounts {
retrievedPolicies, err := ss.RetentionPolicy().GetAll(i, 1)
require.NoError(t, err)
require.Equal(t, 1, len(retrievedPolicies))
CheckRetentionPolicyWithTeamAndChannelCountsAreEqual(t, policiesWithCounts[i], retrievedPolicies[0])
}
})
t.Run("get all with same display name", func(t *testing.T) {
for i := 0; i < 5; i++ {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
proposal := createRetentionPolicyWithTeamAndChannelIds(
"Policy Name", teamIDs, channelIDs)
_, err := ss.RetentionPolicy().Save(proposal)
require.NoError(t, err)
}
policies, err := ss.RetentionPolicy().GetAll(0, 60)
require.NoError(t, err)
for i := 1; i < len(policies); i++ {
require.True(t,
policies[i-1].DisplayName < policies[i].DisplayName ||
(policies[i-1].DisplayName == policies[i].DisplayName &&
policies[i-1].ID < policies[i].ID),
"policies with the same display name should be sorted by ID")
}
})
}
func testRetentionPolicyStoreGetCount(t *testing.T, ss store.Store, s SqlStore) {
defer cleanupRetentionPolicyTest(s)
t.Run("no policies", func(t *testing.T) {
count, err := ss.RetentionPolicy().GetCount()
require.NoError(t, err)
require.Equal(t, int64(0), count)
})
t.Run("some policies", func(t *testing.T) {
for i := 0; i < 2; i++ {
saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy "+strconv.Itoa(i), nil, nil)
}
count, err := ss.RetentionPolicy().GetCount()
require.NoError(t, err)
require.Equal(t, int64(2), count)
})
}
func testRetentionPolicyStoreDelete(t *testing.T, ss store.Store, s SqlStore) {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1", teamIDs, channelIDs)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
defer cleanupRetentionPolicyTest(s)
t.Run("delete policy", func(t *testing.T) {
err := ss.RetentionPolicy().Delete(policy.ID)
require.NoError(t, err)
policies, err := ss.RetentionPolicy().GetAll(0, 1)
require.NoError(t, err)
require.Empty(t, policies)
})
}
func testRetentionPolicyStoreGetChannels(t *testing.T, ss store.Store, s SqlStore) {
defer cleanupRetentionPolicyTest(s)
t.Run("no channels", func(t *testing.T) {
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1", nil, nil)
channels, err := ss.RetentionPolicy().GetChannels(policy.ID, 0, 1)
require.NoError(t, err)
require.Len(t, channels, 0)
})
t.Run("some channels", func(t *testing.T) {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 2", teamIDs, channelIDs)
channels, err := ss.RetentionPolicy().GetChannels(policy.ID, 0, len(channelIDs))
require.NoError(t, err)
require.Len(t, channels, len(channelIDs))
sort.Strings(channelIDs)
sort.Slice(channels, func(i, j int) bool {
return channels[i].Id < channels[j].Id
})
for i := range channelIDs {
require.Equal(t, channelIDs[i], channels[i].Id)
}
})
}
func testRetentionPolicyStoreAddChannels(t *testing.T, ss store.Store, s SqlStore) {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1", teamIDs, channelIDs)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
defer cleanupRetentionPolicyTest(s)
t.Run("add empty array", func(t *testing.T) {
err := ss.RetentionPolicy().AddChannels(policy.ID, []string{})
require.NoError(t, err)
checkRetentionPolicyLikeThisExists(t, ss, policy)
})
t.Run("add new channels", func(t *testing.T) {
channelIDs := createChannelsForRetentionPolicy(t, ss, teamIDs[0], 2)
defer deleteTeamsAndChannels(ss, nil, channelIDs)
err := ss.RetentionPolicy().AddChannels(policy.ID, channelIDs)
require.NoError(t, err)
// verify that the channels were actually added
copy := copyRetentionPolicyWithTeamAndChannelIds(policy)
copy.ChannelIDs = append(copy.ChannelIDs, channelIDs...)
checkRetentionPolicyLikeThisExists(t, ss, copy)
restoreRetentionPolicy(t, ss, policy)
})
t.Run("add channel which does not exist", func(t *testing.T) {
err := ss.RetentionPolicy().AddChannels(policy.ID, []string{"no_such_channel"})
require.Error(t, err)
})
t.Run("add channel to policy which does not exist", func(t *testing.T) {
channelIDs := createChannelsForRetentionPolicy(t, ss, teamIDs[0], 1)
defer deleteTeamsAndChannels(ss, nil, channelIDs)
err := ss.RetentionPolicy().AddChannels("no_such_policy", channelIDs)
require.Error(t, err)
})
}
func testRetentionPolicyStoreRemoveChannels(t *testing.T, ss store.Store, s SqlStore) {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1", teamIDs, channelIDs)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
defer cleanupRetentionPolicyTest(s)
t.Run("remove empty array", func(t *testing.T) {
err := ss.RetentionPolicy().RemoveChannels(policy.ID, []string{})
require.NoError(t, err)
checkRetentionPolicyLikeThisExists(t, ss, policy)
})
t.Run("remove existing channel", func(t *testing.T) {
channelID := channelIDs[0]
err := ss.RetentionPolicy().RemoveChannels(policy.ID, []string{channelID})
require.NoError(t, err)
// verify that the channel was actually removed
copy := copyRetentionPolicyWithTeamAndChannelIds(policy)
copy.ChannelIDs = make([]string, 0)
for _, oldChannelID := range policy.ChannelIDs {
if oldChannelID != channelID {
copy.ChannelIDs = append(copy.ChannelIDs, oldChannelID)
}
}
checkRetentionPolicyLikeThisExists(t, ss, copy)
restoreRetentionPolicy(t, ss, policy)
})
t.Run("remove channel which does not exist", func(t *testing.T) {
err := ss.RetentionPolicy().RemoveChannels(policy.ID, []string{"no_such_channel"})
require.NoError(t, err)
// verify that the policy did not change
checkRetentionPolicyLikeThisExists(t, ss, policy)
})
}
func testRetentionPolicyStoreGetTeams(t *testing.T, ss store.Store, s SqlStore) {
defer cleanupRetentionPolicyTest(s)
t.Run("no teams", func(t *testing.T) {
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1", nil, nil)
teams, err := ss.RetentionPolicy().GetTeams(policy.ID, 0, 1)
require.NoError(t, err)
require.Len(t, teams, 0)
})
t.Run("some teams", func(t *testing.T) {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 2", teamIDs, channelIDs)
teams, err := ss.RetentionPolicy().GetTeams(policy.ID, 0, len(teamIDs))
require.NoError(t, err)
require.Len(t, teams, len(teamIDs))
sort.Strings(teamIDs)
sort.Slice(teams, func(i, j int) bool {
return teams[i].Id < teams[j].Id
})
for i := range teamIDs {
require.Equal(t, teamIDs[i], teams[i].Id)
}
})
}
func testRetentionPolicyStoreAddTeams(t *testing.T, ss store.Store, s SqlStore) {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1", teamIDs, channelIDs)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
defer cleanupRetentionPolicyTest(s)
t.Run("add empty array", func(t *testing.T) {
err := ss.RetentionPolicy().AddTeams(policy.ID, []string{})
require.NoError(t, err)
checkRetentionPolicyLikeThisExists(t, ss, policy)
})
t.Run("add new teams", func(t *testing.T) {
teamIDs := createTeamsForRetentionPolicy(t, ss, 2)
defer deleteTeamsAndChannels(ss, teamIDs, nil)
err := ss.RetentionPolicy().AddTeams(policy.ID, teamIDs)
require.NoError(t, err)
// verify that the teams were actually added
copy := copyRetentionPolicyWithTeamAndChannelIds(policy)
copy.TeamIDs = append(copy.TeamIDs, teamIDs...)
checkRetentionPolicyLikeThisExists(t, ss, copy)
restoreRetentionPolicy(t, ss, policy)
})
t.Run("add team which does not exist", func(t *testing.T) {
err := ss.RetentionPolicy().AddTeams(policy.ID, []string{"no_such_team"})
require.Error(t, err)
})
t.Run("add team to policy which does not exist", func(t *testing.T) {
teamIDs := createTeamsForRetentionPolicy(t, ss, 1)
defer deleteTeamsAndChannels(ss, teamIDs, nil)
err := ss.RetentionPolicy().AddTeams("no_such_policy", teamIDs)
require.Error(t, err)
})
}
func testRetentionPolicyStoreRemoveTeams(t *testing.T, ss store.Store, s SqlStore) {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1", teamIDs, channelIDs)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
defer cleanupRetentionPolicyTest(s)
t.Run("remove empty array", func(t *testing.T) {
err := ss.RetentionPolicy().RemoveTeams(policy.ID, []string{})
require.NoError(t, err)
checkRetentionPolicyLikeThisExists(t, ss, policy)
})
t.Run("remove existing team", func(t *testing.T) {
teamID := teamIDs[0]
err := ss.RetentionPolicy().RemoveTeams(policy.ID, []string{teamID})
require.NoError(t, err)
// verify that the team was actually removed
copy := copyRetentionPolicyWithTeamAndChannelIds(policy)
copy.TeamIDs = make([]string, 0)
for _, oldTeamID := range policy.TeamIDs {
if oldTeamID != teamID {
copy.TeamIDs = append(copy.TeamIDs, oldTeamID)
}
}
checkRetentionPolicyLikeThisExists(t, ss, copy)
restoreRetentionPolicy(t, ss, policy)
})
t.Run("remove team which does not exist", func(t *testing.T) {
err := ss.RetentionPolicy().RemoveTeams(policy.ID, []string{"no_such_team"})
require.NoError(t, err)
// verify that the policy did not change
checkRetentionPolicyLikeThisExists(t, ss, policy)
})
}
func testRetentionPolicyStoreGetPoliciesForUser(t *testing.T, ss store.Store, s SqlStore) {
teamIDs, channelIDs := createTeamsAndChannelsForRetentionPolicy(t, ss)
saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1", teamIDs, channelIDs)
defer deleteTeamsAndChannels(ss, teamIDs, channelIDs)
defer cleanupRetentionPolicyTest(s)
user, userSaveErr := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: model.NewId(),
})
require.NoError(t, userSaveErr)
t.Run("user has no relevant policies", func(t *testing.T) {
// Teams
teamPolicies, err := ss.RetentionPolicy().GetTeamPoliciesForUser(user.Id, 0, 100)
require.NoError(t, err)
require.Empty(t, teamPolicies)
count, err := ss.RetentionPolicy().GetTeamPoliciesCountForUser(user.Id)
require.NoError(t, err)
require.Equal(t, int64(0), count)
// Channels
channelPolicies, err := ss.RetentionPolicy().GetChannelPoliciesForUser(user.Id, 0, 100)
require.NoError(t, err)
require.Empty(t, channelPolicies)
count, err = ss.RetentionPolicy().GetChannelPoliciesCountForUser(user.Id)
require.NoError(t, err)
require.Equal(t, int64(0), count)
})
t.Run("user has relevant policies", func(t *testing.T) {
for _, teamID := range teamIDs {
_, err := ss.Team().SaveMember(&model.TeamMember{TeamId: teamID, UserId: user.Id}, -1)
require.NoError(t, err)
}
for _, channelID := range channelIDs {
_, err := ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channelID, UserId: user.Id, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, err)
}
// Teams
teamPolicies, err := ss.RetentionPolicy().GetTeamPoliciesForUser(user.Id, 0, 100)
require.NoError(t, err)
require.Len(t, teamPolicies, len(teamIDs))
count, err := ss.RetentionPolicy().GetTeamPoliciesCountForUser(user.Id)
require.NoError(t, err)
require.Equal(t, int64(len(teamIDs)), count)
// Channels
channelPolicies, err := ss.RetentionPolicy().GetChannelPoliciesForUser(user.Id, 0, 100)
require.NoError(t, err)
require.Len(t, channelPolicies, len(channelIDs))
count, err = ss.RetentionPolicy().GetChannelPoliciesCountForUser(user.Id)
require.NoError(t, err)
require.Equal(t, int64(len(channelIDs)), count)
})
}
func testRetentionPolicyStoreRemoveOrphanedRows(t *testing.T, ss store.Store, s SqlStore) {
teamID := createTeamsForRetentionPolicy(t, ss, 1)[0]
channelID := createChannelsForRetentionPolicy(t, ss, teamID, 1)[0]
policy := saveRetentionPolicyWithTeamAndChannelIds(t, ss, "Policy 1",
[]string{teamID}, []string{channelID})
err := ss.Channel().PermanentDelete(channelID)
require.NoError(t, err)
err = ss.Team().PermanentDelete(teamID)
require.NoError(t, err)
_, err = ss.RetentionPolicy().DeleteOrphanedRows(1000)
require.NoError(t, err)
policy.ChannelIDs = make([]string, 0)
policy.TeamIDs = make([]string, 0)
checkRetentionPolicyLikeThisExists(t, ss, policy)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestRoleStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("Save", func(t *testing.T) { testRoleStoreSave(t, ss) })
t.Run("Get", func(t *testing.T) { testRoleStoreGet(t, ss) })
t.Run("GetAll", func(t *testing.T) { testRoleStoreGetAll(t, ss) })
t.Run("GetByName", func(t *testing.T) { testRoleStoreGetByName(t, ss) })
t.Run("GetNames", func(t *testing.T) { testRoleStoreGetByNames(t, ss) })
t.Run("Delete", func(t *testing.T) { testRoleStoreDelete(t, ss) })
t.Run("PermanentDeleteAll", func(t *testing.T) { testRoleStorePermanentDeleteAll(t, ss) })
t.Run("LowerScopedChannelSchemeRoles_AllChannelSchemeRoles", func(t *testing.T) { testRoleStoreLowerScopedChannelSchemeRoles(t, ss) })
t.Run("ChannelHigherScopedPermissionsBlankTeamSchemeChannelGuest", func(t *testing.T) { testRoleStoreChannelHigherScopedPermissionsBlankTeamSchemeChannelGuest(t, ss, s) })
}
func testRoleStoreSave(t *testing.T, ss store.Store) {
// Save a new role.
r1 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
d1, err := ss.Role().Save(r1)
assert.NoError(t, err)
assert.Len(t, d1.Id, 26)
assert.Equal(t, r1.Name, d1.Name)
assert.Equal(t, r1.DisplayName, d1.DisplayName)
assert.Equal(t, r1.Description, d1.Description)
assert.Equal(t, r1.Permissions, d1.Permissions)
assert.Equal(t, r1.SchemeManaged, d1.SchemeManaged)
// Change the role permissions and update.
d1.Permissions = []string{
"invite_user",
"add_user_to_team",
"delete_public_channel",
}
d2, err := ss.Role().Save(d1)
assert.NoError(t, err)
assert.Len(t, d2.Id, 26)
assert.Equal(t, r1.Name, d2.Name)
assert.Equal(t, r1.DisplayName, d2.DisplayName)
assert.Equal(t, r1.Description, d2.Description)
assert.Equal(t, d1.Permissions, d2.Permissions)
assert.Equal(t, r1.SchemeManaged, d2.SchemeManaged)
// Try saving one with an invalid ID set.
r3 := &model.Role{
Id: model.NewId(),
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
_, err = ss.Role().Save(r3)
assert.Error(t, err)
// Try saving one with a duplicate "name" field.
r4 := &model.Role{
Name: r1.Name,
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
_, err = ss.Role().Save(r4)
assert.Error(t, err)
}
func testRoleStoreGetAll(t *testing.T, ss store.Store) {
prev, err := ss.Role().GetAll()
require.NoError(t, err)
prevCount := len(prev)
// Save a role to test with.
r1 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
_, err = ss.Role().Save(r1)
require.NoError(t, err)
r2 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
_, err = ss.Role().Save(r2)
require.NoError(t, err)
data, err := ss.Role().GetAll()
require.NoError(t, err)
assert.Len(t, data, prevCount+2)
}
func testRoleStoreGet(t *testing.T, ss store.Store) {
// Save a role to test with.
r1 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
d1, err := ss.Role().Save(r1)
assert.NoError(t, err)
assert.Len(t, d1.Id, 26)
// Get a valid role
d2, err := ss.Role().Get(d1.Id)
assert.NoError(t, err)
assert.Equal(t, d1.Id, d2.Id)
assert.Equal(t, r1.Name, d2.Name)
assert.Equal(t, r1.DisplayName, d2.DisplayName)
assert.Equal(t, r1.Description, d2.Description)
assert.Equal(t, r1.Permissions, d2.Permissions)
assert.Equal(t, r1.SchemeManaged, d2.SchemeManaged)
// Get an invalid role
_, err = ss.Role().Get(model.NewId())
assert.Error(t, err)
}
func testRoleStoreGetByName(t *testing.T, ss store.Store) {
// Save a role to test with.
r1 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
d1, err := ss.Role().Save(r1)
assert.NoError(t, err)
assert.Len(t, d1.Id, 26)
// Get a valid role
d2, err := ss.Role().GetByName(context.Background(), d1.Name)
assert.NoError(t, err)
assert.Equal(t, d1.Id, d2.Id)
assert.Equal(t, r1.Name, d2.Name)
assert.Equal(t, r1.DisplayName, d2.DisplayName)
assert.Equal(t, r1.Description, d2.Description)
assert.Equal(t, r1.Permissions, d2.Permissions)
assert.Equal(t, r1.SchemeManaged, d2.SchemeManaged)
// Get an invalid role
_, err = ss.Role().GetByName(context.Background(), model.NewId())
assert.Error(t, err)
}
func testRoleStoreGetByNames(t *testing.T, ss store.Store) {
// Save some roles to test with.
r1 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
r2 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"read_channel",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
r3 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"delete_private_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
d1, err := ss.Role().Save(r1)
assert.NoError(t, err)
assert.Len(t, d1.Id, 26)
d2, err := ss.Role().Save(r2)
assert.NoError(t, err)
assert.Len(t, d2.Id, 26)
d3, err := ss.Role().Save(r3)
assert.NoError(t, err)
assert.Len(t, d3.Id, 26)
// Get two valid roles.
n4 := []string{r1.Name, r2.Name}
roles4, err := ss.Role().GetByNames(n4)
assert.NoError(t, err)
assert.Len(t, roles4, 2)
assert.Contains(t, roles4, d1)
assert.Contains(t, roles4, d2)
assert.NotContains(t, roles4, d3)
// Get two invalid roles.
n5 := []string{model.NewId(), model.NewId()}
roles5, err := ss.Role().GetByNames(n5)
assert.NoError(t, err)
assert.Empty(t, roles5)
// Get one valid one and one invalid one.
n6 := []string{r1.Name, model.NewId()}
roles6, err := ss.Role().GetByNames(n6)
assert.NoError(t, err)
assert.Len(t, roles6, 1)
assert.Contains(t, roles6, d1)
assert.NotContains(t, roles6, d2)
assert.NotContains(t, roles6, d3)
}
func testRoleStoreDelete(t *testing.T, ss store.Store) {
// Save a role to test with.
r1 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
d1, err := ss.Role().Save(r1)
assert.NoError(t, err)
assert.Len(t, d1.Id, 26)
// Check the role is there.
_, err = ss.Role().Get(d1.Id)
assert.NoError(t, err)
// Delete the role.
_, err = ss.Role().Delete(d1.Id)
assert.NoError(t, err)
// Check the role is deleted there.
d2, err := ss.Role().Get(d1.Id)
assert.NoError(t, err)
assert.NotZero(t, d2.DeleteAt)
d3, err := ss.Role().GetByName(context.Background(), d1.Name)
assert.NoError(t, err)
assert.NotZero(t, d3.DeleteAt)
// Try and delete a role that does not exist.
_, err = ss.Role().Delete(model.NewId())
assert.Error(t, err)
}
func testRoleStorePermanentDeleteAll(t *testing.T, ss store.Store) {
r1 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"invite_user",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
r2 := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{
"read_channel",
"create_public_channel",
"add_user_to_team",
},
SchemeManaged: false,
}
_, err := ss.Role().Save(r1)
require.NoError(t, err)
_, err = ss.Role().Save(r2)
require.NoError(t, err)
roles, err := ss.Role().GetByNames([]string{r1.Name, r2.Name})
assert.NoError(t, err)
assert.Len(t, roles, 2)
err = ss.Role().PermanentDeleteAll()
assert.NoError(t, err)
roles, err = ss.Role().GetByNames([]string{r1.Name, r2.Name})
assert.NoError(t, err)
assert.Empty(t, roles)
}
func testRoleStoreLowerScopedChannelSchemeRoles(t *testing.T, ss store.Store) {
createDefaultRoles(ss)
teamScheme1 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
teamScheme1, err := ss.Scheme().Save(teamScheme1)
require.NoError(t, err)
defer ss.Scheme().Delete(teamScheme1.Id)
teamScheme2 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
teamScheme2, err = ss.Scheme().Save(teamScheme2)
require.NoError(t, err)
defer ss.Scheme().Delete(teamScheme2.Id)
channelScheme1 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
channelScheme1, err = ss.Scheme().Save(channelScheme1)
require.NoError(t, err)
defer ss.Scheme().Delete(channelScheme1.Id)
channelScheme2 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
channelScheme2, err = ss.Scheme().Save(channelScheme2)
require.NoError(t, err)
defer ss.Scheme().Delete(channelScheme1.Id)
team1 := &model.Team{
DisplayName: "Name",
Name: "zz" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &teamScheme1.Id,
}
team1, err = ss.Team().Save(team1)
require.NoError(t, err)
defer ss.Team().PermanentDelete(team1.Id)
team2 := &model.Team{
DisplayName: "Name",
Name: "zz" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &teamScheme2.Id,
}
team2, err = ss.Team().Save(team2)
require.NoError(t, err)
defer ss.Team().PermanentDelete(team2.Id)
channel1 := &model.Channel{
TeamId: team1.Id,
DisplayName: "Display " + model.NewId(),
Name: "zz" + model.NewId() + "b",
Type: model.ChannelTypeOpen,
SchemeId: &channelScheme1.Id,
}
channel1, nErr := ss.Channel().Save(channel1, -1)
require.NoError(t, nErr)
defer ss.Channel().Delete(channel1.Id, 0)
channel2 := &model.Channel{
TeamId: team2.Id,
DisplayName: "Display " + model.NewId(),
Name: "zz" + model.NewId() + "b",
Type: model.ChannelTypeOpen,
SchemeId: &channelScheme2.Id,
}
channel2, nErr = ss.Channel().Save(channel2, -1)
require.NoError(t, nErr)
defer ss.Channel().Delete(channel2.Id, 0)
t.Run("ChannelRolesUnderTeamRole", func(t *testing.T) {
t.Run("guest role for the right team's channels are returned", func(t *testing.T) {
actualRoles, err := ss.Role().ChannelRolesUnderTeamRole(teamScheme1.DefaultChannelGuestRole)
require.NoError(t, err)
var actualRoleNames []string
for _, role := range actualRoles {
actualRoleNames = append(actualRoleNames, role.Name)
}
require.Contains(t, actualRoleNames, channelScheme1.DefaultChannelGuestRole)
require.NotContains(t, actualRoleNames, channelScheme2.DefaultChannelGuestRole)
})
t.Run("user role for the right team's channels are returned", func(t *testing.T) {
actualRoles, err := ss.Role().ChannelRolesUnderTeamRole(teamScheme1.DefaultChannelUserRole)
require.NoError(t, err)
var actualRoleNames []string
for _, role := range actualRoles {
actualRoleNames = append(actualRoleNames, role.Name)
}
require.Contains(t, actualRoleNames, channelScheme1.DefaultChannelUserRole)
require.NotContains(t, actualRoleNames, channelScheme2.DefaultChannelUserRole)
})
t.Run("admin role for the right team's channels are returned", func(t *testing.T) {
actualRoles, err := ss.Role().ChannelRolesUnderTeamRole(teamScheme1.DefaultChannelAdminRole)
require.NoError(t, err)
var actualRoleNames []string
for _, role := range actualRoles {
actualRoleNames = append(actualRoleNames, role.Name)
}
require.Contains(t, actualRoleNames, channelScheme1.DefaultChannelAdminRole)
require.NotContains(t, actualRoleNames, channelScheme2.DefaultChannelAdminRole)
})
})
t.Run("AllChannelSchemeRoles", func(t *testing.T) {
t.Run("guest role for the right team's channels are returned", func(t *testing.T) {
actualRoles, err := ss.Role().AllChannelSchemeRoles()
require.NoError(t, err)
var actualRoleNames []string
for _, role := range actualRoles {
actualRoleNames = append(actualRoleNames, role.Name)
}
allRoleNames := []string{
channelScheme1.DefaultChannelGuestRole,
channelScheme2.DefaultChannelGuestRole,
channelScheme1.DefaultChannelUserRole,
channelScheme2.DefaultChannelUserRole,
channelScheme1.DefaultChannelAdminRole,
channelScheme2.DefaultChannelAdminRole,
}
for _, roleName := range allRoleNames {
require.Contains(t, actualRoleNames, roleName)
}
})
})
}
func testRoleStoreChannelHigherScopedPermissionsBlankTeamSchemeChannelGuest(t *testing.T, ss store.Store, s SqlStore) {
teamScheme := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
teamScheme, err := ss.Scheme().Save(teamScheme)
require.NoError(t, err)
defer ss.Scheme().Delete(teamScheme.Id)
channelScheme := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
channelScheme, err = ss.Scheme().Save(channelScheme)
require.NoError(t, err)
defer ss.Scheme().Delete(channelScheme.Id)
team := &model.Team{
DisplayName: "Name",
Name: "zz" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &teamScheme.Id,
}
team, err = ss.Team().Save(team)
require.NoError(t, err)
defer ss.Team().PermanentDelete(team.Id)
channel := &model.Channel{
TeamId: team.Id,
DisplayName: "Display " + model.NewId(),
Name: "zz" + model.NewId() + "b",
Type: model.ChannelTypeOpen,
SchemeId: &channelScheme.Id,
}
channel, nErr := ss.Channel().Save(channel, -1)
require.NoError(t, nErr)
defer ss.Channel().Delete(channel.Id, 0)
channelSchemeUserRole, err := ss.Role().GetByName(context.Background(), channelScheme.DefaultChannelUserRole)
require.NoError(t, err)
channelSchemeUserRole.Permissions = []string{}
_, err = ss.Role().Save(channelSchemeUserRole)
require.NoError(t, err)
teamSchemeUserRole, err := ss.Role().GetByName(context.Background(), teamScheme.DefaultChannelUserRole)
require.NoError(t, err)
teamSchemeUserRole.Permissions = []string{model.PermissionUploadFile.Id}
_, err = ss.Role().Save(teamSchemeUserRole)
require.NoError(t, err)
// get the channel scheme user role again and ensure that it has the permission inherited from the team
// scheme user role
roleMapBefore, err := ss.Role().ChannelHigherScopedPermissions([]string{channelSchemeUserRole.Name})
require.NoError(t, err)
// blank-out the guest role to simulate an old team scheme, ensure it's blank
result, sqlErr := s.GetMasterX().Exec(fmt.Sprintf("UPDATE Schemes SET DefaultChannelGuestRole = '' WHERE Id = '%s'", teamScheme.Id))
require.NoError(t, sqlErr)
rows, serr := result.RowsAffected()
require.NoError(t, serr)
require.Equal(t, int64(1), rows)
teamScheme, err = ss.Scheme().Get(teamScheme.Id)
require.NoError(t, err)
require.Equal(t, "", teamScheme.DefaultChannelGuestRole)
// trigger a cache clear
_, err = ss.Role().Save(channelSchemeUserRole)
require.NoError(t, err)
roleMapAfter, err := ss.Role().ChannelHigherScopedPermissions([]string{channelSchemeUserRole.Name})
require.NoError(t, err)
require.Equal(t, len(roleMapBefore), len(roleMapAfter))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestSchemeStore(t *testing.T, ss store.Store) {
createDefaultRoles(ss)
t.Run("Save", func(t *testing.T) { testSchemeStoreSave(t, ss) })
t.Run("Get", func(t *testing.T) { testSchemeStoreGet(t, ss) })
t.Run("GetAllPage", func(t *testing.T) { testSchemeStoreGetAllPage(t, ss) })
t.Run("Delete", func(t *testing.T) { testSchemeStoreDelete(t, ss) })
t.Run("PermanentDeleteAll", func(t *testing.T) { testSchemeStorePermanentDeleteAll(t, ss) })
t.Run("GetByName", func(t *testing.T) { testSchemeStoreGetByName(t, ss) })
t.Run("CountByScope", func(t *testing.T) { testSchemeStoreCountByScope(t, ss) })
t.Run("CountWithoutPermission", func(t *testing.T) { testCountWithoutPermission(t, ss) })
}
func createDefaultRoles(ss store.Store) {
ss.Role().Save(&model.Role{
Name: model.TeamAdminRoleId,
DisplayName: model.TeamAdminRoleId,
Permissions: []string{
model.PermissionDeleteOthersPosts.Id,
},
})
ss.Role().Save(&model.Role{
Name: model.TeamUserRoleId,
DisplayName: model.TeamUserRoleId,
Permissions: []string{
model.PermissionViewTeam.Id,
model.PermissionAddUserToTeam.Id,
},
})
ss.Role().Save(&model.Role{
Name: model.TeamGuestRoleId,
DisplayName: model.TeamGuestRoleId,
Permissions: []string{
model.PermissionViewTeam.Id,
},
})
ss.Role().Save(&model.Role{
Name: model.ChannelAdminRoleId,
DisplayName: model.ChannelAdminRoleId,
Permissions: []string{
model.PermissionManagePublicChannelMembers.Id,
model.PermissionManagePrivateChannelMembers.Id,
},
})
ss.Role().Save(&model.Role{
Name: model.ChannelUserRoleId,
DisplayName: model.ChannelUserRoleId,
Permissions: []string{
model.PermissionReadChannel.Id,
model.PermissionCreatePost.Id,
},
})
ss.Role().Save(&model.Role{
Name: model.ChannelGuestRoleId,
DisplayName: model.ChannelGuestRoleId,
Permissions: []string{
model.PermissionReadChannel.Id,
model.PermissionCreatePost.Id,
},
})
ss.Role().Save(&model.Role{
Name: model.PlaybookAdminRoleId,
DisplayName: model.PlaybookAdminRoleId,
Permissions: []string{
model.PermissionPrivatePlaybookManageMembers.Id,
},
})
ss.Role().Save(&model.Role{
Name: model.PlaybookMemberRoleId,
DisplayName: model.PlaybookMemberRoleId,
Permissions: []string{
model.PermissionPrivatePlaybookManageMembers.Id,
},
})
ss.Role().Save(&model.Role{
Name: model.RunAdminRoleId,
DisplayName: model.RunAdminRoleId,
Permissions: []string{
model.PermissionRunManageMembers.Id,
},
})
ss.Role().Save(&model.Role{
Name: model.RunMemberRoleId,
DisplayName: model.RunMemberRoleId,
Permissions: []string{
model.PermissionRunManageMembers.Id,
},
})
}
func testSchemeStoreSave(t *testing.T, ss store.Store) {
// Save a new scheme.
s1 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
// Check all fields saved correctly.
d1, err := ss.Scheme().Save(s1)
assert.NoError(t, err)
assert.Len(t, d1.Id, 26)
assert.Equal(t, s1.DisplayName, d1.DisplayName)
assert.Equal(t, s1.Name, d1.Name)
assert.Equal(t, s1.Description, d1.Description)
assert.NotZero(t, d1.CreateAt)
assert.NotZero(t, d1.UpdateAt)
assert.Zero(t, d1.DeleteAt)
assert.Equal(t, s1.Scope, d1.Scope)
assert.Len(t, d1.DefaultTeamAdminRole, 26)
assert.Len(t, d1.DefaultTeamUserRole, 26)
assert.Len(t, d1.DefaultTeamGuestRole, 26)
assert.Len(t, d1.DefaultChannelAdminRole, 26)
assert.Len(t, d1.DefaultChannelUserRole, 26)
assert.Len(t, d1.DefaultChannelGuestRole, 26)
// Check the default roles were created correctly.
role1, err := ss.Role().GetByName(context.Background(), d1.DefaultTeamAdminRole)
assert.NoError(t, err)
assert.Equal(t, role1.Permissions, []string{"delete_others_posts"})
assert.True(t, role1.SchemeManaged)
role2, err := ss.Role().GetByName(context.Background(), d1.DefaultTeamUserRole)
assert.NoError(t, err)
assert.Equal(t, role2.Permissions, []string{"view_team", "add_user_to_team"})
assert.True(t, role2.SchemeManaged)
role3, err := ss.Role().GetByName(context.Background(), d1.DefaultChannelAdminRole)
assert.NoError(t, err)
assert.Equal(t, role3.Permissions, []string{"manage_public_channel_members", "manage_private_channel_members"})
assert.True(t, role3.SchemeManaged)
role4, err := ss.Role().GetByName(context.Background(), d1.DefaultChannelUserRole)
assert.NoError(t, err)
assert.Equal(t, role4.Permissions, []string{"read_channel", "create_post"})
assert.True(t, role4.SchemeManaged)
role5, err := ss.Role().GetByName(context.Background(), d1.DefaultTeamGuestRole)
assert.NoError(t, err)
assert.Equal(t, role5.Permissions, []string{"view_team"})
assert.True(t, role5.SchemeManaged)
role6, err := ss.Role().GetByName(context.Background(), d1.DefaultChannelGuestRole)
assert.NoError(t, err)
assert.Equal(t, role6.Permissions, []string{"read_channel", "create_post"})
assert.True(t, role6.SchemeManaged)
// Change the scheme description and update.
d1.Description = model.NewId()
d2, err := ss.Scheme().Save(d1)
assert.NoError(t, err)
assert.Equal(t, d1.Id, d2.Id)
assert.Equal(t, s1.DisplayName, d2.DisplayName)
assert.Equal(t, s1.Name, d2.Name)
assert.Equal(t, d1.Description, d2.Description)
assert.NotZero(t, d2.CreateAt)
assert.NotZero(t, d2.UpdateAt)
assert.Zero(t, d2.DeleteAt)
assert.Equal(t, s1.Scope, d2.Scope)
assert.Equal(t, d1.DefaultTeamAdminRole, d2.DefaultTeamAdminRole)
assert.Equal(t, d1.DefaultTeamUserRole, d2.DefaultTeamUserRole)
assert.Equal(t, d1.DefaultTeamGuestRole, d2.DefaultTeamGuestRole)
assert.Equal(t, d1.DefaultChannelAdminRole, d2.DefaultChannelAdminRole)
assert.Equal(t, d1.DefaultChannelUserRole, d2.DefaultChannelUserRole)
assert.Equal(t, d1.DefaultChannelGuestRole, d2.DefaultChannelGuestRole)
// Try saving one with an invalid ID set.
s3 := &model.Scheme{
Id: model.NewId(),
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
_, err = ss.Scheme().Save(s3)
assert.Error(t, err)
}
func testSchemeStoreGet(t *testing.T, ss store.Store) {
// Save a scheme to test with.
s1 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
d1, err := ss.Scheme().Save(s1)
assert.NoError(t, err)
assert.Len(t, d1.Id, 26)
// Get a valid scheme
d2, err := ss.Scheme().Get(d1.Id)
assert.NoError(t, err)
assert.Equal(t, d1.Id, d2.Id)
assert.Equal(t, s1.DisplayName, d2.DisplayName)
assert.Equal(t, s1.Name, d2.Name)
assert.Equal(t, d1.Description, d2.Description)
assert.NotZero(t, d2.CreateAt)
assert.NotZero(t, d2.UpdateAt)
assert.Zero(t, d2.DeleteAt)
assert.Equal(t, s1.Scope, d2.Scope)
assert.Equal(t, d1.DefaultTeamAdminRole, d2.DefaultTeamAdminRole)
assert.Equal(t, d1.DefaultTeamUserRole, d2.DefaultTeamUserRole)
assert.Equal(t, d1.DefaultTeamGuestRole, d2.DefaultTeamGuestRole)
assert.Equal(t, d1.DefaultChannelAdminRole, d2.DefaultChannelAdminRole)
assert.Equal(t, d1.DefaultChannelUserRole, d2.DefaultChannelUserRole)
assert.Equal(t, d1.DefaultChannelGuestRole, d2.DefaultChannelGuestRole)
// Get an invalid scheme
_, err = ss.Scheme().Get(model.NewId())
assert.Error(t, err)
}
func testSchemeStoreGetByName(t *testing.T, ss store.Store) {
// Save a scheme to test with.
s1 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
d1, err := ss.Scheme().Save(s1)
assert.NoError(t, err)
assert.Len(t, d1.Id, 26)
// Get a valid scheme
d2, err := ss.Scheme().GetByName(d1.Name)
assert.NoError(t, err)
assert.Equal(t, d1.Id, d2.Id)
assert.Equal(t, s1.DisplayName, d2.DisplayName)
assert.Equal(t, s1.Name, d2.Name)
assert.Equal(t, d1.Description, d2.Description)
assert.NotZero(t, d2.CreateAt)
assert.NotZero(t, d2.UpdateAt)
assert.Zero(t, d2.DeleteAt)
assert.Equal(t, s1.Scope, d2.Scope)
assert.Equal(t, d1.DefaultTeamAdminRole, d2.DefaultTeamAdminRole)
assert.Equal(t, d1.DefaultTeamUserRole, d2.DefaultTeamUserRole)
assert.Equal(t, d1.DefaultTeamGuestRole, d2.DefaultTeamGuestRole)
assert.Equal(t, d1.DefaultChannelAdminRole, d2.DefaultChannelAdminRole)
assert.Equal(t, d1.DefaultChannelUserRole, d2.DefaultChannelUserRole)
assert.Equal(t, d1.DefaultChannelGuestRole, d2.DefaultChannelGuestRole)
// Get an invalid scheme
_, err = ss.Scheme().GetByName(model.NewId())
assert.Error(t, err)
}
func testSchemeStoreGetAllPage(t *testing.T, ss store.Store) {
// Save a scheme to test with.
schemes := []*model.Scheme{
{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
},
{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
},
{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
},
{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
},
}
for _, scheme := range schemes {
_, err := ss.Scheme().Save(scheme)
require.NoError(t, err)
}
s1, err := ss.Scheme().GetAllPage("", 0, 2)
assert.NoError(t, err)
assert.Len(t, s1, 2)
s2, err := ss.Scheme().GetAllPage("", 2, 2)
assert.NoError(t, err)
assert.Len(t, s2, 2)
assert.NotEqual(t, s1[0].DisplayName, s2[0].DisplayName)
assert.NotEqual(t, s1[0].DisplayName, s2[1].DisplayName)
assert.NotEqual(t, s1[1].DisplayName, s2[0].DisplayName)
assert.NotEqual(t, s1[1].DisplayName, s2[1].DisplayName)
assert.NotEqual(t, s1[0].Name, s2[0].Name)
assert.NotEqual(t, s1[0].Name, s2[1].Name)
assert.NotEqual(t, s1[1].Name, s2[0].Name)
assert.NotEqual(t, s1[1].Name, s2[1].Name)
s3, err := ss.Scheme().GetAllPage("team", 0, 1000)
assert.NoError(t, err)
assert.NotZero(t, len(s3))
for _, s := range s3 {
assert.Equal(t, "team", s.Scope)
}
s4, err := ss.Scheme().GetAllPage("channel", 0, 1000)
assert.NoError(t, err)
assert.NotZero(t, len(s4))
for _, s := range s4 {
assert.Equal(t, "channel", s.Scope)
}
}
func testSchemeStoreDelete(t *testing.T, ss store.Store) {
// Save a new scheme.
s1 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
// Check all fields saved correctly.
d1, err := ss.Scheme().Save(s1)
assert.NoError(t, err)
assert.Len(t, d1.Id, 26)
assert.Equal(t, s1.DisplayName, d1.DisplayName)
assert.Equal(t, s1.Name, d1.Name)
assert.Equal(t, s1.Description, d1.Description)
assert.NotZero(t, d1.CreateAt)
assert.NotZero(t, d1.UpdateAt)
assert.Zero(t, d1.DeleteAt)
assert.Equal(t, s1.Scope, d1.Scope)
assert.Len(t, d1.DefaultTeamAdminRole, 26)
assert.Len(t, d1.DefaultTeamUserRole, 26)
assert.Len(t, d1.DefaultTeamGuestRole, 26)
assert.Len(t, d1.DefaultChannelAdminRole, 26)
assert.Len(t, d1.DefaultChannelUserRole, 26)
assert.Len(t, d1.DefaultChannelGuestRole, 26)
// Check the default roles were created correctly.
role1, err := ss.Role().GetByName(context.Background(), d1.DefaultTeamAdminRole)
assert.NoError(t, err)
assert.Equal(t, role1.Permissions, []string{"delete_others_posts"})
assert.True(t, role1.SchemeManaged)
role2, err := ss.Role().GetByName(context.Background(), d1.DefaultTeamUserRole)
assert.NoError(t, err)
assert.Equal(t, role2.Permissions, []string{"view_team", "add_user_to_team"})
assert.True(t, role2.SchemeManaged)
role3, err := ss.Role().GetByName(context.Background(), d1.DefaultChannelAdminRole)
assert.NoError(t, err)
assert.Equal(t, role3.Permissions, []string{"manage_public_channel_members", "manage_private_channel_members"})
assert.True(t, role3.SchemeManaged)
role4, err := ss.Role().GetByName(context.Background(), d1.DefaultChannelUserRole)
assert.NoError(t, err)
assert.Equal(t, role4.Permissions, []string{"read_channel", "create_post"})
assert.True(t, role4.SchemeManaged)
role5, err := ss.Role().GetByName(context.Background(), d1.DefaultTeamGuestRole)
assert.NoError(t, err)
assert.Equal(t, role5.Permissions, []string{"view_team"})
assert.True(t, role5.SchemeManaged)
role6, err := ss.Role().GetByName(context.Background(), d1.DefaultChannelGuestRole)
assert.NoError(t, err)
assert.Equal(t, role6.Permissions, []string{"read_channel", "create_post"})
assert.True(t, role6.SchemeManaged)
// Delete the scheme.
d2, err := ss.Scheme().Delete(d1.Id)
require.NoError(t, err)
assert.NotZero(t, d2.DeleteAt)
// Check that the roles are deleted too.
role7, err := ss.Role().GetByName(context.Background(), d1.DefaultTeamAdminRole)
assert.NoError(t, err)
assert.NotZero(t, role7.DeleteAt)
role8, err := ss.Role().GetByName(context.Background(), d1.DefaultTeamUserRole)
assert.NoError(t, err)
assert.NotZero(t, role8.DeleteAt)
role9, err := ss.Role().GetByName(context.Background(), d1.DefaultChannelAdminRole)
assert.NoError(t, err)
assert.NotZero(t, role9.DeleteAt)
role10, err := ss.Role().GetByName(context.Background(), d1.DefaultChannelUserRole)
assert.NoError(t, err)
assert.NotZero(t, role10.DeleteAt)
role11, err := ss.Role().GetByName(context.Background(), d1.DefaultTeamGuestRole)
assert.NoError(t, err)
assert.NotZero(t, role11.DeleteAt)
role12, err := ss.Role().GetByName(context.Background(), d1.DefaultChannelGuestRole)
assert.NoError(t, err)
assert.NotZero(t, role12.DeleteAt)
// Try deleting a scheme that does not exist.
_, err = ss.Scheme().Delete(model.NewId())
assert.Error(t, err)
// Try deleting a team scheme that's in use.
s4 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
d4, err := ss.Scheme().Save(s4)
assert.NoError(t, err)
t4 := &model.Team{
Name: "xx" + model.NewId(),
DisplayName: model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &d4.Id,
}
t4, err = ss.Team().Save(t4)
require.NoError(t, err)
_, err = ss.Scheme().Delete(d4.Id)
assert.NoError(t, err)
t5, err := ss.Team().Get(t4.Id)
require.NoError(t, err)
assert.Equal(t, "", *t5.SchemeId)
// Try deleting a channel scheme that's in use.
s5 := &model.Scheme{
DisplayName: model.NewId(),
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
d5, err := ss.Scheme().Save(s5)
assert.NoError(t, err)
c5 := &model.Channel{
TeamId: model.NewId(),
DisplayName: model.NewId(),
Name: model.NewId(),
Type: model.ChannelTypeOpen,
SchemeId: &d5.Id,
}
c5, nErr := ss.Channel().Save(c5, -1)
assert.NoError(t, nErr)
_, err = ss.Scheme().Delete(d5.Id)
assert.NoError(t, err)
c6, nErr := ss.Channel().Get(c5.Id, true)
assert.NoError(t, nErr)
assert.Equal(t, "", *c6.SchemeId)
}
func testSchemeStorePermanentDeleteAll(t *testing.T, ss store.Store) {
s1 := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeTeam,
}
s2 := &model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: model.SchemeScopeChannel,
}
s1, err := ss.Scheme().Save(s1)
require.NoError(t, err)
s2, err = ss.Scheme().Save(s2)
require.NoError(t, err)
err = ss.Scheme().PermanentDeleteAll()
assert.NoError(t, err)
_, err = ss.Scheme().Get(s1.Id)
assert.Error(t, err)
_, err = ss.Scheme().Get(s2.Id)
assert.Error(t, err)
schemes, err := ss.Scheme().GetAllPage("", 0, 100000)
assert.NoError(t, err)
assert.Empty(t, schemes)
}
func testSchemeStoreCountByScope(t *testing.T, ss store.Store) {
testCounts := func(expectedTeamCount, expectedChannelCount int) {
actualCount, err := ss.Scheme().CountByScope(model.SchemeScopeTeam)
require.NoError(t, err)
require.Equal(t, int64(expectedTeamCount), actualCount)
actualCount, err = ss.Scheme().CountByScope(model.SchemeScopeChannel)
require.NoError(t, err)
require.Equal(t, int64(expectedChannelCount), actualCount)
}
createScheme := func(scope string) {
_, err := ss.Scheme().Save(&model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: scope,
})
require.NoError(t, err)
}
err := ss.Scheme().PermanentDeleteAll()
require.NoError(t, err)
createScheme(model.SchemeScopeChannel)
createScheme(model.SchemeScopeTeam)
testCounts(1, 1)
createScheme(model.SchemeScopeTeam)
testCounts(2, 1)
createScheme(model.SchemeScopeChannel)
testCounts(2, 2)
}
func testCountWithoutPermission(t *testing.T, ss store.Store) {
perm := model.PermissionCreatePost.Id
createScheme := func(scope string) *model.Scheme {
scheme, err := ss.Scheme().Save(&model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Scope: scope,
})
require.NoError(t, err)
return scheme
}
getRoles := func(scheme *model.Scheme) (channelUser, channelGuest *model.Role) {
var err error
channelUser, err = ss.Role().GetByName(context.Background(), scheme.DefaultChannelUserRole)
require.NoError(t, err)
require.NotNil(t, channelUser)
channelGuest, err = ss.Role().GetByName(context.Background(), scheme.DefaultChannelGuestRole)
require.NoError(t, err)
require.NotNil(t, channelGuest)
return
}
teamScheme1 := createScheme(model.SchemeScopeTeam)
defer ss.Scheme().Delete(teamScheme1.Id)
teamScheme2 := createScheme(model.SchemeScopeTeam)
defer ss.Scheme().Delete(teamScheme2.Id)
channelScheme1 := createScheme(model.SchemeScopeChannel)
defer ss.Scheme().Delete(channelScheme1.Id)
channelScheme2 := createScheme(model.SchemeScopeChannel)
defer ss.Scheme().Delete(channelScheme2.Id)
ts1User, ts1Guest := getRoles(teamScheme1)
ts2User, ts2Guest := getRoles(teamScheme2)
cs1User, cs1Guest := getRoles(channelScheme1)
cs2User, cs2Guest := getRoles(channelScheme2)
allRoles := []*model.Role{
ts1User,
ts1Guest,
ts2User,
ts2Guest,
cs1User,
cs1Guest,
cs2User,
cs2Guest,
}
teamUserCount, err := ss.Scheme().CountWithoutPermission(model.SchemeScopeTeam, perm, model.RoleScopeChannel, model.RoleTypeUser)
require.NoError(t, err)
require.Equal(t, int64(0), teamUserCount)
teamGuestCount, err := ss.Scheme().CountWithoutPermission(model.SchemeScopeTeam, perm, model.RoleScopeChannel, model.RoleTypeGuest)
require.NoError(t, err)
require.Equal(t, int64(0), teamGuestCount)
var tests = []struct {
removePermissionFromRole *model.Role
expectTeamSchemeChannelUserCount int
expectTeamSchemeChannelGuestCount int
expectChannelSchemeChannelUserCount int
expectChannelSchemeChannelGuestCount int
}{
{ts1User, 1, 0, 0, 0},
{ts1Guest, 1, 1, 0, 0},
{ts2User, 2, 1, 0, 0},
{ts2Guest, 2, 2, 0, 0},
{cs1User, 2, 2, 1, 0},
{cs1Guest, 2, 2, 1, 1},
{cs2User, 2, 2, 2, 1},
{cs2Guest, 2, 2, 2, 2},
}
removePermission := func(targetRole *model.Role) {
roleMatched := false
for _, role := range allRoles {
if targetRole == role {
roleMatched = true
role.Permissions = []string{}
_, err = ss.Role().Save(role)
require.NoError(t, err)
}
}
require.True(t, roleMatched)
}
for _, test := range tests {
removePermission(test.removePermissionFromRole)
count, err := ss.Scheme().CountWithoutPermission(model.SchemeScopeTeam, perm, model.RoleScopeChannel, model.RoleTypeUser)
require.NoError(t, err)
require.Equal(t, int64(test.expectTeamSchemeChannelUserCount), count)
count, err = ss.Scheme().CountWithoutPermission(model.SchemeScopeTeam, perm, model.RoleScopeChannel, model.RoleTypeGuest)
require.NoError(t, err)
require.Equal(t, int64(test.expectTeamSchemeChannelGuestCount), count)
count, err = ss.Scheme().CountWithoutPermission(model.SchemeScopeChannel, perm, model.RoleScopeChannel, model.RoleTypeUser)
require.NoError(t, err)
require.Equal(t, int64(test.expectChannelSchemeChannelUserCount), count)
count, err = ss.Scheme().CountWithoutPermission(model.SchemeScopeChannel, perm, model.RoleScopeChannel, model.RoleTypeGuest)
require.NoError(t, err)
require.Equal(t, int64(test.expectChannelSchemeChannelGuestCount), count)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const (
TenMinutes = 600000
)
func TestSessionStore(t *testing.T, ss store.Store) {
// Run serially to prevent interfering with other tests
testSessionCleanup(t, ss)
t.Run("Save", func(t *testing.T) { testSessionStoreSave(t, ss) })
t.Run("SessionGet", func(t *testing.T) { testSessionGet(t, ss) })
t.Run("SessionGetWithDeviceId", func(t *testing.T) { testSessionGetWithDeviceId(t, ss) })
t.Run("SessionRemove", func(t *testing.T) { testSessionRemove(t, ss) })
t.Run("SessionRemoveAll", func(t *testing.T) { testSessionRemoveAll(t, ss) })
t.Run("SessionRemoveByUser", func(t *testing.T) { testSessionRemoveByUser(t, ss) })
t.Run("SessionRemoveToken", func(t *testing.T) { testSessionRemoveToken(t, ss) })
t.Run("SessionUpdateDeviceId", func(t *testing.T) { testSessionUpdateDeviceId(t, ss) })
t.Run("SessionUpdateDeviceId2", func(t *testing.T) { testSessionUpdateDeviceId2(t, ss) })
t.Run("UpdateExpiresAt", func(t *testing.T) { testSessionStoreUpdateExpiresAt(t, ss) })
t.Run("UpdateLastActivityAt", func(t *testing.T) { testSessionStoreUpdateLastActivityAt(t, ss) })
t.Run("SessionCount", func(t *testing.T) { testSessionCount(t, ss) })
t.Run("GetSessionsExpired", func(t *testing.T) { testGetSessionsExpired(t, ss) })
t.Run("UpdateExpiredNotify", func(t *testing.T) { testUpdateExpiredNotify(t, ss) })
}
func testSessionStoreSave(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
_, err := ss.Session().Save(s1)
require.NoError(t, err)
}
func testSessionGet(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
s2 := &model.Session{}
s2.UserId = s1.UserId
_, err = ss.Session().Save(s2)
require.NoError(t, err)
s3 := &model.Session{}
s3.UserId = s1.UserId
s3.ExpiresAt = 1
_, err = ss.Session().Save(s3)
require.NoError(t, err)
session, err := ss.Session().Get(context.Background(), s1.Id)
require.NoError(t, err)
require.Equal(t, session.Id, s1.Id, "should match")
session.Props[model.SessionPropOs] = "linux"
session.Props[model.SessionPropBrowser] = "Chrome"
err = ss.Session().UpdateProps(session)
require.NoError(t, err)
session2, err := ss.Session().Get(context.Background(), session.Id)
require.NoError(t, err)
require.Equal(t, session.Props, session2.Props, "should match")
data, err := ss.Session().GetSessions(s1.UserId)
require.NoError(t, err)
require.Len(t, data, 3, "should match len")
}
func testSessionGetWithDeviceId(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1.ExpiresAt = model.GetMillis() + 10000
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
s2 := &model.Session{}
s2.UserId = s1.UserId
s2.DeviceId = model.NewId()
s2.ExpiresAt = model.GetMillis() + 10000
_, err = ss.Session().Save(s2)
require.NoError(t, err)
s3 := &model.Session{}
s3.UserId = s1.UserId
s3.ExpiresAt = 1
s3.DeviceId = model.NewId()
_, err = ss.Session().Save(s3)
require.NoError(t, err)
data, err := ss.Session().GetSessionsWithActiveDeviceIds(s1.UserId)
require.NoError(t, err)
require.Len(t, data, 1, "should match len")
}
func testSessionRemove(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
session, err := ss.Session().Get(context.Background(), s1.Id)
require.NoError(t, err)
require.Equal(t, session.Id, s1.Id, "should match")
removeErr := ss.Session().Remove(s1.Id)
require.NoError(t, removeErr)
_, err = ss.Session().Get(context.Background(), s1.Id)
require.Error(t, err, "should have been removed")
}
func testSessionRemoveAll(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
session, err := ss.Session().Get(context.Background(), s1.Id)
require.NoError(t, err)
require.Equal(t, session.Id, s1.Id, "should match")
removeErr := ss.Session().RemoveAllSessions()
require.NoError(t, removeErr)
_, err = ss.Session().Get(context.Background(), s1.Id)
require.Error(t, err, "should have been removed")
}
func testSessionRemoveByUser(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
session, err := ss.Session().Get(context.Background(), s1.Id)
require.NoError(t, err)
require.Equal(t, session.Id, s1.Id, "should match")
deleteErr := ss.Session().PermanentDeleteSessionsByUser(s1.UserId)
require.NoError(t, deleteErr)
_, err = ss.Session().Get(context.Background(), s1.Id)
require.Error(t, err, "should have been removed")
}
func testSessionRemoveToken(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
session, err := ss.Session().Get(context.Background(), s1.Id)
require.NoError(t, err)
require.Equal(t, session.Id, s1.Id, "should match")
removeErr := ss.Session().Remove(s1.Token)
require.NoError(t, removeErr)
_, err = ss.Session().Get(context.Background(), s1.Id)
require.Error(t, err, "should have been removed")
data, err := ss.Session().GetSessions(s1.UserId)
require.NoError(t, err)
require.Empty(t, data, "should match len")
}
func testSessionUpdateDeviceId(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
_, err = ss.Session().UpdateDeviceId(s1.Id, model.PushNotifyApple+":1234567890", s1.ExpiresAt)
require.NoError(t, err)
s2 := &model.Session{}
s2.UserId = model.NewId()
s2, err = ss.Session().Save(s2)
require.NoError(t, err)
_, err = ss.Session().UpdateDeviceId(s2.Id, model.PushNotifyApple+":1234567890", s1.ExpiresAt)
require.NoError(t, err)
}
func testSessionUpdateDeviceId2(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
_, err = ss.Session().UpdateDeviceId(s1.Id, model.PushNotifyAppleReactNative+":1234567890", s1.ExpiresAt)
require.NoError(t, err)
s2 := &model.Session{}
s2.UserId = model.NewId()
s2, err = ss.Session().Save(s2)
require.NoError(t, err)
_, err = ss.Session().UpdateDeviceId(s2.Id, model.PushNotifyAppleReactNative+":1234567890", s1.ExpiresAt)
require.NoError(t, err)
}
func testSessionStoreUpdateExpiresAt(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
err = ss.Session().UpdateExpiresAt(s1.Id, 1234567890)
require.NoError(t, err)
session, err := ss.Session().Get(context.Background(), s1.Id)
require.NoError(t, err)
require.EqualValues(t, session.ExpiresAt, 1234567890, "ExpiresAt not updated correctly")
}
func testSessionStoreUpdateLastActivityAt(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
err = ss.Session().UpdateLastActivityAt(s1.Id, 1234567890)
require.NoError(t, err)
session, err := ss.Session().Get(context.Background(), s1.Id)
require.NoError(t, err)
require.EqualValues(t, session.LastActivityAt, 1234567890, "LastActivityAt not updated correctly")
}
func testSessionCount(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1.ExpiresAt = model.GetMillis() + 100000
_, err := ss.Session().Save(s1)
require.NoError(t, err)
count, err := ss.Session().AnalyticsSessionCount()
require.NoError(t, err)
require.NotZero(t, count, "should have at least 1 session")
}
func testSessionCleanup(t *testing.T, ss store.Store) {
now := model.GetMillis()
s1 := &model.Session{}
s1.UserId = model.NewId()
s1.ExpiresAt = 0 // never expires
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
s2 := &model.Session{}
s2.UserId = s1.UserId
s2.ExpiresAt = now + 1000000 // expires in the future
s2, err = ss.Session().Save(s2)
require.NoError(t, err)
s3 := &model.Session{}
s3.UserId = model.NewId()
s3.ExpiresAt = 1 // expired
s3, err = ss.Session().Save(s3)
require.NoError(t, err)
s4 := &model.Session{}
s4.UserId = model.NewId()
s4.ExpiresAt = 2 // expired
s4, err = ss.Session().Save(s4)
require.NoError(t, err)
err = ss.Session().Cleanup(now, 1)
require.NoError(t, err)
_, err = ss.Session().Get(context.Background(), s1.Id)
assert.NoError(t, err)
_, err = ss.Session().Get(context.Background(), s2.Id)
assert.NoError(t, err)
_, err = ss.Session().Get(context.Background(), s3.Id)
assert.Error(t, err)
_, err = ss.Session().Get(context.Background(), s4.Id)
assert.Error(t, err)
removeErr := ss.Session().Remove(s1.Id)
require.NoError(t, removeErr)
removeErr = ss.Session().Remove(s2.Id)
require.NoError(t, removeErr)
}
func testGetSessionsExpired(t *testing.T, ss store.Store) {
now := model.GetMillis()
// Clear existing sessions.
err := ss.Session().RemoveAllSessions()
require.NoError(t, err)
s1 := &model.Session{}
s1.UserId = model.NewId()
s1.DeviceId = model.NewId()
s1.ExpiresAt = 0 // never expires
_, err = ss.Session().Save(s1)
require.NoError(t, err)
s2 := &model.Session{}
s2.UserId = model.NewId()
s2.DeviceId = model.NewId()
s2.ExpiresAt = now - TenMinutes // expired within threshold
s2, err = ss.Session().Save(s2)
require.NoError(t, err)
s3 := &model.Session{}
s3.UserId = model.NewId()
s3.DeviceId = model.NewId()
s3.ExpiresAt = now - (TenMinutes * 100) // expired outside threshold
_, err = ss.Session().Save(s3)
require.NoError(t, err)
s4 := &model.Session{}
s4.UserId = model.NewId()
s4.ExpiresAt = now - TenMinutes // expired within threshold, but not mobile
s4, err = ss.Session().Save(s4)
require.NoError(t, err)
s5 := &model.Session{}
s5.UserId = model.NewId()
s5.DeviceId = model.NewId()
s5.ExpiresAt = now + (TenMinutes * 100000) // not expired
_, err = ss.Session().Save(s5)
require.NoError(t, err)
sessions, err := ss.Session().GetSessionsExpired(TenMinutes*2, true, true) // mobile only
require.NoError(t, err)
require.Len(t, sessions, 1)
require.Equal(t, s2.Id, sessions[0].Id)
sessions, err = ss.Session().GetSessionsExpired(TenMinutes*2, false, true) // all client types
require.NoError(t, err)
require.Len(t, sessions, 2)
expected := []string{s2.Id, s4.Id}
for _, sess := range sessions {
require.Contains(t, expected, sess.Id)
}
}
func testUpdateExpiredNotify(t *testing.T, ss store.Store) {
s1 := &model.Session{}
s1.UserId = model.NewId()
s1.DeviceId = model.NewId()
s1.ExpiresAt = model.GetMillis() + TenMinutes
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
session, err := ss.Session().Get(context.Background(), s1.Id)
require.NoError(t, err)
require.False(t, session.ExpiredNotify)
err = ss.Session().UpdateExpiredNotify(session.Id, true)
require.NoError(t, err)
session, err = ss.Session().Get(context.Background(), s1.Id)
require.NoError(t, err)
require.True(t, session.ExpiredNotify)
err = ss.Session().UpdateExpiredNotify(session.Id, false)
require.NoError(t, err)
session, err = ss.Session().Get(context.Background(), s1.Id)
require.NoError(t, err)
require.False(t, session.ExpiredNotify)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"database/sql"
"flag"
"fmt"
"net/url"
"os"
"path"
"strings"
"github.com/go-sql-driver/mysql"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
defaultMysqlDSN = "mmuser:mostest@tcp(localhost:3306)/mattermost_test?charset=utf8mb4,utf8&readTimeout=30s&writeTimeout=30s&multiStatements=true"
defaultPostgresqlDSN = "postgres://mmuser:mostest@localhost:5432/mattermost_test?sslmode=disable&connect_timeout=10"
defaultMysqlRootPWD = "mostest"
defaultMysqlReplicaDSN = "root:mostest@tcp(localhost:3307)/mattermost_test?charset=utf8mb4,utf8\u0026readTimeout=30s"
)
func getEnv(name, defaultValue string) string {
if value := os.Getenv(name); value != "" {
return value
}
return defaultValue
}
func log(message string) {
verbose := false
if verboseFlag := flag.Lookup("test.v"); verboseFlag != nil {
verbose = verboseFlag.Value.String() != ""
}
if verboseFlag := flag.Lookup("v"); verboseFlag != nil {
verbose = verboseFlag.Value.String() != ""
}
if verbose {
fmt.Println(message)
}
}
func getDefaultMysqlDSN() string {
if os.Getenv("IS_CI") == "true" {
return strings.ReplaceAll(defaultMysqlDSN, "localhost", "mysql")
}
return defaultMysqlDSN
}
func getDefaultPostgresqlDSN() string {
if os.Getenv("IS_CI") == "true" {
return strings.ReplaceAll(defaultPostgresqlDSN, "localhost", "postgres")
}
return defaultPostgresqlDSN
}
// MySQLSettings returns the database settings to connect to the MySQL unittesting database.
// The database name is generated randomly and must be created before use.
func MySQLSettings(withReplica bool) *model.SqlSettings {
dsn := os.Getenv("TEST_DATABASE_MYSQL_DSN")
if dsn == "" {
dsn = getDefaultMysqlDSN()
mlog.Info("No TEST_DATABASE_MYSQL_DSN override, using default", mlog.String("default_dsn", dsn))
} else {
mlog.Info("Using TEST_DATABASE_MYSQL_DSN override", mlog.String("dsn", dsn))
}
cfg, err := mysql.ParseDSN(dsn)
if err != nil {
panic("failed to parse dsn " + dsn + ": " + err.Error())
}
cfg.DBName = "db" + model.NewId()
mySQLSettings := databaseSettings("mysql", cfg.FormatDSN())
if withReplica {
mySQLSettings.DataSourceReplicas = []string{getEnv("TEST_DATABASE_MYSQL_REPLICA_DSN", defaultMysqlReplicaDSN)}
}
return mySQLSettings
}
// PostgresSQLSettings returns the database settings to connect to the PostgreSQL unittesting database.
// The database name is generated randomly and must be created before use.
func PostgreSQLSettings() *model.SqlSettings {
dsn := os.Getenv("TEST_DATABASE_POSTGRESQL_DSN")
if dsn == "" {
dsn = getDefaultPostgresqlDSN()
mlog.Info("No TEST_DATABASE_POSTGRESQL_DSN override, using default", mlog.String("default_dsn", dsn))
} else {
mlog.Info("Using TEST_DATABASE_POSTGRESQL_DSN override", mlog.String("dsn", dsn))
}
dsnURL, err := url.Parse(dsn)
if err != nil {
panic("failed to parse dsn " + dsn + ": " + err.Error())
}
// Generate a random database name
dsnURL.Path = "db" + model.NewId()
return databaseSettings("postgres", dsnURL.String())
}
func mySQLRootDSN(dsn string) string {
rootPwd := getEnv("TEST_DATABASE_MYSQL_ROOT_PASSWD", defaultMysqlRootPWD)
cfg, err := mysql.ParseDSN(dsn)
if err != nil {
panic("failed to parse dsn " + dsn + ": " + err.Error())
}
cfg.User = "root"
cfg.Passwd = rootPwd
cfg.DBName = "mysql"
return cfg.FormatDSN()
}
func postgreSQLRootDSN(dsn string) string {
dsnURL, err := url.Parse(dsn)
if err != nil {
panic("failed to parse dsn " + dsn + ": " + err.Error())
}
// // Assume the unittesting database has the same password.
// password := ""
// if dsnUrl.User != nil {
// password, _ = dsnUrl.User.Password()
// }
// dsnUrl.User = url.UserPassword("", password)
dsnURL.Path = "postgres"
return dsnURL.String()
}
func mySQLDSNDatabase(dsn string) string {
cfg, err := mysql.ParseDSN(dsn)
if err != nil {
panic("failed to parse dsn " + dsn + ": " + err.Error())
}
return cfg.DBName
}
func postgreSQLDSNDatabase(dsn string) string {
dsnURL, err := url.Parse(dsn)
if err != nil {
panic("failed to parse dsn " + dsn + ": " + err.Error())
}
return path.Base(dsnURL.Path)
}
func databaseSettings(driver, dataSource string) *model.SqlSettings {
settings := &model.SqlSettings{
DriverName: &driver,
DataSource: &dataSource,
DataSourceReplicas: []string{},
DataSourceSearchReplicas: []string{},
MaxIdleConns: new(int),
ConnMaxLifetimeMilliseconds: new(int),
ConnMaxIdleTimeMilliseconds: new(int),
MaxOpenConns: new(int),
Trace: model.NewBool(false),
AtRestEncryptKey: model.NewString(model.NewRandomString(32)),
QueryTimeout: new(int),
MigrationsStatementTimeoutSeconds: new(int),
}
*settings.MaxIdleConns = 10
*settings.ConnMaxLifetimeMilliseconds = 3600000
*settings.ConnMaxIdleTimeMilliseconds = 300000
*settings.MaxOpenConns = 100
*settings.QueryTimeout = 60
*settings.MigrationsStatementTimeoutSeconds = 10
return settings
}
// execAsRoot executes the given sql as root against the testing database
func execAsRoot(settings *model.SqlSettings, sqlCommand string) error {
var dsn string
var driver = *settings.DriverName
switch driver {
case model.DatabaseDriverMysql:
dsn = mySQLRootDSN(*settings.DataSource)
case model.DatabaseDriverPostgres:
dsn = postgreSQLRootDSN(*settings.DataSource)
default:
return fmt.Errorf("unsupported driver %s", driver)
}
db, err := sql.Open(driver, dsn)
if err != nil {
return errors.Wrapf(err, "failed to connect to %s database as root", driver)
}
defer db.Close()
if _, err = db.Exec(sqlCommand); err != nil {
return errors.Wrapf(err, "failed to execute `%s` against %s database as root", sqlCommand, driver)
}
return nil
}
func replaceMySQLDatabaseName(dsn, newDBName string) string {
cfg, err := mysql.ParseDSN(dsn)
if err != nil {
panic("failed to parse dsn " + dsn + ": " + err.Error())
}
cfg.DBName = newDBName
return cfg.FormatDSN()
}
// MakeSqlSettings creates a randomly named database and returns the corresponding sql settings
func MakeSqlSettings(driver string, withReplica bool) *model.SqlSettings {
var settings *model.SqlSettings
var dbName string
switch driver {
case model.DatabaseDriverMysql:
settings = MySQLSettings(withReplica)
dbName = mySQLDSNDatabase(*settings.DataSource)
newDSRs := []string{}
for _, dataSource := range settings.DataSourceReplicas {
newDSRs = append(newDSRs, replaceMySQLDatabaseName(dataSource, dbName))
}
settings.DataSourceReplicas = newDSRs
case model.DatabaseDriverPostgres:
settings = PostgreSQLSettings()
dbName = postgreSQLDSNDatabase(*settings.DataSource)
default:
panic("unsupported driver " + driver)
}
if err := execAsRoot(settings, "CREATE DATABASE "+dbName); err != nil {
panic("failed to create temporary database " + dbName + ": " + err.Error())
}
switch driver {
case model.DatabaseDriverMysql:
if err := execAsRoot(settings, "GRANT ALL PRIVILEGES ON "+dbName+".* TO 'mmuser'"); err != nil {
panic("failed to grant mmuser permission to " + dbName + ":" + err.Error())
}
case model.DatabaseDriverPostgres:
if err := execAsRoot(settings, "GRANT ALL PRIVILEGES ON DATABASE \""+dbName+"\" TO mmuser"); err != nil {
panic("failed to grant mmuser permission to " + dbName + ":" + err.Error())
}
default:
panic("unsupported driver " + driver)
}
log("Created temporary " + driver + " database " + dbName)
return settings
}
func CleanupSqlSettings(settings *model.SqlSettings) {
var driver = *settings.DriverName
var dbName string
switch driver {
case model.DatabaseDriverMysql:
dbName = mySQLDSNDatabase(*settings.DataSource)
case model.DatabaseDriverPostgres:
dbName = postgreSQLDSNDatabase(*settings.DataSource)
default:
panic("unsupported driver " + driver)
}
if err := execAsRoot(settings, "DROP DATABASE "+dbName); err != nil {
panic("failed to drop temporary database " + dbName + ": " + err.Error())
}
log("Dropped temporary database " + dbName)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestSharedChannelStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("SaveSharedChannel", func(t *testing.T) { testSaveSharedChannel(t, ss) })
t.Run("GetSharedChannel", func(t *testing.T) { testGetSharedChannel(t, ss) })
t.Run("HasSharedChannel", func(t *testing.T) { testHasSharedChannel(t, ss) })
t.Run("GetSharedChannels", func(t *testing.T) { testGetSharedChannels(t, ss) })
t.Run("UpdateSharedChannel", func(t *testing.T) { testUpdateSharedChannel(t, ss) })
t.Run("DeleteSharedChannel", func(t *testing.T) { testDeleteSharedChannel(t, ss) })
t.Run("SaveSharedChannelRemote", func(t *testing.T) { testSaveSharedChannelRemote(t, ss) })
t.Run("UpdateSharedChannelRemote", func(t *testing.T) { testUpdateSharedChannelRemote(t, ss) })
t.Run("GetSharedChannelRemote", func(t *testing.T) { testGetSharedChannelRemote(t, ss) })
t.Run("GetSharedChannelRemoteByIds", func(t *testing.T) { testGetSharedChannelRemoteByIds(t, ss) })
t.Run("GetSharedChannelRemotes", func(t *testing.T) { testGetSharedChannelRemotes(t, ss) })
t.Run("HasRemote", func(t *testing.T) { testHasRemote(t, ss) })
t.Run("GetRemoteForUser", func(t *testing.T) { testGetRemoteForUser(t, ss) })
t.Run("UpdateSharedChannelRemoteNextSyncAt", func(t *testing.T) { testUpdateSharedChannelRemoteCursor(t, ss) })
t.Run("DeleteSharedChannelRemote", func(t *testing.T) { testDeleteSharedChannelRemote(t, ss) })
t.Run("SaveSharedChannelUser", func(t *testing.T) { testSaveSharedChannelUser(t, ss) })
t.Run("GetSharedChannelSingleUser", func(t *testing.T) { testGetSingleSharedChannelUser(t, ss) })
t.Run("GetSharedChannelUser", func(t *testing.T) { testGetSharedChannelUser(t, ss) })
t.Run("GetSharedChannelUsersForSync", func(t *testing.T) { testGetSharedChannelUsersForSync(t, ss) })
t.Run("UpdateSharedChannelUserLastSyncAt", func(t *testing.T) { testUpdateSharedChannelUserLastSyncAt(t, ss) })
t.Run("SaveSharedChannelAttachment", func(t *testing.T) { testSaveSharedChannelAttachment(t, ss) })
t.Run("UpsertSharedChannelAttachment", func(t *testing.T) { testUpsertSharedChannelAttachment(t, ss) })
t.Run("GetSharedChannelAttachment", func(t *testing.T) { testGetSharedChannelAttachment(t, ss) })
t.Run("UpdateSharedChannelAttachmentLastSyncAt", func(t *testing.T) { testUpdateSharedChannelAttachmentLastSyncAt(t, ss) })
}
func testSaveSharedChannel(t *testing.T, ss store.Store) {
t.Run("Save shared channel (home)", func(t *testing.T) {
channel, err := createTestChannel(ss, "test_save")
require.NoError(t, err)
sc := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
CreatorId: model.NewId(),
ShareName: "testshare",
Home: true,
}
scSaved, err := ss.SharedChannel().Save(sc)
require.NoError(t, err, "couldn't save shared channel")
require.Equal(t, sc.ChannelId, scSaved.ChannelId)
require.Equal(t, sc.TeamId, scSaved.TeamId)
require.Equal(t, sc.CreatorId, scSaved.CreatorId)
// ensure channel's Shared flag is set
channelMod, err := ss.Channel().Get(channel.Id, false)
require.NoError(t, err)
require.True(t, channelMod.IsShared())
})
t.Run("Save shared channel (remote)", func(t *testing.T) {
channel, err := createTestChannel(ss, "test_save2")
require.NoError(t, err)
sc := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
CreatorId: model.NewId(),
ShareName: "testshare",
RemoteId: model.NewId(),
}
scSaved, err := ss.SharedChannel().Save(sc)
require.NoError(t, err, "couldn't save shared channel", err)
require.Equal(t, sc.ChannelId, scSaved.ChannelId)
require.Equal(t, sc.TeamId, scSaved.TeamId)
require.Equal(t, sc.CreatorId, scSaved.CreatorId)
// ensure channel's Shared flag is set
channelMod, err := ss.Channel().Get(channel.Id, false)
require.NoError(t, err)
require.True(t, channelMod.IsShared())
})
t.Run("Save invalid shared channel", func(t *testing.T) {
sc := &model.SharedChannel{
ChannelId: "",
TeamId: model.NewId(),
CreatorId: model.NewId(),
ShareName: "testshare",
Home: true,
}
_, err := ss.SharedChannel().Save(sc)
require.Error(t, err, "should error saving invalid shared channel", err)
})
t.Run("Save with invalid channel id", func(t *testing.T) {
sc := &model.SharedChannel{
ChannelId: model.NewId(),
TeamId: model.NewId(),
CreatorId: model.NewId(),
ShareName: "testshare",
RemoteId: model.NewId(),
}
_, err := ss.SharedChannel().Save(sc)
require.Error(t, err, "expected error for invalid channel id")
})
}
func testGetSharedChannel(t *testing.T, ss store.Store) {
channel, err := createTestChannel(ss, "test_get")
require.NoError(t, err)
sc := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
CreatorId: model.NewId(),
ShareName: "testshare",
Home: true,
}
scSaved, err := ss.SharedChannel().Save(sc)
require.NoError(t, err, "couldn't save shared channel", err)
t.Run("Get existing shared channel", func(t *testing.T) {
sc, err := ss.SharedChannel().Get(scSaved.ChannelId)
require.NoError(t, err, "couldn't get shared channel", err)
require.Equal(t, sc.ChannelId, scSaved.ChannelId)
require.Equal(t, sc.TeamId, scSaved.TeamId)
require.Equal(t, sc.CreatorId, scSaved.CreatorId)
})
t.Run("Get non-existent shared channel", func(t *testing.T) {
sc, err := ss.SharedChannel().Get(model.NewId())
require.Error(t, err)
require.Nil(t, sc)
})
}
func testHasSharedChannel(t *testing.T, ss store.Store) {
channel, err := createTestChannel(ss, "test_get")
require.NoError(t, err)
sc := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
CreatorId: model.NewId(),
ShareName: "testshare",
Home: true,
}
scSaved, err := ss.SharedChannel().Save(sc)
require.NoError(t, err, "couldn't save shared channel", err)
t.Run("Get existing shared channel", func(t *testing.T) {
exists, err := ss.SharedChannel().HasChannel(scSaved.ChannelId)
require.NoError(t, err, "couldn't get shared channel", err)
assert.True(t, exists)
})
t.Run("Get non-existent shared channel", func(t *testing.T) {
exists, err := ss.SharedChannel().HasChannel(model.NewId())
require.NoError(t, err)
assert.False(t, exists)
})
}
func testGetSharedChannels(t *testing.T, ss store.Store) {
require.NoError(t, clearSharedChannels(ss))
user, err := createTestUser(ss, "gary.goodspeed")
require.NoError(t, err)
creator := model.NewId()
team1 := model.NewId()
team2 := model.NewId()
rid := model.NewId()
data := []model.SharedChannel{
{CreatorId: creator, TeamId: team1, ShareName: "test1", Home: true},
{CreatorId: creator, TeamId: team1, ShareName: "test2", Home: false, RemoteId: rid},
{CreatorId: creator, TeamId: team1, ShareName: "test3", Home: false, RemoteId: rid},
{CreatorId: creator, TeamId: team1, ShareName: "test4", Home: true},
{CreatorId: creator, TeamId: team2, ShareName: "test5", Home: true},
{CreatorId: creator, TeamId: team2, ShareName: "test6", Home: false, RemoteId: rid},
{CreatorId: creator, TeamId: team2, ShareName: "test7", Home: false, RemoteId: rid},
{CreatorId: creator, TeamId: team2, ShareName: "test8", Home: true},
{CreatorId: creator, TeamId: team2, ShareName: "test9", Home: true},
}
for i, sc := range data {
channel, err := createTestChannelWithUser(ss, "test_get2_"+strconv.Itoa(i), user)
require.NoError(t, err)
sc.ChannelId = channel.Id
_, err = ss.SharedChannel().Save(&sc)
require.NoError(t, err, "error saving shared channel")
}
t.Run("Get shared channels home only", func(t *testing.T) {
opts := model.SharedChannelFilterOpts{
ExcludeRemote: true,
CreatorId: creator,
}
count, err := ss.SharedChannel().GetAllCount(opts)
require.NoError(t, err, "error getting shared channels count")
home, err := ss.SharedChannel().GetAll(0, 100, opts)
require.NoError(t, err, "error getting shared channels")
require.Equal(t, int(count), len(home))
require.Len(t, home, 5, "should be 5 home channels")
for _, sc := range home {
require.True(t, sc.Home, "should be home channel")
}
})
t.Run("Get shared channels remote only", func(t *testing.T) {
opts := model.SharedChannelFilterOpts{
ExcludeHome: true,
}
count, err := ss.SharedChannel().GetAllCount(opts)
require.NoError(t, err, "error getting shared channels count")
remotes, err := ss.SharedChannel().GetAll(0, 100, opts)
require.NoError(t, err, "error getting shared channels")
require.Equal(t, int(count), len(remotes))
require.Len(t, remotes, 4, "should be 4 remote channels")
for _, sc := range remotes {
require.False(t, sc.Home, "should be remote channel")
}
})
t.Run("Get shared channels bad opts", func(t *testing.T) {
opts := model.SharedChannelFilterOpts{
ExcludeHome: true,
ExcludeRemote: true,
}
_, err := ss.SharedChannel().GetAll(0, 100, opts)
require.Error(t, err, "error expected")
})
t.Run("Get shared channels by team", func(t *testing.T) {
opts := model.SharedChannelFilterOpts{
TeamId: team1,
}
count, err := ss.SharedChannel().GetAllCount(opts)
require.NoError(t, err, "error getting shared channels count")
remotes, err := ss.SharedChannel().GetAll(0, 100, opts)
require.NoError(t, err, "error getting shared channels")
require.Equal(t, int(count), len(remotes))
require.Len(t, remotes, 4, "should be 4 matching channels")
for _, sc := range remotes {
require.Equal(t, team1, sc.TeamId)
}
})
t.Run("Get shared channels invalid pagination", func(t *testing.T) {
opts := model.SharedChannelFilterOpts{
TeamId: team1,
}
_, err := ss.SharedChannel().GetAll(-1, 100, opts)
require.Error(t, err)
_, err = ss.SharedChannel().GetAll(0, -100, opts)
require.Error(t, err)
})
t.Run("Get shared channels for member", func(t *testing.T) {
opts := model.SharedChannelFilterOpts{
TeamId: team1,
MemberId: user.Id,
}
count, err := ss.SharedChannel().GetAllCount(opts)
require.NoError(t, err, "error getting shared channels count")
remotes, err := ss.SharedChannel().GetAll(0, 100, opts)
require.NoError(t, err, "error getting shared channels")
require.Equal(t, int(count), len(remotes))
require.Len(t, remotes, 4, "should be 4 matching channels")
for _, sc := range remotes {
require.Equal(t, team1, sc.TeamId)
}
})
t.Run("Get shared channels for non-member", func(t *testing.T) {
opts := model.SharedChannelFilterOpts{
TeamId: team1,
MemberId: model.NewId(),
}
count, err := ss.SharedChannel().GetAllCount(opts)
require.NoError(t, err, "error getting shared channels count")
remotes, err := ss.SharedChannel().GetAll(0, 100, opts)
require.NoError(t, err, "error getting shared channels")
require.Equal(t, int(count), len(remotes))
require.Len(t, remotes, 0, "should be 0 matching channels")
})
}
func testUpdateSharedChannel(t *testing.T, ss store.Store) {
channel, err := createTestChannel(ss, "test_update")
require.NoError(t, err)
sc := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
CreatorId: model.NewId(),
ShareName: "testshare",
Home: true,
}
scSaved, err := ss.SharedChannel().Save(sc)
require.NoError(t, err, "couldn't save shared channel", err)
t.Run("Update existing shared channel", func(t *testing.T) {
id := model.NewId()
scMod := scSaved // copy struct (contains basic types only)
scMod.ShareName = "newname"
scMod.ShareDisplayName = "For testing"
scMod.ShareHeader = "This is a header."
scMod.RemoteId = id
scUpdated, err := ss.SharedChannel().Update(scMod)
require.NoError(t, err, "couldn't update shared channel", err)
require.Equal(t, "newname", scUpdated.ShareName)
require.Equal(t, "For testing", scUpdated.ShareDisplayName)
require.Equal(t, "This is a header.", scUpdated.ShareHeader)
require.Equal(t, id, scUpdated.RemoteId)
})
t.Run("Update non-existent shared channel", func(t *testing.T) {
sc := &model.SharedChannel{
ChannelId: model.NewId(),
TeamId: model.NewId(),
CreatorId: model.NewId(),
ShareName: "missingshare",
}
_, err := ss.SharedChannel().Update(sc)
require.Error(t, err, "should error when updating non-existent shared channel", err)
})
}
func testDeleteSharedChannel(t *testing.T, ss store.Store) {
channel, err := createTestChannel(ss, "test_delete")
require.NoError(t, err)
sc := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
CreatorId: model.NewId(),
ShareName: "testshare",
RemoteId: model.NewId(),
}
_, err = ss.SharedChannel().Save(sc)
require.NoError(t, err, "couldn't save shared channel", err)
// add some remotes
for i := 0; i < 10; i++ {
remote := &model.SharedChannelRemote{
ChannelId: channel.Id,
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
_, err := ss.SharedChannel().SaveRemote(remote)
require.NoError(t, err, "couldn't add remote", err)
}
t.Run("Delete existing shared channel", func(t *testing.T) {
deleted, err := ss.SharedChannel().Delete(channel.Id)
require.NoError(t, err, "delete existing shared channel should not error", err)
require.True(t, deleted, "expected true from delete shared channel")
sc, err := ss.SharedChannel().Get(channel.Id)
require.Error(t, err)
require.Nil(t, sc)
// make sure the remotes were deleted.
remotes, err := ss.SharedChannel().GetRemotes(model.SharedChannelRemoteFilterOpts{ChannelId: channel.Id})
require.NoError(t, err)
require.Len(t, remotes, 0, "expected empty remotes list")
// ensure channel's Shared flag is unset
channelMod, err := ss.Channel().Get(channel.Id, false)
require.NoError(t, err)
require.False(t, channelMod.IsShared())
})
t.Run("Delete non-existent shared channel", func(t *testing.T) {
deleted, err := ss.SharedChannel().Delete(model.NewId())
require.NoError(t, err, "delete non-existent shared channel should not error", err)
require.False(t, deleted, "expected false from delete shared channel")
})
}
func testSaveSharedChannelRemote(t *testing.T, ss store.Store) {
t.Run("Save shared channel remote", func(t *testing.T) {
channel, err := createTestChannel(ss, "test_save_remote")
require.NoError(t, err)
remote := &model.SharedChannelRemote{
ChannelId: channel.Id,
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
remoteSaved, err := ss.SharedChannel().SaveRemote(remote)
require.NoError(t, err, "couldn't save shared channel remote", err)
require.Equal(t, remote.ChannelId, remoteSaved.ChannelId)
require.Equal(t, remote.CreatorId, remoteSaved.CreatorId)
})
t.Run("Save invalid shared channel remote", func(t *testing.T) {
remote := &model.SharedChannelRemote{
ChannelId: "",
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
_, err := ss.SharedChannel().SaveRemote(remote)
require.Error(t, err, "should error saving invalid remote", err)
})
t.Run("Save shared channel remote with invalid channel id", func(t *testing.T) {
remote := &model.SharedChannelRemote{
ChannelId: model.NewId(),
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
_, err := ss.SharedChannel().SaveRemote(remote)
require.Error(t, err, "expected error for invalid channel id")
})
}
func testUpdateSharedChannelRemote(t *testing.T, ss store.Store) {
t.Run("Update shared channel remote", func(t *testing.T) {
channel, err := createTestChannel(ss, "test_update_remote")
require.NoError(t, err)
remote := &model.SharedChannelRemote{
ChannelId: channel.Id,
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
remoteSaved, err := ss.SharedChannel().SaveRemote(remote)
require.NoError(t, err, "couldn't save shared channel remote", err)
remoteSaved.IsInviteAccepted = true
remoteSaved.IsInviteConfirmed = true
remoteUpdated, err := ss.SharedChannel().UpdateRemote(remoteSaved)
require.NoError(t, err, "couldn't update shared channel remote", err)
require.Equal(t, true, remoteUpdated.IsInviteAccepted)
require.Equal(t, true, remoteUpdated.IsInviteConfirmed)
})
t.Run("Update invalid shared channel remote", func(t *testing.T) {
remote := &model.SharedChannelRemote{
ChannelId: "",
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
_, err := ss.SharedChannel().UpdateRemote(remote)
require.Error(t, err, "should error updating invalid remote", err)
})
t.Run("Update shared channel remote with invalid channel id", func(t *testing.T) {
remote := &model.SharedChannelRemote{
ChannelId: model.NewId(),
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
_, err := ss.SharedChannel().UpdateRemote(remote)
require.Error(t, err, "expected error for invalid channel id")
})
}
func testGetSharedChannelRemote(t *testing.T, ss store.Store) {
channel, err := createTestChannel(ss, "test_remote_get")
require.NoError(t, err)
remote := &model.SharedChannelRemote{
ChannelId: channel.Id,
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
remoteSaved, err := ss.SharedChannel().SaveRemote(remote)
require.NoError(t, err, "couldn't save remote", err)
t.Run("Get existing shared channel remote", func(t *testing.T) {
r, err := ss.SharedChannel().GetRemote(remoteSaved.Id)
require.NoError(t, err, "could not get shared channel remote", err)
require.Equal(t, remoteSaved.Id, r.Id)
require.Equal(t, remoteSaved.ChannelId, r.ChannelId)
require.Equal(t, remoteSaved.CreatorId, r.CreatorId)
require.Equal(t, remoteSaved.RemoteId, r.RemoteId)
})
t.Run("Get non-existent shared channel remote", func(t *testing.T) {
r, err := ss.SharedChannel().GetRemote(model.NewId())
require.Error(t, err)
require.Nil(t, r)
})
}
func testGetSharedChannelRemoteByIds(t *testing.T, ss store.Store) {
channel, err := createTestChannel(ss, "test_remote_get_by_ids")
require.NoError(t, err)
remote := &model.SharedChannelRemote{
ChannelId: channel.Id,
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
remoteSaved, err := ss.SharedChannel().SaveRemote(remote)
require.NoError(t, err, "could not save remote", err)
t.Run("Get existing shared channel remote by ids", func(t *testing.T) {
r, err := ss.SharedChannel().GetRemoteByIds(remoteSaved.ChannelId, remoteSaved.RemoteId)
require.NoError(t, err, "couldn't get shared channel remote by ids", err)
require.Equal(t, remoteSaved.Id, r.Id)
require.Equal(t, remoteSaved.ChannelId, r.ChannelId)
require.Equal(t, remoteSaved.CreatorId, r.CreatorId)
require.Equal(t, remoteSaved.RemoteId, r.RemoteId)
})
t.Run("Get non-existent shared channel remote by ids", func(t *testing.T) {
r, err := ss.SharedChannel().GetRemoteByIds(model.NewId(), model.NewId())
require.Error(t, err)
require.Nil(t, r)
})
}
func testGetSharedChannelRemotes(t *testing.T, ss store.Store) {
channel, err := createTestChannel(ss, "test_remotes_get2")
require.NoError(t, err)
creator := model.NewId()
remoteId := model.NewId()
data := []model.SharedChannelRemote{
{ChannelId: channel.Id, CreatorId: creator, RemoteId: model.NewId(), IsInviteConfirmed: true},
{ChannelId: channel.Id, CreatorId: creator, RemoteId: model.NewId(), IsInviteConfirmed: true},
{ChannelId: channel.Id, CreatorId: creator, RemoteId: model.NewId(), IsInviteConfirmed: true},
{CreatorId: creator, RemoteId: remoteId, IsInviteConfirmed: true},
{CreatorId: creator, RemoteId: remoteId, IsInviteConfirmed: true},
{CreatorId: creator, RemoteId: remoteId},
}
for i, r := range data {
if r.ChannelId == "" {
c, err := createTestChannel(ss, "test_remotes_get2_"+strconv.Itoa(i))
require.NoError(t, err)
r.ChannelId = c.Id
}
_, err := ss.SharedChannel().SaveRemote(&r)
require.NoError(t, err, "error saving shared channel remote")
}
t.Run("Get shared channel remotes by channel_id", func(t *testing.T) {
opts := model.SharedChannelRemoteFilterOpts{
ChannelId: channel.Id,
}
remotes, err := ss.SharedChannel().GetRemotes(opts)
require.NoError(t, err, "should not error", err)
require.Len(t, remotes, 3)
for _, r := range remotes {
require.Equal(t, channel.Id, r.ChannelId)
}
})
t.Run("Get shared channel remotes by invalid channel_id", func(t *testing.T) {
opts := model.SharedChannelRemoteFilterOpts{
ChannelId: model.NewId(),
}
remotes, err := ss.SharedChannel().GetRemotes(opts)
require.NoError(t, err, "should not error", err)
require.Len(t, remotes, 0)
})
t.Run("Get shared channel remotes by remote_id", func(t *testing.T) {
opts := model.SharedChannelRemoteFilterOpts{
RemoteId: remoteId,
}
remotes, err := ss.SharedChannel().GetRemotes(opts)
require.NoError(t, err, "should not error", err)
require.Len(t, remotes, 2) // only confirmed invitations
for _, r := range remotes {
require.Equal(t, remoteId, r.RemoteId)
require.True(t, r.IsInviteConfirmed)
}
})
t.Run("Get shared channel remotes by invalid remote_id", func(t *testing.T) {
opts := model.SharedChannelRemoteFilterOpts{
RemoteId: model.NewId(),
}
remotes, err := ss.SharedChannel().GetRemotes(opts)
require.NoError(t, err, "should not error", err)
require.Len(t, remotes, 0)
})
t.Run("Get shared channel remotes by remote_id including unconfirmed", func(t *testing.T) {
opts := model.SharedChannelRemoteFilterOpts{
RemoteId: remoteId,
InclUnconfirmed: true,
}
remotes, err := ss.SharedChannel().GetRemotes(opts)
require.NoError(t, err, "should not error", err)
require.Len(t, remotes, 3)
for _, r := range remotes {
require.Equal(t, remoteId, r.RemoteId)
}
})
}
func testHasRemote(t *testing.T, ss store.Store) {
channel, err := createTestChannel(ss, "test_remotes_get2")
require.NoError(t, err)
remote1 := model.NewId()
remote2 := model.NewId()
creator := model.NewId()
data := []model.SharedChannelRemote{
{ChannelId: channel.Id, CreatorId: creator, RemoteId: remote1},
{ChannelId: channel.Id, CreatorId: creator, RemoteId: remote2},
}
for _, r := range data {
_, err := ss.SharedChannel().SaveRemote(&r)
require.NoError(t, err, "error saving shared channel remote")
}
t.Run("has remote", func(t *testing.T) {
has, err := ss.SharedChannel().HasRemote(channel.Id, remote1)
require.NoError(t, err)
assert.True(t, has)
has, err = ss.SharedChannel().HasRemote(channel.Id, remote2)
require.NoError(t, err)
assert.True(t, has)
})
t.Run("wrong channel id ", func(t *testing.T) {
has, err := ss.SharedChannel().HasRemote(model.NewId(), remote1)
require.NoError(t, err)
assert.False(t, has)
})
t.Run("wrong remote id", func(t *testing.T) {
has, err := ss.SharedChannel().HasRemote(channel.Id, model.NewId())
require.NoError(t, err)
assert.False(t, has)
})
}
func testGetRemoteForUser(t *testing.T, ss store.Store) {
// add remotes, and users to simulated shared channels.
teamId := model.NewId()
channel, err := createSharedTestChannel(ss, "share_test_channel", true, nil)
require.NoError(t, err)
remotes := []*model.RemoteCluster{
{RemoteId: model.NewId(), SiteURL: model.NewId(), CreatorId: model.NewId(), RemoteTeamId: teamId, Name: "Test_Remote_1"},
{RemoteId: model.NewId(), SiteURL: model.NewId(), CreatorId: model.NewId(), RemoteTeamId: teamId, Name: "Test_Remote_2"},
{RemoteId: model.NewId(), SiteURL: model.NewId(), CreatorId: model.NewId(), RemoteTeamId: teamId, Name: "Test_Remote_3"},
}
for _, rc := range remotes {
_, err := ss.RemoteCluster().Save(rc)
require.NoError(t, err)
scr := &model.SharedChannelRemote{Id: model.NewId(), CreatorId: rc.CreatorId, ChannelId: channel.Id, RemoteId: rc.RemoteId}
_, err = ss.SharedChannel().SaveRemote(scr)
require.NoError(t, err)
}
users := []string{model.NewId(), model.NewId(), model.NewId()}
for _, id := range users {
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: false,
SchemeUser: true,
}
_, err := ss.Channel().SaveMember(member)
require.NoError(t, err)
}
t.Run("user is member", func(t *testing.T) {
for _, rc := range remotes {
for _, userId := range users {
rcFound, err := ss.SharedChannel().GetRemoteForUser(rc.RemoteId, userId)
assert.NoError(t, err, "remote should be found for user")
assert.Equal(t, rc.RemoteId, rcFound.RemoteId, "remoteIds should match")
}
}
})
t.Run("user is not a member", func(t *testing.T) {
for _, rc := range remotes {
rcFound, err := ss.SharedChannel().GetRemoteForUser(rc.RemoteId, model.NewId())
assert.Error(t, err, "remote should not be found for user")
assert.Nil(t, rcFound)
}
})
t.Run("unknown remote id", func(t *testing.T) {
rcFound, err := ss.SharedChannel().GetRemoteForUser(model.NewId(), users[0])
assert.Error(t, err, "remote should not be found for unknown remote id")
assert.Nil(t, rcFound)
})
}
func testUpdateSharedChannelRemoteCursor(t *testing.T, ss store.Store) {
channel, err := createTestChannel(ss, "test_remote_update_next_sync_at")
require.NoError(t, err)
remote := &model.SharedChannelRemote{
ChannelId: channel.Id,
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
remoteSaved, err := ss.SharedChannel().SaveRemote(remote)
require.NoError(t, err, "couldn't save remote", err)
future := model.GetMillis() + 3600000 // 1 hour in the future
postID := model.NewId()
cursor := model.GetPostsSinceForSyncCursor{
LastPostUpdateAt: future,
LastPostId: postID,
}
t.Run("Update NextSyncAt for remote", func(t *testing.T) {
err := ss.SharedChannel().UpdateRemoteCursor(remoteSaved.Id, cursor)
require.NoError(t, err, "update NextSyncAt should not error", err)
r, err := ss.SharedChannel().GetRemote(remoteSaved.Id)
require.NoError(t, err)
require.Equal(t, future, r.LastPostUpdateAt)
require.Equal(t, postID, r.LastPostId)
})
t.Run("Update NextSyncAt for non-existent shared channel remote", func(t *testing.T) {
err := ss.SharedChannel().UpdateRemoteCursor(model.NewId(), cursor)
require.Error(t, err, "update non-existent remote should error", err)
})
}
func testDeleteSharedChannelRemote(t *testing.T, ss store.Store) {
channel, err := createTestChannel(ss, "test_remote_delete")
require.NoError(t, err)
remote := &model.SharedChannelRemote{
ChannelId: channel.Id,
CreatorId: model.NewId(),
RemoteId: model.NewId(),
}
remoteSaved, err := ss.SharedChannel().SaveRemote(remote)
require.NoError(t, err, "couldn't save remote", err)
t.Run("Delete existing shared channel remote", func(t *testing.T) {
deleted, err := ss.SharedChannel().DeleteRemote(remoteSaved.Id)
require.NoError(t, err, "delete existing remote should not error", err)
require.True(t, deleted, "expected true from delete remote")
r, err := ss.SharedChannel().GetRemote(remoteSaved.Id)
require.Error(t, err)
require.Nil(t, r)
})
t.Run("Delete non-existent shared channel remote", func(t *testing.T) {
deleted, err := ss.SharedChannel().DeleteRemote(model.NewId())
require.NoError(t, err, "delete non-existent remote should not error", err)
require.False(t, deleted, "expected false from delete remote")
})
}
func createTestUser(ss store.Store, username string) (*model.User, error) {
user := &model.User{
Username: username,
Email: "gary@example.com",
}
return ss.User().Save(user)
}
func createTestChannel(ss store.Store, name string) (*model.Channel, error) {
channel, err := createSharedTestChannel(ss, name, false, nil)
return channel, err
}
func createTestChannelWithUser(ss store.Store, name string, member *model.User) (*model.Channel, error) {
channel, err := createSharedTestChannel(ss, name, false, member)
return channel, err
}
func createSharedTestChannel(ss store.Store, name string, shared bool, member *model.User) (*model.Channel, error) {
channel := &model.Channel{
TeamId: model.NewId(),
Type: model.ChannelTypeOpen,
Name: name,
DisplayName: name + " display name",
Header: name + " header",
Purpose: name + "purpose",
CreatorId: model.NewId(),
Shared: model.NewBool(shared),
}
channel, err := ss.Channel().Save(channel, 10000)
if err != nil {
return nil, err
}
if member != nil {
newMember := &model.ChannelMember{
ChannelId: channel.Id,
UserId: member.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: member.IsGuest(),
SchemeUser: !member.IsGuest(),
}
_, err = ss.Channel().SaveMember(newMember)
if err != nil {
return nil, err
}
}
if shared {
sc := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
CreatorId: channel.CreatorId,
ShareName: channel.Name,
Home: true,
}
_, err = ss.SharedChannel().Save(sc)
if err != nil {
return nil, err
}
}
return channel, nil
}
func clearSharedChannels(ss store.Store) error {
opts := model.SharedChannelFilterOpts{}
all, err := ss.SharedChannel().GetAll(0, 1000, opts)
if err != nil {
return err
}
for _, sc := range all {
if _, err := ss.SharedChannel().Delete(sc.ChannelId); err != nil {
return err
}
}
return nil
}
func testSaveSharedChannelUser(t *testing.T, ss store.Store) {
t.Run("Save shared channel user", func(t *testing.T) {
scUser := &model.SharedChannelUser{
UserId: model.NewId(),
RemoteId: model.NewId(),
ChannelId: model.NewId(),
}
userSaved, err := ss.SharedChannel().SaveUser(scUser)
require.NoError(t, err, "couldn't save shared channel user", err)
require.Equal(t, scUser.UserId, userSaved.UserId)
require.Equal(t, scUser.RemoteId, userSaved.RemoteId)
})
t.Run("Save invalid shared channel user", func(t *testing.T) {
scUser := &model.SharedChannelUser{
UserId: "",
RemoteId: model.NewId(),
}
_, err := ss.SharedChannel().SaveUser(scUser)
require.Error(t, err, "should error saving invalid user", err)
})
t.Run("Save shared channel user with invalid remote id", func(t *testing.T) {
scUser := &model.SharedChannelUser{
UserId: model.NewId(),
RemoteId: "bogus",
}
_, err := ss.SharedChannel().SaveUser(scUser)
require.Error(t, err, "expected error for invalid remote id")
})
}
func testGetSingleSharedChannelUser(t *testing.T, ss store.Store) {
scUser := &model.SharedChannelUser{
UserId: model.NewId(),
RemoteId: model.NewId(),
ChannelId: model.NewId(),
}
userSaved, err := ss.SharedChannel().SaveUser(scUser)
require.NoError(t, err, "could not save user", err)
t.Run("Get existing shared channel user", func(t *testing.T) {
r, err := ss.SharedChannel().GetSingleUser(userSaved.UserId, userSaved.ChannelId, userSaved.RemoteId)
require.NoError(t, err, "couldn't get shared channel user", err)
require.Equal(t, userSaved.Id, r.Id)
require.Equal(t, userSaved.UserId, r.UserId)
require.Equal(t, userSaved.RemoteId, r.RemoteId)
require.Equal(t, userSaved.CreateAt, r.CreateAt)
})
t.Run("Get non-existent shared channel user", func(t *testing.T) {
u, err := ss.SharedChannel().GetSingleUser(model.NewId(), model.NewId(), model.NewId())
require.Error(t, err)
require.Nil(t, u)
})
}
func testGetSharedChannelUser(t *testing.T, ss store.Store) {
userId := model.NewId()
for i := 0; i < 10; i++ {
scUser := &model.SharedChannelUser{
UserId: userId,
RemoteId: model.NewId(),
ChannelId: model.NewId(),
}
_, err := ss.SharedChannel().SaveUser(scUser)
require.NoError(t, err, "could not save user", err)
}
t.Run("Get existing shared channel user", func(t *testing.T) {
scus, err := ss.SharedChannel().GetUsersForUser(userId)
require.NoError(t, err, "couldn't get shared channel user", err)
require.Len(t, scus, 10, "should be 10 shared channel user records")
require.Equal(t, userId, scus[0].UserId)
})
t.Run("Get non-existent shared channel user", func(t *testing.T) {
scus, err := ss.SharedChannel().GetUsersForUser(model.NewId())
require.NoError(t, err, "should not error when not found")
require.Empty(t, scus, "should be empty")
})
}
func testGetSharedChannelUsersForSync(t *testing.T, ss store.Store) {
channelID := model.NewId()
remoteID := model.NewId()
earlier := model.GetMillis() - 300000
later := model.GetMillis() + 300000
var users []*model.User
for i := 0; i < 10; i++ { // need real users
u := &model.User{
Username: model.NewId(),
Email: model.NewId() + "@example.com",
LastPictureUpdate: model.GetMillis(),
}
u, err := ss.User().Save(u)
require.NoError(t, err)
users = append(users, u)
}
data := []model.SharedChannelUser{
{UserId: users[0].Id, ChannelId: model.NewId(), RemoteId: model.NewId(), LastSyncAt: later},
{UserId: users[1].Id, ChannelId: model.NewId(), RemoteId: model.NewId(), LastSyncAt: earlier},
{UserId: users[1].Id, ChannelId: model.NewId(), RemoteId: model.NewId(), LastSyncAt: earlier},
{UserId: users[1].Id, ChannelId: channelID, RemoteId: remoteID, LastSyncAt: later},
{UserId: users[2].Id, ChannelId: channelID, RemoteId: model.NewId(), LastSyncAt: later},
{UserId: users[3].Id, ChannelId: channelID, RemoteId: model.NewId(), LastSyncAt: earlier},
{UserId: users[4].Id, ChannelId: channelID, RemoteId: model.NewId(), LastSyncAt: later},
{UserId: users[5].Id, ChannelId: channelID, RemoteId: remoteID, LastSyncAt: earlier},
{UserId: users[6].Id, ChannelId: channelID, RemoteId: remoteID, LastSyncAt: later},
}
for i, u := range data {
scu := &model.SharedChannelUser{
UserId: u.UserId,
ChannelId: u.ChannelId,
RemoteId: u.RemoteId,
LastSyncAt: u.LastSyncAt,
}
_, err := ss.SharedChannel().SaveUser(scu)
require.NoError(t, err, "could not save user #", i, err)
}
t.Run("Filter by channelId", func(t *testing.T) {
filter := model.GetUsersForSyncFilter{
CheckProfileImage: false,
ChannelID: channelID,
}
usersFound, err := ss.SharedChannel().GetUsersForSync(filter)
require.NoError(t, err, "shouldn't error getting users", err)
require.Len(t, usersFound, 2)
for _, user := range usersFound {
require.Contains(t, []string{users[3].Id, users[5].Id}, user.Id)
}
})
t.Run("Filter by channelId for profile image", func(t *testing.T) {
filter := model.GetUsersForSyncFilter{
CheckProfileImage: true,
ChannelID: channelID,
}
usersFound, err := ss.SharedChannel().GetUsersForSync(filter)
require.NoError(t, err, "shouldn't error getting users", err)
require.Len(t, usersFound, 2)
for _, user := range usersFound {
require.Contains(t, []string{users[3].Id, users[5].Id}, user.Id)
}
})
t.Run("Filter by channelId with Limit", func(t *testing.T) {
filter := model.GetUsersForSyncFilter{
CheckProfileImage: true,
ChannelID: channelID,
Limit: 1,
}
usersFound, err := ss.SharedChannel().GetUsersForSync(filter)
require.NoError(t, err, "shouldn't error getting users", err)
require.Len(t, usersFound, 1)
})
}
func testUpdateSharedChannelUserLastSyncAt(t *testing.T, ss store.Store) {
u1 := &model.User{
Username: model.NewId(),
Email: model.NewId() + "@example.com",
LastPictureUpdate: model.GetMillis() - 300000, // 5 mins
}
u1, err := ss.User().Save(u1)
require.NoError(t, err)
u2 := &model.User{
Username: model.NewId(),
Email: model.NewId() + "@example.com",
LastPictureUpdate: model.GetMillis() + 300000,
}
u2, err = ss.User().Save(u2)
require.NoError(t, err)
channelID := model.NewId()
remoteID := model.NewId()
scUser1 := &model.SharedChannelUser{
UserId: u1.Id,
RemoteId: remoteID,
ChannelId: channelID,
}
_, err = ss.SharedChannel().SaveUser(scUser1)
require.NoError(t, err, "couldn't save user", err)
scUser2 := &model.SharedChannelUser{
UserId: u2.Id,
RemoteId: remoteID,
ChannelId: channelID,
}
_, err = ss.SharedChannel().SaveUser(scUser2)
require.NoError(t, err, "couldn't save user", err)
t.Run("Update LastSyncAt for user via UpdateAt", func(t *testing.T) {
err := ss.SharedChannel().UpdateUserLastSyncAt(u1.Id, channelID, remoteID)
require.NoError(t, err, "updateLastSyncAt should not error", err)
scu, err := ss.SharedChannel().GetSingleUser(u1.Id, channelID, remoteID)
require.NoError(t, err)
require.Equal(t, u1.UpdateAt, scu.LastSyncAt)
})
t.Run("Update LastSyncAt for user via LastPictureUpdate", func(t *testing.T) {
err := ss.SharedChannel().UpdateUserLastSyncAt(u2.Id, channelID, remoteID)
require.NoError(t, err, "updateLastSyncAt should not error", err)
scu, err := ss.SharedChannel().GetSingleUser(u2.Id, channelID, remoteID)
require.NoError(t, err)
require.Equal(t, u2.LastPictureUpdate, scu.LastSyncAt)
})
t.Run("Update LastSyncAt for non-existent shared channel user", func(t *testing.T) {
err := ss.SharedChannel().UpdateUserLastSyncAt(model.NewId(), channelID, remoteID)
require.Error(t, err, "update non-existent user should error", err)
})
}
func testSaveSharedChannelAttachment(t *testing.T, ss store.Store) {
t.Run("Save shared channel attachment", func(t *testing.T) {
attachment := &model.SharedChannelAttachment{
FileId: model.NewId(),
RemoteId: model.NewId(),
}
saved, err := ss.SharedChannel().SaveAttachment(attachment)
require.NoError(t, err, "couldn't save shared channel attachment", err)
require.Equal(t, attachment.FileId, saved.FileId)
require.Equal(t, attachment.RemoteId, saved.RemoteId)
})
t.Run("Save invalid shared channel attachment", func(t *testing.T) {
attachment := &model.SharedChannelAttachment{
FileId: "",
RemoteId: model.NewId(),
}
_, err := ss.SharedChannel().SaveAttachment(attachment)
require.Error(t, err, "should error saving invalid attachment", err)
})
t.Run("Save shared channel attachment with invalid remote id", func(t *testing.T) {
attachment := &model.SharedChannelAttachment{
FileId: model.NewId(),
RemoteId: "bogus",
}
_, err := ss.SharedChannel().SaveAttachment(attachment)
require.Error(t, err, "expected error for invalid remote id")
})
}
func testUpsertSharedChannelAttachment(t *testing.T, ss store.Store) {
t.Run("Upsert new shared channel attachment", func(t *testing.T) {
attachment := &model.SharedChannelAttachment{
FileId: model.NewId(),
RemoteId: model.NewId(),
}
_, err := ss.SharedChannel().UpsertAttachment(attachment)
require.NoError(t, err, "couldn't upsert shared channel attachment", err)
saved, err := ss.SharedChannel().GetAttachment(attachment.FileId, attachment.RemoteId)
require.NoError(t, err, "couldn't get shared channel attachment", err)
require.NotZero(t, saved.CreateAt)
require.Equal(t, saved.CreateAt, saved.LastSyncAt)
})
t.Run("Upsert existing shared channel attachment", func(t *testing.T) {
attachment := &model.SharedChannelAttachment{
FileId: model.NewId(),
RemoteId: model.NewId(),
}
saved, err := ss.SharedChannel().SaveAttachment(attachment)
require.NoError(t, err, "couldn't save shared channel attachment", err)
// make sure enough time passed that GetMillis returns a different value
time.Sleep(2 * time.Millisecond)
_, err = ss.SharedChannel().UpsertAttachment(saved)
require.NoError(t, err, "couldn't upsert shared channel attachment", err)
updated, err := ss.SharedChannel().GetAttachment(attachment.FileId, attachment.RemoteId)
require.NoError(t, err, "couldn't get shared channel attachment", err)
require.NotZero(t, updated.CreateAt)
require.Greater(t, updated.LastSyncAt, updated.CreateAt)
})
t.Run("Upsert invalid shared channel attachment", func(t *testing.T) {
attachment := &model.SharedChannelAttachment{
FileId: "",
RemoteId: model.NewId(),
}
id, err := ss.SharedChannel().UpsertAttachment(attachment)
require.Error(t, err, "should error upserting invalid attachment", err)
require.Empty(t, id)
})
t.Run("Upsert shared channel attachment with invalid remote id", func(t *testing.T) {
attachment := &model.SharedChannelAttachment{
FileId: model.NewId(),
RemoteId: "bogus",
}
id, err := ss.SharedChannel().UpsertAttachment(attachment)
require.Error(t, err, "expected error for invalid remote id")
require.Empty(t, id)
})
}
func testGetSharedChannelAttachment(t *testing.T, ss store.Store) {
attachment := &model.SharedChannelAttachment{
FileId: model.NewId(),
RemoteId: model.NewId(),
}
saved, err := ss.SharedChannel().SaveAttachment(attachment)
require.NoError(t, err, "could not save attachment", err)
t.Run("Get existing shared channel attachment", func(t *testing.T) {
r, err := ss.SharedChannel().GetAttachment(saved.FileId, saved.RemoteId)
require.NoError(t, err, "couldn't get shared channel attachment", err)
require.Equal(t, saved.Id, r.Id)
require.Equal(t, saved.FileId, r.FileId)
require.Equal(t, saved.RemoteId, r.RemoteId)
require.Equal(t, saved.CreateAt, r.CreateAt)
})
t.Run("Get non-existent shared channel attachment", func(t *testing.T) {
u, err := ss.SharedChannel().GetAttachment(model.NewId(), model.NewId())
require.Error(t, err)
require.Nil(t, u)
})
}
func testUpdateSharedChannelAttachmentLastSyncAt(t *testing.T, ss store.Store) {
attachment := &model.SharedChannelAttachment{
FileId: model.NewId(),
RemoteId: model.NewId(),
}
saved, err := ss.SharedChannel().SaveAttachment(attachment)
require.NoError(t, err, "couldn't save attachment", err)
future := model.GetMillis() + 3600000 // 1 hour in the future
t.Run("Update LastSyncAt for attachment", func(t *testing.T) {
err := ss.SharedChannel().UpdateAttachmentLastSyncAt(saved.Id, future)
require.NoError(t, err, "updateLastSyncAt should not error", err)
f, err := ss.SharedChannel().GetAttachment(saved.FileId, saved.RemoteId)
require.NoError(t, err)
require.Equal(t, future, f.LastSyncAt)
})
t.Run("Update LastSyncAt for non-existent shared channel attachment", func(t *testing.T) {
err := ss.SharedChannel().UpdateAttachmentLastSyncAt(model.NewId(), future)
require.Error(t, err, "update non-existent attachment should error", err)
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestStatusStore(t *testing.T, ss store.Store) {
t.Run("", func(t *testing.T) { testStatusStore(t, ss) })
t.Run("ActiveUserCount", func(t *testing.T) { testActiveUserCount(t, ss) })
t.Run("UpdateExpiredDNDStatuses", func(t *testing.T) { testUpdateExpiredDNDStatuses(t, ss) })
}
func testStatusStore(t *testing.T, ss store.Store) {
status := &model.Status{UserId: model.NewId(), Status: model.StatusOnline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
require.NoError(t, ss.Status().SaveOrUpdate(status))
status.LastActivityAt = 10
_, err := ss.Status().Get(status.UserId)
require.NoError(t, err)
status2 := &model.Status{UserId: model.NewId(), Status: model.StatusAway, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
require.NoError(t, ss.Status().SaveOrUpdate(status2))
status3 := &model.Status{UserId: model.NewId(), Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
require.NoError(t, ss.Status().SaveOrUpdate(status3))
statuses, err := ss.Status().GetByIds([]string{status.UserId, "junk"})
require.NoError(t, err)
require.Len(t, statuses, 1, "should only have 1 status")
err = ss.Status().ResetAll()
require.NoError(t, err)
statusParameter, err := ss.Status().Get(status.UserId)
require.NoError(t, err)
require.Equal(t, statusParameter.Status, model.StatusOffline, "should be offline")
err = ss.Status().UpdateLastActivityAt(status.UserId, 10)
require.NoError(t, err)
}
func testActiveUserCount(t *testing.T, ss store.Store) {
status := &model.Status{UserId: model.NewId(), Status: model.StatusOnline, Manual: false, LastActivityAt: model.GetMillis(), ActiveChannel: ""}
require.NoError(t, ss.Status().SaveOrUpdate(status))
count, err := ss.Status().GetTotalActiveUsersCount()
require.NoError(t, err)
require.True(t, count > 0, "expected count > 0, got %d", count)
}
type ByUserId []*model.Status
func (s ByUserId) Len() int { return len(s) }
func (s ByUserId) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s ByUserId) Less(i, j int) bool { return s[i].UserId < s[j].UserId }
func testUpdateExpiredDNDStatuses(t *testing.T, ss store.Store) {
userID := NewTestId()
status := &model.Status{UserId: userID, Status: model.StatusDnd, Manual: true,
DNDEndTime: time.Now().Add(5 * time.Second).Unix(), PrevStatus: model.StatusOnline}
require.NoError(t, ss.Status().SaveOrUpdate(status))
time.Sleep(2 * time.Second)
// after 2 seconds no statuses should be expired
statuses, err := ss.Status().UpdateExpiredDNDStatuses()
require.NoError(t, err)
require.Len(t, statuses, 0)
time.Sleep(3 * time.Second)
// after 3 more seconds test status should be updated
statuses, err = ss.Status().UpdateExpiredDNDStatuses()
require.NoError(t, err)
require.Len(t, statuses, 1)
updatedStatus := *statuses[0]
require.Equal(t, updatedStatus.UserId, userID)
require.Equal(t, updatedStatus.Status, model.StatusOnline)
require.Equal(t, updatedStatus.DNDEndTime, int64(0))
require.Equal(t, updatedStatus.PrevStatus, model.StatusDnd)
require.Equal(t, updatedStatus.Manual, false)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"database/sql"
"time"
"github.com/stretchr/testify/mock"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
)
// Store can be used to provide mock stores for testing.
type Store struct {
TeamStore mocks.TeamStore
ChannelStore mocks.ChannelStore
PostStore mocks.PostStore
UserStore mocks.UserStore
RetentionPolicyStore mocks.RetentionPolicyStore
BotStore mocks.BotStore
AuditStore mocks.AuditStore
ClusterDiscoveryStore mocks.ClusterDiscoveryStore
RemoteClusterStore mocks.RemoteClusterStore
ComplianceStore mocks.ComplianceStore
SessionStore mocks.SessionStore
OAuthStore mocks.OAuthStore
SystemStore mocks.SystemStore
WebhookStore mocks.WebhookStore
CommandStore mocks.CommandStore
CommandWebhookStore mocks.CommandWebhookStore
PreferenceStore mocks.PreferenceStore
LicenseStore mocks.LicenseStore
TokenStore mocks.TokenStore
EmojiStore mocks.EmojiStore
ThreadStore mocks.ThreadStore
StatusStore mocks.StatusStore
FileInfoStore mocks.FileInfoStore
UploadSessionStore mocks.UploadSessionStore
ReactionStore mocks.ReactionStore
JobStore mocks.JobStore
UserAccessTokenStore mocks.UserAccessTokenStore
PluginStore mocks.PluginStore
ChannelMemberHistoryStore mocks.ChannelMemberHistoryStore
RoleStore mocks.RoleStore
SchemeStore mocks.SchemeStore
TermsOfServiceStore mocks.TermsOfServiceStore
GroupStore mocks.GroupStore
UserTermsOfServiceStore mocks.UserTermsOfServiceStore
LinkMetadataStore mocks.LinkMetadataStore
SharedChannelStore mocks.SharedChannelStore
ProductNoticesStore mocks.ProductNoticesStore
DraftStore mocks.DraftStore
context context.Context
NotifyAdminStore mocks.NotifyAdminStore
PostPriorityStore mocks.PostPriorityStore
PostAcknowledgementStore mocks.PostAcknowledgementStore
TrueUpReviewStore mocks.TrueUpReviewStore
}
func (s *Store) SetContext(context context.Context) { s.context = context }
func (s *Store) Context() context.Context { return s.context }
func (s *Store) Team() store.TeamStore { return &s.TeamStore }
func (s *Store) Channel() store.ChannelStore { return &s.ChannelStore }
func (s *Store) Post() store.PostStore { return &s.PostStore }
func (s *Store) User() store.UserStore { return &s.UserStore }
func (s *Store) RetentionPolicy() store.RetentionPolicyStore { return &s.RetentionPolicyStore }
func (s *Store) Bot() store.BotStore { return &s.BotStore }
func (s *Store) ProductNotices() store.ProductNoticesStore { return &s.ProductNoticesStore }
func (s *Store) Audit() store.AuditStore { return &s.AuditStore }
func (s *Store) ClusterDiscovery() store.ClusterDiscoveryStore { return &s.ClusterDiscoveryStore }
func (s *Store) RemoteCluster() store.RemoteClusterStore { return &s.RemoteClusterStore }
func (s *Store) Compliance() store.ComplianceStore { return &s.ComplianceStore }
func (s *Store) Session() store.SessionStore { return &s.SessionStore }
func (s *Store) OAuth() store.OAuthStore { return &s.OAuthStore }
func (s *Store) System() store.SystemStore { return &s.SystemStore }
func (s *Store) Webhook() store.WebhookStore { return &s.WebhookStore }
func (s *Store) Command() store.CommandStore { return &s.CommandStore }
func (s *Store) CommandWebhook() store.CommandWebhookStore { return &s.CommandWebhookStore }
func (s *Store) Preference() store.PreferenceStore { return &s.PreferenceStore }
func (s *Store) License() store.LicenseStore { return &s.LicenseStore }
func (s *Store) Token() store.TokenStore { return &s.TokenStore }
func (s *Store) Emoji() store.EmojiStore { return &s.EmojiStore }
func (s *Store) Thread() store.ThreadStore { return &s.ThreadStore }
func (s *Store) Status() store.StatusStore { return &s.StatusStore }
func (s *Store) FileInfo() store.FileInfoStore { return &s.FileInfoStore }
func (s *Store) UploadSession() store.UploadSessionStore { return &s.UploadSessionStore }
func (s *Store) Reaction() store.ReactionStore { return &s.ReactionStore }
func (s *Store) Job() store.JobStore { return &s.JobStore }
func (s *Store) UserAccessToken() store.UserAccessTokenStore { return &s.UserAccessTokenStore }
func (s *Store) Plugin() store.PluginStore { return &s.PluginStore }
func (s *Store) Role() store.RoleStore { return &s.RoleStore }
func (s *Store) Scheme() store.SchemeStore { return &s.SchemeStore }
func (s *Store) TermsOfService() store.TermsOfServiceStore { return &s.TermsOfServiceStore }
func (s *Store) UserTermsOfService() store.UserTermsOfServiceStore { return &s.UserTermsOfServiceStore }
func (s *Store) Draft() store.DraftStore { return &s.DraftStore }
func (s *Store) ChannelMemberHistory() store.ChannelMemberHistoryStore {
return &s.ChannelMemberHistoryStore
}
func (s *Store) TrueUpReview() store.TrueUpReviewStore { return &s.TrueUpReviewStore }
func (s *Store) NotifyAdmin() store.NotifyAdminStore { return &s.NotifyAdminStore }
func (s *Store) Group() store.GroupStore { return &s.GroupStore }
func (s *Store) LinkMetadata() store.LinkMetadataStore { return &s.LinkMetadataStore }
func (s *Store) SharedChannel() store.SharedChannelStore { return &s.SharedChannelStore }
func (s *Store) PostPriority() store.PostPriorityStore { return &s.PostPriorityStore }
func (s *Store) PostAcknowledgement() store.PostAcknowledgementStore {
return &s.PostAcknowledgementStore
}
func (s *Store) MarkSystemRanUnitTests() { /* do nothing */ }
func (s *Store) Close() { /* do nothing */ }
func (s *Store) LockToMaster() { /* do nothing */ }
func (s *Store) UnlockFromMaster() { /* do nothing */ }
func (s *Store) DropAllTables() { /* do nothing */ }
func (s *Store) GetDbVersion(bool) (string, error) { return "", nil }
func (s *Store) GetInternalMasterDB() *sql.DB { return nil }
func (s *Store) GetInternalReplicaDB() *sql.DB { return nil }
func (s *Store) GetInternalReplicaDBs() []*sql.DB { return nil }
func (s *Store) RecycleDBConnections(time.Duration) {}
func (s *Store) GetDBSchemaVersion() (int, error) { return 1, nil }
func (s *Store) GetAppliedMigrations() ([]model.AppliedMigration, error) {
return []model.AppliedMigration{}, nil
}
func (s *Store) TotalMasterDbConnections() int { return 1 }
func (s *Store) TotalReadDbConnections() int { return 1 }
func (s *Store) TotalSearchDbConnections() int { return 1 }
func (s *Store) CheckIntegrity() <-chan model.IntegrityCheckResult {
return make(chan model.IntegrityCheckResult)
}
func (s *Store) ReplicaLagAbs() error { return nil }
func (s *Store) ReplicaLagTime() error { return nil }
func (s *Store) AssertExpectations(t mock.TestingT) bool {
return mock.AssertExpectationsForObjects(t,
&s.TeamStore,
&s.ChannelStore,
&s.PostStore,
&s.UserStore,
&s.BotStore,
&s.AuditStore,
&s.ClusterDiscoveryStore,
&s.RemoteClusterStore,
&s.ComplianceStore,
&s.SessionStore,
&s.OAuthStore,
&s.SystemStore,
&s.WebhookStore,
&s.CommandStore,
&s.CommandWebhookStore,
&s.PreferenceStore,
&s.LicenseStore,
&s.TokenStore,
&s.EmojiStore,
&s.StatusStore,
&s.FileInfoStore,
&s.UploadSessionStore,
&s.ReactionStore,
&s.JobStore,
&s.UserAccessTokenStore,
&s.ChannelMemberHistoryStore,
&s.PluginStore,
&s.RoleStore,
&s.SchemeStore,
&s.ThreadStore,
&s.ProductNoticesStore,
&s.SharedChannelStore,
&s.DraftStore,
&s.NotifyAdminStore,
&s.PostPriorityStore,
&s.PostAcknowledgementStore,
)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"github.com/mattermost/mattermost-server/v6/model"
)
func MakeEmail() string {
return "success_" + model.NewId() + "@simulator.amazonses.com"
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestSystemStore(t *testing.T, ss store.Store) {
t.Run("", func(t *testing.T) { testSystemStore(t, ss) })
t.Run("SaveOrUpdate", func(t *testing.T) { testSystemStoreSaveOrUpdate(t, ss) })
t.Run("PermanentDeleteByName", func(t *testing.T) { testSystemStorePermanentDeleteByName(t, ss) })
t.Run("InsertIfExists", func(t *testing.T) {
testInsertIfExists(t, ss)
})
t.Run("SaveOrUpdateWithWarnMetricHandling", func(t *testing.T) { testSystemStoreSaveOrUpdateWithWarnMetricHandling(t, ss) })
t.Run("GetByNameNoEntries", func(t *testing.T) { testSystemStoreGetByNameNoEntries(t, ss) })
}
func testSystemStore(t *testing.T, ss store.Store) {
system := &model.System{Name: model.NewId(), Value: "value"}
err := ss.System().Save(system)
require.NoError(t, err)
system2 := &model.System{Name: model.NewId(), Value: "value2"}
err = ss.System().Save(system2)
require.NoError(t, err)
systems, err := ss.System().Get()
require.NoError(t, err)
require.Equal(t, system.Value, systems[system.Name])
system.Value = "value1"
err = ss.System().Update(system)
require.NoError(t, err)
systems2, err := ss.System().Get()
require.NoError(t, err)
require.Equal(t, system.Value, systems2[system.Name])
require.Equal(t, system2.Value, systems2[system2.Name])
rsystem, err := ss.System().GetByName(system.Name)
require.NoError(t, err)
require.Equal(t, system.Value, rsystem.Value)
}
func testSystemStoreSaveOrUpdate(t *testing.T, ss store.Store) {
system := &model.System{Name: model.NewId(), Value: "value"}
err := ss.System().SaveOrUpdate(system)
require.NoError(t, err)
res, err := ss.System().GetByName(system.Name)
require.NoError(t, err)
assert.Equal(t, system.Value, res.Value)
system.Value = "value2"
err = ss.System().SaveOrUpdate(system)
require.NoError(t, err)
res, err = ss.System().GetByName(system.Name)
require.NoError(t, err)
assert.Equal(t, system.Value, res.Value)
}
func testSystemStoreSaveOrUpdateWithWarnMetricHandling(t *testing.T, ss store.Store) {
system := &model.System{Name: model.NewId(), Value: "value"}
err := ss.System().SaveOrUpdateWithWarnMetricHandling(system)
require.NoError(t, err)
_, err = ss.System().GetByName(model.SystemWarnMetricLastRunTimestampKey)
assert.Error(t, err)
system.Name = "warn_metric_number_of_active_users_100"
system.Value = model.WarnMetricStatusRunonce
err = ss.System().SaveOrUpdateWithWarnMetricHandling(system)
require.NoError(t, err)
val1, nerr := ss.System().GetByName(model.SystemWarnMetricLastRunTimestampKey)
assert.NoError(t, nerr)
system.Name = "warn_metric_number_of_active_users_100"
system.Value = model.WarnMetricStatusAck
err = ss.System().SaveOrUpdateWithWarnMetricHandling(system)
require.NoError(t, err)
val2, nerr := ss.System().GetByName(model.SystemWarnMetricLastRunTimestampKey)
assert.NoError(t, nerr)
assert.Equal(t, val1, val2)
}
func testSystemStoreGetByNameNoEntries(t *testing.T, ss store.Store) {
res, nErr := ss.System().GetByName(model.SystemFirstAdminVisitMarketplace)
_, ok := nErr.(*store.ErrNotFound)
require.Error(t, nErr)
assert.True(t, ok)
assert.Nil(t, res)
}
func testSystemStorePermanentDeleteByName(t *testing.T, ss store.Store) {
s1 := &model.System{Name: model.NewId(), Value: "value"}
s2 := &model.System{Name: model.NewId(), Value: "value"}
err := ss.System().Save(s1)
require.NoError(t, err)
err = ss.System().Save(s2)
require.NoError(t, err)
_, err = ss.System().GetByName(s1.Name)
assert.NoError(t, err)
_, err = ss.System().GetByName(s2.Name)
assert.NoError(t, err)
_, err = ss.System().PermanentDeleteByName(s1.Name)
assert.NoError(t, err)
_, err = ss.System().GetByName(s1.Name)
assert.Error(t, err)
_, err = ss.System().GetByName(s2.Name)
assert.NoError(t, err)
_, err = ss.System().PermanentDeleteByName(s2.Name)
assert.NoError(t, err)
_, err = ss.System().GetByName(s1.Name)
assert.Error(t, err)
_, err = ss.System().GetByName(s2.Name)
assert.Error(t, err)
}
func testInsertIfExists(t *testing.T, ss store.Store) {
t.Run("Serial", func(t *testing.T) {
s1 := &model.System{Name: model.SystemClusterEncryptionKey, Value: "somekey"}
s2, err := ss.System().InsertIfExists(s1)
require.NoError(t, err)
assert.Equal(t, s1.Value, s2.Value)
s1New := &model.System{Name: model.SystemClusterEncryptionKey, Value: "anotherKey"}
s3, err := ss.System().InsertIfExists(s1New)
require.NoError(t, err)
assert.Equal(t, s1.Value, s3.Value)
})
t.Run("Concurrent", func(t *testing.T) {
var s2, s3 *model.System
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
s1 := &model.System{Name: model.SystemClusterEncryptionKey, Value: "firstKey"}
var err error
s2, err = ss.System().InsertIfExists(s1)
require.NoError(t, err)
}()
go func() {
defer wg.Done()
s1 := &model.System{Name: model.SystemClusterEncryptionKey, Value: "secondKey"}
var err error
s3, err = ss.System().InsertIfExists(s1)
require.NoError(t, err)
}()
wg.Wait()
assert.Equal(t, s2.Value, s3.Value)
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"errors"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func cleanupTeamStore(t *testing.T, ss store.Store) {
allTeams, err := ss.Team().GetAll()
for _, team := range allTeams {
ss.Team().PermanentDelete(team.Id)
}
assert.NoError(t, err)
}
func TestTeamStore(t *testing.T, ss store.Store) {
createDefaultRoles(ss)
t.Run("Save", func(t *testing.T) { testTeamStoreSave(t, ss) })
t.Run("Update", func(t *testing.T) { testTeamStoreUpdate(t, ss) })
t.Run("Get", func(t *testing.T) { testTeamStoreGet(t, ss) })
t.Run("GetMany", func(t *testing.T) { testTeamStoreGetMany(t, ss) })
t.Run("GetByName", func(t *testing.T) { testTeamStoreGetByName(t, ss) })
t.Run("GetByNames", func(t *testing.T) { testTeamStoreGetByNames(t, ss) })
t.Run("SearchAll", func(t *testing.T) { testTeamStoreSearchAll(t, ss) })
t.Run("SearchOpen", func(t *testing.T) { testTeamStoreSearchOpen(t, ss) })
t.Run("SearchPrivate", func(t *testing.T) { testTeamStoreSearchPrivate(t, ss) })
t.Run("GetByInviteId", func(t *testing.T) { testTeamStoreGetByInviteId(t, ss) })
t.Run("ByUserId", func(t *testing.T) { testTeamStoreByUserId(t, ss) })
t.Run("GetAllTeamListing", func(t *testing.T) { testGetAllTeamListing(t, ss) })
t.Run("GetAllTeamPage", func(t *testing.T) { testTeamStoreGetAllPage(t, ss) })
t.Run("GetAllTeamPageListing", func(t *testing.T) { testGetAllTeamPageListing(t, ss) })
t.Run("GetAllPrivateTeamListing", func(t *testing.T) { testGetAllPrivateTeamListing(t, ss) })
t.Run("GetAllPrivateTeamPageListing", func(t *testing.T) { testGetAllPrivateTeamPageListing(t, ss) })
t.Run("GetAllPublicTeamPageListing", func(t *testing.T) { testGetAllPublicTeamPageListing(t, ss) })
t.Run("Delete", func(t *testing.T) { testDelete(t, ss) })
t.Run("TeamCount", func(t *testing.T) { testTeamCount(t, ss) })
t.Run("TeamPublicCount", func(t *testing.T) { testPublicTeamCount(t, ss) })
t.Run("TeamPrivateCount", func(t *testing.T) { testPrivateTeamCount(t, ss) })
t.Run("TeamMembers", func(t *testing.T) { testTeamMembers(t, ss) })
t.Run("TestGetMembers", func(t *testing.T) { testGetMembers(t, ss) })
t.Run("SaveMember", func(t *testing.T) { testTeamSaveMember(t, ss) })
t.Run("SaveMultipleMembers", func(t *testing.T) { testTeamSaveMultipleMembers(t, ss) })
t.Run("UpdateMember", func(t *testing.T) { testTeamUpdateMember(t, ss) })
t.Run("UpdateMultipleMembers", func(t *testing.T) { testTeamUpdateMultipleMembers(t, ss) })
t.Run("RemoveMember", func(t *testing.T) { testTeamRemoveMember(t, ss) })
t.Run("RemoveMembers", func(t *testing.T) { testTeamRemoveMembers(t, ss) })
t.Run("SaveTeamMemberMaxMembers", func(t *testing.T) { testSaveTeamMemberMaxMembers(t, ss) })
t.Run("GetTeamMember", func(t *testing.T) { testGetTeamMember(t, ss) })
t.Run("GetTeamMembersByIds", func(t *testing.T) { testGetTeamMembersByIds(t, ss) })
t.Run("MemberCount", func(t *testing.T) { testTeamStoreMemberCount(t, ss) })
t.Run("GetChannelUnreadsForAllTeams", func(t *testing.T) { testGetChannelUnreadsForAllTeams(t, ss) })
t.Run("GetChannelUnreadsForTeam", func(t *testing.T) { testGetChannelUnreadsForTeam(t, ss) })
t.Run("UpdateLastTeamIconUpdate", func(t *testing.T) { testUpdateLastTeamIconUpdate(t, ss) })
t.Run("GetTeamsByScheme", func(t *testing.T) { testGetTeamsByScheme(t, ss) })
t.Run("MigrateTeamMembers", func(t *testing.T) { testTeamStoreMigrateTeamMembers(t, ss) })
t.Run("ResetAllTeamSchemes", func(t *testing.T) { testResetAllTeamSchemes(t, ss) })
t.Run("ClearAllCustomRoleAssignments", func(t *testing.T) { testTeamStoreClearAllCustomRoleAssignments(t, ss) })
t.Run("AnalyticsGetTeamCountForScheme", func(t *testing.T) { testTeamStoreAnalyticsGetTeamCountForScheme(t, ss) })
t.Run("GetAllForExportAfter", func(t *testing.T) { testTeamStoreGetAllForExportAfter(t, ss) })
t.Run("GetTeamMembersForExport", func(t *testing.T) { testTeamStoreGetTeamMembersForExport(t, ss) })
t.Run("GetTeamsForUserWithPagination", func(t *testing.T) { testTeamMembersWithPagination(t, ss) })
t.Run("GroupSyncedTeamCount", func(t *testing.T) { testGroupSyncedTeamCount(t, ss) })
t.Run("GetNewTeamMembersSince", func(t *testing.T) { testGetNewTeamMembersSince(t, ss) })
}
func testTeamStoreSave(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
_, err := ss.Team().Save(&o1)
require.NoError(t, err, "couldn't save item")
_, err = ss.Team().Save(&o1)
require.Error(t, err, "shouldn't be able to update from save")
o1.Id = ""
_, err = ss.Team().Save(&o1)
require.Error(t, err, "should be unique domain")
}
func testTeamStoreUpdate(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)
_, err = ss.Team().Update(&o1)
require.NoError(t, err)
o1.Id = "missing"
_, err = ss.Team().Update(&o1)
require.Error(t, err, "Update should have failed because of missing key")
o1.Id = model.NewId()
_, err = ss.Team().Update(&o1)
require.Error(t, err, "Update should have faile because id change")
}
func testTeamStoreGet(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
r1, err := ss.Team().Get(o1.Id)
require.NoError(t, err)
require.Equal(t, r1, &o1)
_, err = ss.Team().Get("")
require.Error(t, err, "Missing id should have failed")
}
func testTeamStoreGetMany(t *testing.T, ss store.Store) {
o1, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
o2, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName2",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
res, err := ss.Team().GetMany([]string{o1.Id, o2.Id})
require.NoError(t, err)
assert.Len(t, res, 2)
res, err = ss.Team().GetMany([]string{o1.Id, "notexists"})
require.NoError(t, err)
assert.Len(t, res, 1)
_, err = ss.Team().GetMany([]string{"whereisit", "notexists"})
require.Error(t, err)
var nfErr *store.ErrNotFound
assert.True(t, errors.As(err, &nfErr))
}
func testTeamStoreGetByNames(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
o2 := model.Team{}
o2.DisplayName = "DisplayName2"
o2.Name = NewTestId()
o2.Email = MakeEmail()
o2.Type = model.TeamOpen
_, err = ss.Team().Save(&o2)
require.NoError(t, err)
t.Run("Get empty list", func(t *testing.T) {
var teams []*model.Team
teams, err = ss.Team().GetByNames([]string{})
require.NoError(t, err)
require.Empty(t, teams)
})
t.Run("Get existing teams", func(t *testing.T) {
var teams []*model.Team
teams, err = ss.Team().GetByNames([]string{o1.Name, o2.Name})
require.NoError(t, err)
teamsIds := []string{}
for _, team := range teams {
teamsIds = append(teamsIds, team.Id)
}
assert.Contains(t, teamsIds, o1.Id, "invalid returned team")
assert.Contains(t, teamsIds, o2.Id, "invalid returned team")
})
t.Run("Get existing team and one invalid team name", func(t *testing.T) {
_, err = ss.Team().GetByNames([]string{o1.Name, ""})
require.Error(t, err)
})
t.Run("Get existing team and not existing team", func(t *testing.T) {
_, err = ss.Team().GetByNames([]string{o1.Name, "not-existing-team-name"})
require.Error(t, err)
})
t.Run("Get not existing teams", func(t *testing.T) {
_, err = ss.Team().GetByNames([]string{"not-existing-team-name", "not-existing-team-name-2"})
require.Error(t, err)
})
}
func testTeamStoreGetByName(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
t.Run("Get existing team", func(t *testing.T) {
var team *model.Team
team, err = ss.Team().GetByName(o1.Name)
require.NoError(t, err)
require.Equal(t, *team, o1, "invalid returned team")
})
t.Run("Get invalid team name", func(t *testing.T) {
_, err = ss.Team().GetByName("")
require.Error(t, err, "Missing id should have failed")
})
t.Run("Get not existing team", func(t *testing.T) {
_, err = ss.Team().GetByName("not-existing-team-name")
require.Error(t, err, "Missing id should have failed")
})
}
func testTeamStoreSearchAll(t *testing.T, ss store.Store) {
cleanupTeamStore(t, ss)
o := model.Team{}
o.DisplayName = "ADisplayName" + NewTestId()
o.Name = "searchterm-" + NewTestId()
o.Email = MakeEmail()
o.Type = model.TeamOpen
o.AllowOpenInvite = true
_, err := ss.Team().Save(&o)
require.NoError(t, err)
p := model.Team{}
p.DisplayName = "BDisplayName" + NewTestId()
p.Name = "searchterm-" + NewTestId()
p.Email = MakeEmail()
p.Type = model.TeamOpen
p.AllowOpenInvite = false
_, err = ss.Team().Save(&p)
require.NoError(t, err)
g := model.Team{}
g.DisplayName = "CDisplayName" + NewTestId()
g.Name = "searchterm-" + NewTestId()
g.Email = MakeEmail()
g.Type = model.TeamOpen
g.AllowOpenInvite = false
g.GroupConstrained = model.NewBool(true)
_, err = ss.Team().Save(&g)
require.NoError(t, err)
q := &model.Team{}
q.DisplayName = "CHOCOLATE"
q.Name = "ilovecake"
q.Email = MakeEmail()
q.Type = model.TeamOpen
q.AllowOpenInvite = false
q, err = ss.Team().Save(q)
require.NoError(t, err)
_, err = ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "Policy 1",
PostDurationDays: model.NewInt64(20),
},
TeamIDs: []string{q.Id},
})
require.NoError(t, err)
testCases := []struct {
Name string
Opts *model.TeamSearch
ExpectedLenth int
ExpectedTeamIds []string
}{
{
"Search chocolate by display name",
&model.TeamSearch{Term: "ocola"},
1,
[]string{q.Id},
},
{
"Search chocolate by display name",
&model.TeamSearch{Term: "choc"},
1,
[]string{q.Id},
},
{
"Search chocolate by display name",
&model.TeamSearch{Term: "late"},
1,
[]string{q.Id},
},
{
"Search chocolate by name",
&model.TeamSearch{Term: "ilov"},
1,
[]string{q.Id},
},
{
"Search chocolate by name",
&model.TeamSearch{Term: "ecake"},
1,
[]string{q.Id},
},
{
"Search for open team name",
&model.TeamSearch{Term: o.Name},
1,
[]string{o.Id},
},
{
"Search for open team displayName",
&model.TeamSearch{Term: o.DisplayName},
1,
[]string{o.Id},
},
{
"Search for open team without results",
&model.TeamSearch{Term: "nonexistent"},
0,
[]string{},
},
{
"Search for private team",
&model.TeamSearch{Term: p.DisplayName},
1,
[]string{p.Id},
},
{
"Search for all 3 searchterm teams",
&model.TeamSearch{Term: "searchterm"},
3,
[]string{o.Id, p.Id, g.Id},
},
{
"Search for all 3 teams filter by allow open invite",
&model.TeamSearch{Term: "searchterm", AllowOpenInvite: model.NewBool(true)},
1,
[]string{o.Id},
},
{
"Search for all 3 teams filter by allow open invite = false",
&model.TeamSearch{Term: "searchterm", AllowOpenInvite: model.NewBool(false)},
1,
[]string{p.Id},
},
{
"Search for all 3 teams filter by group constrained",
&model.TeamSearch{Term: "searchterm", GroupConstrained: model.NewBool(true)},
1,
[]string{g.Id},
},
{
"Search for all 3 teams filter by group constrained = false",
&model.TeamSearch{Term: "searchterm", GroupConstrained: model.NewBool(false)},
2,
[]string{o.Id, p.Id},
},
{
"Search for all 3 teams filter by allow open invite and include group constrained",
&model.TeamSearch{Term: "searchterm", AllowOpenInvite: model.NewBool(true), GroupConstrained: model.NewBool(true)},
2,
[]string{o.Id, g.Id},
},
{
"Search for all 3 teams filter by group constrained and not open invite",
&model.TeamSearch{Term: "searchterm", GroupConstrained: model.NewBool(true), AllowOpenInvite: model.NewBool(false)},
2,
[]string{g.Id, p.Id},
},
{
"Search for all 3 teams filter by group constrained false and open invite",
&model.TeamSearch{Term: "searchterm", GroupConstrained: model.NewBool(false), AllowOpenInvite: model.NewBool(true)},
2,
[]string{o.Id, p.Id},
},
{
"Search for all 3 teams filter by group constrained false and open invite false",
&model.TeamSearch{Term: "searchterm", GroupConstrained: model.NewBool(false), AllowOpenInvite: model.NewBool(false)},
2,
[]string{p.Id, o.Id},
},
{
"Search for teams which are not part of a data retention policy",
&model.TeamSearch{Term: "", ExcludePolicyConstrained: model.NewBool(true)},
3,
[]string{o.Id, p.Id, g.Id},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
response, err := ss.Team().SearchAll(tc.Opts)
require.NoError(t, err)
require.Equal(t, tc.ExpectedLenth, len(response))
responseTeamIds := []string{}
for _, team := range response {
responseTeamIds = append(responseTeamIds, team.Id)
}
require.ElementsMatch(t, tc.ExpectedTeamIds, responseTeamIds)
})
}
}
func testTeamStoreSearchOpen(t *testing.T, ss store.Store) {
o := model.Team{}
o.DisplayName = "ADisplayName" + NewTestId()
o.Name = NewTestId()
o.Email = MakeEmail()
o.Type = model.TeamOpen
o.AllowOpenInvite = true
_, err := ss.Team().Save(&o)
require.NoError(t, err)
p := model.Team{}
p.DisplayName = "ADisplayName" + NewTestId()
p.Name = NewTestId()
p.Email = MakeEmail()
p.Type = model.TeamOpen
p.AllowOpenInvite = false
_, err = ss.Team().Save(&p)
require.NoError(t, err)
q := model.Team{}
q.DisplayName = "PINEAPPLEPIE"
q.Name = "ihadsomepineapplepiewithstrawberry"
q.Email = MakeEmail()
q.Type = model.TeamOpen
q.AllowOpenInvite = true
_, err = ss.Team().Save(&q)
require.NoError(t, err)
testCases := []struct {
Name string
Term string
ExpectedLength int
ExpectedFirstId string
}{
{
"Search PINEAPPLEPIE by display name",
"neapplep",
1,
q.Id,
},
{
"Search PINEAPPLEPIE by display name",
"pine",
1,
q.Id,
},
{
"Search PINEAPPLEPIE by display name",
"epie",
1,
q.Id,
},
{
"Search PINEAPPLEPIE by name",
"ihadsome",
1,
q.Id,
},
{
"Search PINEAPPLEPIE by name",
"pineapplepiewithstrawberry",
1,
q.Id,
},
{
"Search for open team name",
o.Name,
1,
o.Id,
},
{
"Search for open team displayName",
o.DisplayName,
1,
o.Id,
},
{
"Search for open team without results",
"nonexistent",
0,
"",
},
{
"Search for a private team (expected no results)",
p.DisplayName,
0,
"",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
r1, err := ss.Team().SearchOpen(&model.TeamSearch{Term: tc.Term})
require.NoError(t, err)
results := r1
require.Equal(t, tc.ExpectedLength, len(results))
if tc.ExpectedFirstId != "" {
assert.Equal(t, tc.ExpectedFirstId, results[0].Id)
}
})
}
}
func testTeamStoreSearchPrivate(t *testing.T, ss store.Store) {
o := model.Team{}
o.DisplayName = "ADisplayName" + NewTestId()
o.Name = NewTestId()
o.Email = MakeEmail()
o.Type = model.TeamOpen
o.AllowOpenInvite = true
_, err := ss.Team().Save(&o)
require.NoError(t, err)
p := model.Team{}
p.DisplayName = "ADisplayName" + NewTestId()
p.Name = NewTestId()
p.Email = MakeEmail()
p.Type = model.TeamOpen
p.AllowOpenInvite = false
_, err = ss.Team().Save(&p)
require.NoError(t, err)
q := model.Team{}
q.DisplayName = "FOOBARDISPLAYNAME"
q.Name = "averylongname"
q.Email = MakeEmail()
q.Type = model.TeamOpen
q.AllowOpenInvite = false
_, err = ss.Team().Save(&q)
require.NoError(t, err)
testCases := []struct {
Name string
Term string
ExpectedLength int
ExpectedFirstId string
}{
{
"Search FooBar by display name from text in the middle of display name",
"oobardisplay",
1,
q.Id,
},
{
"Search FooBar by display name from text at the beginning of display name",
"foobar",
1,
q.Id,
},
{
"Search FooBar by display name from text at the end of display name",
"bardisplayname",
1,
q.Id,
},
{
"Search FooBar by name from text at the beginning name",
"averyl",
1,
q.Id,
},
{
"Search FooBar by name from text at the end of name",
"ongname",
1,
q.Id,
},
{
"Search for private team name",
p.Name,
1,
p.Id,
},
{
"Search for private team displayName",
p.DisplayName,
1,
p.Id,
},
{
"Search for private team without results",
"nonexistent",
0,
"",
},
{
"Search for a open team (expected no results)",
o.DisplayName,
0,
"",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
r1, err := ss.Team().SearchPrivate(&model.TeamSearch{Term: tc.Term})
require.NoError(t, err)
results := r1
require.Equal(t, tc.ExpectedLength, len(results))
if tc.ExpectedFirstId != "" {
assert.Equal(t, tc.ExpectedFirstId, results[0].Id)
}
})
}
}
func testTeamStoreGetByInviteId(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.InviteId = model.NewId()
save1, err := ss.Team().Save(&o1)
require.NoError(t, err)
r1, err := ss.Team().GetByInviteId(save1.InviteId)
require.NoError(t, err)
require.Equal(t, *r1, o1, "invalid returned team")
_, err = ss.Team().GetByInviteId("")
require.Error(t, err, "Missing id should have failed")
}
func testTeamStoreByUserId(t *testing.T, ss store.Store) {
o1 := &model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.InviteId = model.NewId()
o1, err := ss.Team().Save(o1)
require.NoError(t, err)
m1 := &model.TeamMember{TeamId: o1.Id, UserId: model.NewId()}
_, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
teams, err := ss.Team().GetTeamsByUserId(m1.UserId)
require.NoError(t, err)
require.Len(t, teams, 1, "Should return a team")
require.Equal(t, teams[0].Id, o1.Id, "should be a member")
}
func testTeamStoreGetAllPage(t *testing.T, ss store.Store) {
o := model.Team{}
o.DisplayName = "ADisplayName" + model.NewId()
o.Name = "zz" + model.NewId() + "a"
o.Email = MakeEmail()
o.Type = model.TeamOpen
o.AllowOpenInvite = true
_, err := ss.Team().Save(&o)
require.NoError(t, err)
policy, err := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "Policy 1",
PostDurationDays: model.NewInt64(30),
},
TeamIDs: []string{o.Id},
})
require.NoError(t, err)
// Without ExcludePolicyConstrained
teams, err := ss.Team().GetAllPage(0, 100, nil)
require.NoError(t, err)
found := false
for _, team := range teams {
if team.Id == o.Id {
found = true
require.Nil(t, team.PolicyID)
break
}
}
require.True(t, found)
// With ExcludePolicyConstrained
teams, err = ss.Team().GetAllPage(0, 100, &model.TeamSearch{ExcludePolicyConstrained: model.NewBool(true)})
require.NoError(t, err)
found = false
for _, team := range teams {
if team.Id == o.Id {
found = true
break
}
}
require.False(t, found)
// With policy ID
teams, err = ss.Team().GetAllPage(0, 100, &model.TeamSearch{IncludePolicyID: model.NewBool(true)})
require.NoError(t, err)
found = false
for _, team := range teams {
if team.Id == o.Id {
found = true
require.Equal(t, *team.PolicyID, policy.ID)
break
}
}
require.True(t, found)
}
func testGetAllTeamListing(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.AllowOpenInvite = true
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
o2 := model.Team{}
o2.DisplayName = "DisplayName"
o2.Name = NewTestId()
o2.Email = MakeEmail()
o2.Type = model.TeamOpen
_, err = ss.Team().Save(&o2)
require.NoError(t, err)
o3 := model.Team{}
o3.DisplayName = "DisplayName"
o3.Name = NewTestId()
o3.Email = MakeEmail()
o3.Type = model.TeamInvite
o3.AllowOpenInvite = true
_, err = ss.Team().Save(&o3)
require.NoError(t, err)
o4 := model.Team{}
o4.DisplayName = "DisplayName"
o4.Name = NewTestId()
o4.Email = MakeEmail()
o4.Type = model.TeamInvite
_, err = ss.Team().Save(&o4)
require.NoError(t, err)
teams, err := ss.Team().GetAllTeamListing()
require.NoError(t, err)
for _, team := range teams {
require.True(t, team.AllowOpenInvite, "should have returned team with AllowOpenInvite as true")
}
require.NotEmpty(t, teams, "failed team listing")
}
func testGetAllTeamPageListing(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.AllowOpenInvite = true
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
o2 := model.Team{}
o2.DisplayName = "DisplayName"
o2.Name = NewTestId()
o2.Email = MakeEmail()
o2.Type = model.TeamOpen
o2.AllowOpenInvite = false
_, err = ss.Team().Save(&o2)
require.NoError(t, err)
o3 := model.Team{}
o3.DisplayName = "DisplayName"
o3.Name = NewTestId()
o3.Email = MakeEmail()
o3.Type = model.TeamInvite
o3.AllowOpenInvite = true
_, err = ss.Team().Save(&o3)
require.NoError(t, err)
o4 := model.Team{}
o4.DisplayName = "DisplayName"
o4.Name = NewTestId()
o4.Email = MakeEmail()
o4.Type = model.TeamInvite
o4.AllowOpenInvite = false
_, err = ss.Team().Save(&o4)
require.NoError(t, err)
opts := &model.TeamSearch{AllowOpenInvite: model.NewBool(true)}
teams, err := ss.Team().GetAllPage(0, 10, opts)
require.NoError(t, err)
for _, team := range teams {
require.True(t, team.AllowOpenInvite, "should have returned team with AllowOpenInvite as true")
}
require.LessOrEqual(t, len(teams), 10, "should have returned max of 10 teams")
o5 := model.Team{}
o5.DisplayName = "DisplayName"
o5.Name = NewTestId()
o5.Email = MakeEmail()
o5.Type = model.TeamOpen
o5.AllowOpenInvite = true
_, err = ss.Team().Save(&o5)
require.NoError(t, err)
teams, err = ss.Team().GetAllPage(0, 4, opts)
require.NoError(t, err)
for _, team := range teams {
require.True(t, team.AllowOpenInvite, "should have returned team with AllowOpenInvite as true")
}
require.LessOrEqual(t, len(teams), 4, "should have returned max of 4 teams")
teams, err = ss.Team().GetAllPage(1, 1, opts)
require.NoError(t, err)
for _, team := range teams {
require.True(t, team.AllowOpenInvite, "should have returned team with AllowOpenInvite as true")
}
require.LessOrEqual(t, len(teams), 1, "should have returned max of 1 team")
}
func testGetAllPrivateTeamListing(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.AllowOpenInvite = true
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
o2 := model.Team{}
o2.DisplayName = "DisplayName"
o2.Name = NewTestId()
o2.Email = MakeEmail()
o2.Type = model.TeamOpen
_, err = ss.Team().Save(&o2)
require.NoError(t, err)
o3 := model.Team{}
o3.DisplayName = "DisplayName"
o3.Name = NewTestId()
o3.Email = MakeEmail()
o3.Type = model.TeamInvite
o3.AllowOpenInvite = true
_, err = ss.Team().Save(&o3)
require.NoError(t, err)
o4 := model.Team{}
o4.DisplayName = "DisplayName"
o4.Name = NewTestId()
o4.Email = MakeEmail()
o4.Type = model.TeamInvite
_, err = ss.Team().Save(&o4)
require.NoError(t, err)
teams, err := ss.Team().GetAllPrivateTeamListing()
require.NoError(t, err)
require.NotEmpty(t, teams, "failed team listing")
for _, team := range teams {
require.False(t, team.AllowOpenInvite, "should have returned team with AllowOpenInvite as false")
}
}
func testGetAllPrivateTeamPageListing(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.AllowOpenInvite = true
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
o2 := model.Team{}
o2.DisplayName = "DisplayName"
o2.Name = NewTestId()
o2.Email = MakeEmail()
o2.Type = model.TeamOpen
o2.AllowOpenInvite = false
_, err = ss.Team().Save(&o2)
require.NoError(t, err)
o3 := model.Team{}
o3.DisplayName = "DisplayName"
o3.Name = NewTestId()
o3.Email = MakeEmail()
o3.Type = model.TeamInvite
o3.AllowOpenInvite = true
_, err = ss.Team().Save(&o3)
require.NoError(t, err)
o4 := model.Team{}
o4.DisplayName = "DisplayName"
o4.Name = NewTestId()
o4.Email = MakeEmail()
o4.Type = model.TeamInvite
o4.AllowOpenInvite = false
_, err = ss.Team().Save(&o4)
require.NoError(t, err)
opts := &model.TeamSearch{AllowOpenInvite: model.NewBool(false)}
teams, listErr := ss.Team().GetAllPage(0, 10, opts)
require.NoError(t, listErr)
for _, team := range teams {
require.False(t, team.AllowOpenInvite, "should have returned team with AllowOpenInvite as false")
}
require.LessOrEqual(t, len(teams), 10, "should have returned max of 10 teams")
o5 := model.Team{}
o5.DisplayName = "DisplayName"
o5.Name = NewTestId()
o5.Email = MakeEmail()
o5.Type = model.TeamOpen
o5.AllowOpenInvite = true
_, err = ss.Team().Save(&o5)
require.NoError(t, err)
teams, listErr = ss.Team().GetAllPage(0, 4, opts)
require.NoError(t, listErr)
for _, team := range teams {
require.False(t, team.AllowOpenInvite, "should have returned team with AllowOpenInvite as false")
}
require.LessOrEqual(t, len(teams), 4, "should have returned max of 4 teams")
teams, listErr = ss.Team().GetAllPage(1, 1, opts)
require.NoError(t, listErr)
for _, team := range teams {
require.False(t, team.AllowOpenInvite, "should have returned team with AllowOpenInvite as false")
}
require.LessOrEqual(t, len(teams), 1, "should have returned max of 1 team")
}
func testGetAllPublicTeamPageListing(t *testing.T, ss store.Store) {
cleanupTeamStore(t, ss)
o1 := model.Team{}
o1.DisplayName = "DisplayName1"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.AllowOpenInvite = true
t1, err := ss.Team().Save(&o1)
require.NoError(t, err)
o2 := model.Team{}
o2.DisplayName = "DisplayName2"
o2.Name = NewTestId()
o2.Email = MakeEmail()
o2.Type = model.TeamOpen
o2.AllowOpenInvite = false
_, err = ss.Team().Save(&o2)
require.NoError(t, err)
o3 := model.Team{}
o3.DisplayName = "DisplayName3"
o3.Name = NewTestId()
o3.Email = MakeEmail()
o3.Type = model.TeamInvite
o3.AllowOpenInvite = true
t3, err := ss.Team().Save(&o3)
require.NoError(t, err)
o4 := model.Team{}
o4.DisplayName = "DisplayName4"
o4.Name = NewTestId()
o4.Email = MakeEmail()
o4.Type = model.TeamInvite
o4.AllowOpenInvite = false
_, err = ss.Team().Save(&o4)
require.NoError(t, err)
opts := &model.TeamSearch{AllowOpenInvite: model.NewBool(true)}
teams, err := ss.Team().GetAllPage(0, 10, opts)
assert.NoError(t, err)
assert.Equal(t, []*model.Team{t1, t3}, teams)
o5 := model.Team{}
o5.DisplayName = "DisplayName5"
o5.Name = NewTestId()
o5.Email = MakeEmail()
o5.Type = model.TeamOpen
o5.AllowOpenInvite = true
t5, err := ss.Team().Save(&o5)
require.NoError(t, err)
teams, err = ss.Team().GetAllPage(0, 4, opts)
assert.NoError(t, err)
assert.Equal(t, []*model.Team{t1, t3, t5}, teams)
_, err = ss.Team().GetAllPage(1, 1, opts)
assert.NoError(t, err)
}
func testDelete(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.AllowOpenInvite = true
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
o2 := model.Team{}
o2.DisplayName = "DisplayName"
o2.Name = NewTestId()
o2.Email = MakeEmail()
o2.Type = model.TeamOpen
_, err = ss.Team().Save(&o2)
require.NoError(t, err)
r1 := ss.Team().PermanentDelete(o1.Id)
require.NoError(t, r1)
}
func testPublicTeamCount(t *testing.T, ss store.Store) {
cleanupTeamStore(t, ss)
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.AllowOpenInvite = true
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
o2 := model.Team{}
o2.DisplayName = "DisplayName"
o2.Name = NewTestId()
o2.Email = MakeEmail()
o2.Type = model.TeamOpen
o2.AllowOpenInvite = false
_, err = ss.Team().Save(&o2)
require.NoError(t, err)
o3 := model.Team{}
o3.DisplayName = "DisplayName"
o3.Name = NewTestId()
o3.Email = MakeEmail()
o3.Type = model.TeamOpen
o3.AllowOpenInvite = true
_, err = ss.Team().Save(&o3)
require.NoError(t, err)
teamCount, err := ss.Team().AnalyticsTeamCount(&model.TeamSearch{AllowOpenInvite: model.NewBool(true)})
require.NoError(t, err)
require.Equal(t, int64(2), teamCount, "should only be 1 team")
}
func testPrivateTeamCount(t *testing.T, ss store.Store) {
cleanupTeamStore(t, ss)
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.AllowOpenInvite = false
_, err := ss.Team().Save(&o1)
require.NoError(t, err)
o2 := model.Team{}
o2.DisplayName = "DisplayName"
o2.Name = NewTestId()
o2.Email = MakeEmail()
o2.Type = model.TeamOpen
o2.AllowOpenInvite = true
_, err = ss.Team().Save(&o2)
require.NoError(t, err)
o3 := model.Team{}
o3.DisplayName = "DisplayName"
o3.Name = NewTestId()
o3.Email = MakeEmail()
o3.Type = model.TeamOpen
o3.AllowOpenInvite = false
_, err = ss.Team().Save(&o3)
require.NoError(t, err)
teamCount, err := ss.Team().AnalyticsTeamCount(&model.TeamSearch{AllowOpenInvite: model.NewBool(false)})
require.NoError(t, err)
require.Equal(t, int64(2), teamCount, "should only be 1 team")
}
func testTeamCount(t *testing.T, ss store.Store) {
o1 := model.Team{}
o1.DisplayName = "DisplayName"
o1.Name = NewTestId()
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.AllowOpenInvite = true
team, err := ss.Team().Save(&o1)
require.NoError(t, err)
// not including deleted teams
teamCount, err := ss.Team().AnalyticsTeamCount(nil)
require.NoError(t, err)
require.NotEqual(t, 0, int(teamCount), "should be at least 1 team")
// delete the team for the next check
team.DeleteAt = model.GetMillis()
_, err = ss.Team().Update(team)
require.NoError(t, err)
// get the count of teams not including deleted
countNotIncludingDeleted, err := ss.Team().AnalyticsTeamCount(nil)
require.NoError(t, err)
// get the count of teams including deleted
countIncludingDeleted, err := ss.Team().AnalyticsTeamCount(&model.TeamSearch{IncludeDeleted: model.NewBool(true)})
require.NoError(t, err)
// count including deleted should be one greater than not including deleted
require.Equal(t, countNotIncludingDeleted+1, countIncludingDeleted)
}
func testGetMembers(t *testing.T, ss store.Store) {
// Each user should have a mention count of exactly 1 in the DB at this point.
t.Run("Test GetMembers Order By UserID", func(t *testing.T) {
teamId1 := model.NewId()
teamId2 := model.NewId()
m1 := &model.TeamMember{TeamId: teamId1, UserId: "55555555555555555555555555"}
m2 := &model.TeamMember{TeamId: teamId1, UserId: "11111111111111111111111111"}
m3 := &model.TeamMember{TeamId: teamId1, UserId: "33333333333333333333333333"}
m4 := &model.TeamMember{TeamId: teamId1, UserId: "22222222222222222222222222"}
m5 := &model.TeamMember{TeamId: teamId1, UserId: "44444444444444444444444444"}
m6 := &model.TeamMember{TeamId: teamId2, UserId: "00000000000000000000000000"}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3, m4, m5, m6}, -1)
require.NoError(t, nErr)
// Gets users ordered by UserId
ms, err := ss.Team().GetMembers(teamId1, 0, 100, nil)
require.NoError(t, err)
assert.Len(t, ms, 5)
assert.Equal(t, "11111111111111111111111111", ms[0].UserId)
assert.Equal(t, "22222222222222222222222222", ms[1].UserId)
assert.Equal(t, "33333333333333333333333333", ms[2].UserId)
assert.Equal(t, "44444444444444444444444444", ms[3].UserId)
assert.Equal(t, "55555555555555555555555555", ms[4].UserId)
})
t.Run("Test GetMembers Order By Username And Exclude Deleted Members", func(t *testing.T) {
teamId1 := model.NewId()
teamId2 := model.NewId()
u1 := &model.User{Username: "a", Email: MakeEmail(), DeleteAt: int64(1)}
u2 := &model.User{Username: "c", Email: MakeEmail()}
u3 := &model.User{Username: "b", Email: MakeEmail(), DeleteAt: int64(1)}
u4 := &model.User{Username: "f", Email: MakeEmail()}
u5 := &model.User{Username: "e", Email: MakeEmail(), DeleteAt: int64(1)}
u6 := &model.User{Username: "d", Email: MakeEmail()}
u1, err := ss.User().Save(u1)
require.NoError(t, err)
u2, err = ss.User().Save(u2)
require.NoError(t, err)
u3, err = ss.User().Save(u3)
require.NoError(t, err)
u4, err = ss.User().Save(u4)
require.NoError(t, err)
u5, err = ss.User().Save(u5)
require.NoError(t, err)
u6, err = ss.User().Save(u6)
require.NoError(t, err)
m1 := &model.TeamMember{TeamId: teamId1, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: teamId1, UserId: u2.Id}
m3 := &model.TeamMember{TeamId: teamId1, UserId: u3.Id}
m4 := &model.TeamMember{TeamId: teamId1, UserId: u4.Id}
m5 := &model.TeamMember{TeamId: teamId1, UserId: u5.Id}
m6 := &model.TeamMember{TeamId: teamId2, UserId: u6.Id}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3, m4, m5, m6}, -1)
require.NoError(t, nErr)
// Gets users ordered by UserName
ms, nErr := ss.Team().GetMembers(teamId1, 0, 100, &model.TeamMembersGetOptions{Sort: model.USERNAME})
require.NoError(t, nErr)
assert.Len(t, ms, 5)
assert.Equal(t, u1.Id, ms[0].UserId)
assert.Equal(t, u3.Id, ms[1].UserId)
assert.Equal(t, u2.Id, ms[2].UserId)
assert.Equal(t, u5.Id, ms[3].UserId)
assert.Equal(t, u4.Id, ms[4].UserId)
// Gets users ordered by UserName and excludes deleted members
ms, nErr = ss.Team().GetMembers(teamId1, 0, 100, &model.TeamMembersGetOptions{Sort: model.USERNAME, ExcludeDeletedUsers: true})
require.NoError(t, nErr)
assert.Len(t, ms, 2)
assert.Equal(t, u2.Id, ms[0].UserId)
assert.Equal(t, u4.Id, ms[1].UserId)
})
t.Run("Test GetMembers Excluded Deleted Users", func(t *testing.T) {
teamId1 := model.NewId()
teamId2 := model.NewId()
u1 := &model.User{Email: MakeEmail()}
u2 := &model.User{Email: MakeEmail(), DeleteAt: int64(1)}
u3 := &model.User{Email: MakeEmail()}
u4 := &model.User{Email: MakeEmail(), DeleteAt: int64(3)}
u5 := &model.User{Email: MakeEmail()}
u6 := &model.User{Email: MakeEmail(), DeleteAt: int64(5)}
u1, err := ss.User().Save(u1)
require.NoError(t, err)
u2, err = ss.User().Save(u2)
require.NoError(t, err)
u3, err = ss.User().Save(u3)
require.NoError(t, err)
u4, err = ss.User().Save(u4)
require.NoError(t, err)
u5, err = ss.User().Save(u5)
require.NoError(t, err)
u6, err = ss.User().Save(u6)
require.NoError(t, err)
m1 := &model.TeamMember{TeamId: teamId1, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: teamId1, UserId: u2.Id}
m3 := &model.TeamMember{TeamId: teamId1, UserId: u3.Id}
m4 := &model.TeamMember{TeamId: teamId1, UserId: u4.Id}
m5 := &model.TeamMember{TeamId: teamId1, UserId: u5.Id}
m6 := &model.TeamMember{TeamId: teamId2, UserId: u6.Id}
t1, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(m2, -1)
require.NoError(t, nErr)
t3, nErr := ss.Team().SaveMember(m3, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(m4, -1)
require.NoError(t, nErr)
t5, nErr := ss.Team().SaveMember(m5, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(m6, -1)
require.NoError(t, nErr)
// Gets users ordered by UserName
ms, nErr := ss.Team().GetMembers(teamId1, 0, 100, &model.TeamMembersGetOptions{ExcludeDeletedUsers: true})
require.NoError(t, nErr)
assert.Len(t, ms, 3)
require.ElementsMatch(t, ms, [3]*model.TeamMember{t1, t3, t5})
})
}
func testTeamMembers(t *testing.T, ss store.Store) {
teamId1 := model.NewId()
teamId2 := model.NewId()
m1 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
m2 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
m3 := &model.TeamMember{TeamId: teamId2, UserId: model.NewId()}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3}, -1)
require.NoError(t, nErr)
ms, err := ss.Team().GetMembers(teamId1, 0, 100, nil)
require.NoError(t, err)
assert.Len(t, ms, 2)
ms, err = ss.Team().GetMembers(teamId2, 0, 100, nil)
require.NoError(t, err)
require.Len(t, ms, 1)
require.Equal(t, m3.UserId, ms[0].UserId)
ctx := context.Background()
ms, err = ss.Team().GetTeamsForUser(ctx, m1.UserId, "", true)
require.NoError(t, err)
require.Len(t, ms, 1)
require.Equal(t, m1.TeamId, ms[0].TeamId)
err = ss.Team().RemoveMember(teamId1, m1.UserId)
require.NoError(t, err)
ms, err = ss.Team().GetMembers(teamId1, 0, 100, nil)
require.NoError(t, err)
require.Len(t, ms, 1)
require.Equal(t, m2.UserId, ms[0].UserId)
_, nErr = ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
err = ss.Team().RemoveAllMembersByTeam(teamId1)
require.NoError(t, err)
ms, err = ss.Team().GetMembers(teamId1, 0, 100, nil)
require.NoError(t, err)
require.Empty(t, ms)
uid := model.NewId()
m4 := &model.TeamMember{TeamId: teamId1, UserId: uid}
m5 := &model.TeamMember{TeamId: teamId2, UserId: uid}
_, nErr = ss.Team().SaveMultipleMembers([]*model.TeamMember{m4, m5}, -1)
require.NoError(t, nErr)
ms, err = ss.Team().GetTeamsForUser(ctx, uid, "", true)
require.NoError(t, err)
require.Len(t, ms, 2)
ms, err = ss.Team().GetTeamsForUser(ctx, uid, teamId2, true)
require.NoError(t, err)
require.Len(t, ms, 1)
m4.DeleteAt = model.GetMillis()
_, err = ss.Team().UpdateMember(m4)
require.NoError(t, err)
ms, err = ss.Team().GetTeamsForUser(ctx, uid, "", true)
require.NoError(t, err)
require.Len(t, ms, 2)
ms, err = ss.Team().GetTeamsForUser(ctx, uid, "", false)
require.NoError(t, err)
require.Len(t, ms, 1)
nErr = ss.Team().RemoveAllMembersByUser(uid)
require.NoError(t, nErr)
ms, err = ss.Team().GetTeamsForUser(ctx, m1.UserId, "", true)
require.NoError(t, err)
require.Empty(t, ms)
}
func testTeamSaveMember(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
t.Run("not valid team member", func(t *testing.T) {
member := &model.TeamMember{TeamId: "wrong", UserId: u1.Id}
_, nErr := ss.Team().SaveMember(member, -1)
require.Error(t, nErr)
require.Equal(t, "TeamMember.IsValid: model.team_member.is_valid.team_id.app_error", nErr.Error())
})
t.Run("too many members", func(t *testing.T) {
member := &model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}
_, nErr := ss.Team().SaveMember(member, 0)
require.Error(t, nErr)
require.Equal(t, "limit exceeded: what: TeamMember count: 1 metadata: team members limit exceeded", nErr.Error())
})
t.Run("too many members because previous existing members", func(t *testing.T) {
teamID := model.NewId()
m1 := &model.TeamMember{TeamId: teamID, UserId: u1.Id}
_, nErr := ss.Team().SaveMember(m1, 1)
require.NoError(t, nErr)
m2 := &model.TeamMember{TeamId: teamID, UserId: u2.Id}
_, nErr = ss.Team().SaveMember(m2, 1)
require.Error(t, nErr)
require.Equal(t, "limit exceeded: what: TeamMember count: 2 metadata: team members limit exceeded", nErr.Error())
})
t.Run("duplicated entries should fail", func(t *testing.T) {
teamID1 := model.NewId()
m1 := &model.TeamMember{TeamId: teamID1, UserId: u1.Id}
_, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
m2 := &model.TeamMember{TeamId: teamID1, UserId: u1.Id}
_, nErr = ss.Team().SaveMember(m2, -1)
require.Error(t, nErr)
require.IsType(t, &store.ErrConflict{}, nErr)
})
t.Run("insert member correctly (in team without scheme)", func(t *testing.T) {
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "team user implicit",
SchemeUser: true,
ExpectedRoles: "team_user",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit",
ExplicitRoles: "team_user",
ExpectedRoles: "team_user",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit",
SchemeGuest: true,
ExpectedRoles: "team_guest",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit",
ExplicitRoles: "team_guest",
ExpectedRoles: "team_guest",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: "team_user team_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit",
ExplicitRoles: "team_user team_admin",
ExpectedRoles: "team_user team_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit and explicit custom role",
ExplicitRoles: "team_user test",
ExpectedRoles: "test team_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit and explicit custom role",
ExplicitRoles: "team_guest test",
ExpectedRoles: "test team_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_user team_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit and explicit custom role",
ExplicitRoles: "team_user team_admin test",
ExpectedRoles: "test team_user team_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member := &model.TeamMember{
TeamId: team.Id,
UserId: u1.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
}
member, nErr := ss.Team().SaveMember(member, -1)
require.NoError(t, nErr)
defer ss.Team().RemoveMember(team.Id, u1.Id)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert member correctly (in team with scheme)", func(t *testing.T) {
ts := &model.Scheme{
Name: NewTestId(),
DisplayName: NewTestId(),
Description: NewTestId(),
Scope: model.SchemeScopeTeam,
}
ts, nErr := ss.Scheme().Save(ts)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &ts.Id,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "team user implicit",
SchemeUser: true,
ExpectedRoles: ts.DefaultTeamUserRole,
ExpectedSchemeUser: true,
},
{
Name: "team user explicit",
ExplicitRoles: "team_user",
ExpectedRoles: ts.DefaultTeamUserRole,
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit",
SchemeGuest: true,
ExpectedRoles: ts.DefaultTeamGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit",
ExplicitRoles: "team_guest",
ExpectedRoles: ts.DefaultTeamGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit",
ExplicitRoles: "team_user team_admin",
ExpectedRoles: ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit and explicit custom role",
ExplicitRoles: "team_user test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit and explicit custom role",
ExplicitRoles: "team_guest test",
ExpectedRoles: "test " + ts.DefaultTeamGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit and explicit custom role",
ExplicitRoles: "team_user team_admin test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member := &model.TeamMember{
TeamId: team.Id,
UserId: u1.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
}
member, nErr := ss.Team().SaveMember(member, -1)
require.NoError(t, nErr)
defer ss.Team().RemoveMember(team.Id, u1.Id)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
}
func testTeamSaveMultipleMembers(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u3, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u4, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
t.Run("any not valid team member", func(t *testing.T) {
m1 := &model.TeamMember{TeamId: "wrong", UserId: u1.Id}
m2 := &model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2}, -1)
require.Error(t, nErr)
require.Equal(t, "TeamMember.IsValid: model.team_member.is_valid.team_id.app_error", nErr.Error())
})
t.Run("too many members in one team", func(t *testing.T) {
teamID := model.NewId()
m1 := &model.TeamMember{TeamId: teamID, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: teamID, UserId: u2.Id}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2}, 0)
require.Error(t, nErr)
require.Equal(t, "limit exceeded: what: TeamMember count: 2 metadata: team members limit exceeded", nErr.Error())
})
t.Run("too many members in one team because previous existing members", func(t *testing.T) {
teamID := model.NewId()
m1 := &model.TeamMember{TeamId: teamID, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: teamID, UserId: u2.Id}
m3 := &model.TeamMember{TeamId: teamID, UserId: u3.Id}
m4 := &model.TeamMember{TeamId: teamID, UserId: u4.Id}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2}, 3)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMultipleMembers([]*model.TeamMember{m3, m4}, 3)
require.Error(t, nErr)
require.Equal(t, "limit exceeded: what: TeamMember count: 4 metadata: team members limit exceeded", nErr.Error())
})
t.Run("too many members, but in different teams", func(t *testing.T) {
teamID1 := model.NewId()
teamID2 := model.NewId()
m1 := &model.TeamMember{TeamId: teamID1, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: teamID1, UserId: u2.Id}
m3 := &model.TeamMember{TeamId: teamID1, UserId: u3.Id}
m4 := &model.TeamMember{TeamId: teamID2, UserId: u1.Id}
m5 := &model.TeamMember{TeamId: teamID2, UserId: u2.Id}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3, m4, m5}, 2)
require.Error(t, nErr)
require.Equal(t, "limit exceeded: what: TeamMember count: 3 metadata: team members limit exceeded", nErr.Error())
})
t.Run("duplicated entries should fail", func(t *testing.T) {
teamID1 := model.NewId()
m1 := &model.TeamMember{TeamId: teamID1, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: teamID1, UserId: u1.Id}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2}, 10)
require.Error(t, nErr)
require.IsType(t, &store.ErrConflict{}, nErr)
})
t.Run("insert members correctly (in team without scheme)", func(t *testing.T) {
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "team user implicit",
SchemeUser: true,
ExpectedRoles: "team_user",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit",
ExplicitRoles: "team_user",
ExpectedRoles: "team_user",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit",
SchemeGuest: true,
ExpectedRoles: "team_guest",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit",
ExplicitRoles: "team_guest",
ExpectedRoles: "team_guest",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: "team_user team_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit",
ExplicitRoles: "team_user team_admin",
ExpectedRoles: "team_user team_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit and explicit custom role",
ExplicitRoles: "team_user test",
ExpectedRoles: "test team_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit and explicit custom role",
ExplicitRoles: "team_guest test",
ExpectedRoles: "test team_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_user team_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit and explicit custom role",
ExplicitRoles: "team_user team_admin test",
ExpectedRoles: "test team_user team_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member := &model.TeamMember{
TeamId: team.Id,
UserId: u1.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
}
otherMember := &model.TeamMember{
TeamId: team.Id,
UserId: u2.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
}
var members []*model.TeamMember
members, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{member, otherMember}, -1)
require.NoError(t, nErr)
require.Len(t, members, 2)
member = members[0]
defer ss.Team().RemoveMember(team.Id, u1.Id)
defer ss.Team().RemoveMember(team.Id, u2.Id)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert members correctly (in team with scheme)", func(t *testing.T) {
ts := &model.Scheme{
Name: NewTestId(),
DisplayName: NewTestId(),
Description: NewTestId(),
Scope: model.SchemeScopeTeam,
}
ts, nErr := ss.Scheme().Save(ts)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &ts.Id,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "team user implicit",
SchemeUser: true,
ExpectedRoles: ts.DefaultTeamUserRole,
ExpectedSchemeUser: true,
},
{
Name: "team user explicit",
ExplicitRoles: "team_user",
ExpectedRoles: ts.DefaultTeamUserRole,
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit",
SchemeGuest: true,
ExpectedRoles: ts.DefaultTeamGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit",
ExplicitRoles: "team_guest",
ExpectedRoles: ts.DefaultTeamGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit",
ExplicitRoles: "team_user team_admin",
ExpectedRoles: ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit and explicit custom role",
ExplicitRoles: "team_user test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit and explicit custom role",
ExplicitRoles: "team_guest test",
ExpectedRoles: "test " + ts.DefaultTeamGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit and explicit custom role",
ExplicitRoles: "team_user team_admin test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member := &model.TeamMember{
TeamId: team.Id,
UserId: u1.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
}
otherMember := &model.TeamMember{
TeamId: team.Id,
UserId: u2.Id,
SchemeGuest: tc.SchemeGuest,
SchemeUser: tc.SchemeUser,
SchemeAdmin: tc.SchemeAdmin,
ExplicitRoles: tc.ExplicitRoles,
}
members, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{member, otherMember}, -1)
require.NoError(t, nErr)
require.Len(t, members, 2)
member = members[0]
defer ss.Team().RemoveMember(team.Id, u1.Id)
defer ss.Team().RemoveMember(team.Id, u2.Id)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
}
func testTeamUpdateMember(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
t.Run("not valid team member", func(t *testing.T) {
member := &model.TeamMember{TeamId: "wrong", UserId: u1.Id}
_, nErr := ss.Team().UpdateMember(member)
require.Error(t, nErr)
var appErr *model.AppError
require.True(t, errors.As(nErr, &appErr))
require.Equal(t, "model.team_member.is_valid.team_id.app_error", appErr.Id)
})
t.Run("insert member correctly (in team without scheme)", func(t *testing.T) {
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
member := &model.TeamMember{TeamId: team.Id, UserId: u1.Id}
member, nErr = ss.Team().SaveMember(member, -1)
require.NoError(t, nErr)
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "team user implicit",
SchemeUser: true,
ExpectedRoles: "team_user",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit",
ExplicitRoles: "team_user",
ExpectedRoles: "team_user",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit",
SchemeGuest: true,
ExpectedRoles: "team_guest",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit",
ExplicitRoles: "team_guest",
ExpectedRoles: "team_guest",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: "team_user team_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit",
ExplicitRoles: "team_user team_admin",
ExpectedRoles: "team_user team_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit and explicit custom role",
ExplicitRoles: "team_user test",
ExpectedRoles: "test team_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit and explicit custom role",
ExplicitRoles: "team_guest test",
ExpectedRoles: "test team_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_user team_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit and explicit custom role",
ExplicitRoles: "team_user team_admin test",
ExpectedRoles: "test team_user team_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member.SchemeGuest = tc.SchemeGuest
member.SchemeUser = tc.SchemeUser
member.SchemeAdmin = tc.SchemeAdmin
member.ExplicitRoles = tc.ExplicitRoles
member, nErr = ss.Team().UpdateMember(member)
require.NoError(t, nErr)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert member correctly (in team with scheme)", func(t *testing.T) {
ts := &model.Scheme{
Name: NewTestId(),
DisplayName: NewTestId(),
Description: NewTestId(),
Scope: model.SchemeScopeTeam,
}
ts, nErr := ss.Scheme().Save(ts)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &ts.Id,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
member := &model.TeamMember{TeamId: team.Id, UserId: u1.Id}
member, nErr = ss.Team().SaveMember(member, -1)
require.NoError(t, nErr)
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "team user implicit",
SchemeUser: true,
ExpectedRoles: ts.DefaultTeamUserRole,
ExpectedSchemeUser: true,
},
{
Name: "team user explicit",
ExplicitRoles: "team_user",
ExpectedRoles: ts.DefaultTeamUserRole,
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit",
SchemeGuest: true,
ExpectedRoles: ts.DefaultTeamGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit",
ExplicitRoles: "team_guest",
ExpectedRoles: ts.DefaultTeamGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit",
ExplicitRoles: "team_user team_admin",
ExpectedRoles: ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit and explicit custom role",
ExplicitRoles: "team_user test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit and explicit custom role",
ExplicitRoles: "team_guest test",
ExpectedRoles: "test " + ts.DefaultTeamGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit and explicit custom role",
ExplicitRoles: "team_user team_admin test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member.SchemeGuest = tc.SchemeGuest
member.SchemeUser = tc.SchemeUser
member.SchemeAdmin = tc.SchemeAdmin
member.ExplicitRoles = tc.ExplicitRoles
member, nErr = ss.Team().UpdateMember(member)
require.NoError(t, nErr)
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
}
func testTeamUpdateMultipleMembers(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
t.Run("any not valid team member", func(t *testing.T) {
m1 := &model.TeamMember{TeamId: "wrong", UserId: u1.Id}
m2 := &model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}
_, nErr := ss.Team().UpdateMultipleMembers([]*model.TeamMember{m1, m2})
require.Error(t, nErr)
var appErr *model.AppError
require.True(t, errors.As(nErr, &appErr))
require.Equal(t, "model.team_member.is_valid.team_id.app_error", appErr.Id)
})
t.Run("update members correctly (in team without scheme)", func(t *testing.T) {
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
team, nErr := ss.Team().Save(team)
require.NoError(t, nErr)
member := &model.TeamMember{TeamId: team.Id, UserId: u1.Id}
otherMember := &model.TeamMember{TeamId: team.Id, UserId: u2.Id}
var members []*model.TeamMember
members, nErr = ss.Team().SaveMultipleMembers([]*model.TeamMember{member, otherMember}, -1)
require.NoError(t, nErr)
require.Len(t, members, 2)
member = members[0]
otherMember = members[1]
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "team user implicit",
SchemeUser: true,
ExpectedRoles: "team_user",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit",
ExplicitRoles: "team_user",
ExpectedRoles: "team_user",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit",
SchemeGuest: true,
ExpectedRoles: "team_guest",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit",
ExplicitRoles: "team_guest",
ExpectedRoles: "team_guest",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: "team_user team_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit",
ExplicitRoles: "team_user team_admin",
ExpectedRoles: "team_user team_admin",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit and explicit custom role",
ExplicitRoles: "team_user test",
ExpectedRoles: "test team_user",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit and explicit custom role",
ExplicitRoles: "team_guest test",
ExpectedRoles: "test team_guest",
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test team_user team_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit and explicit custom role",
ExplicitRoles: "team_user team_admin test",
ExpectedRoles: "test team_user team_admin",
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member.SchemeGuest = tc.SchemeGuest
member.SchemeUser = tc.SchemeUser
member.SchemeAdmin = tc.SchemeAdmin
member.ExplicitRoles = tc.ExplicitRoles
var members []*model.TeamMember
members, nErr = ss.Team().UpdateMultipleMembers([]*model.TeamMember{member, otherMember})
require.NoError(t, nErr)
require.Len(t, members, 2)
member = members[0]
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
t.Run("insert members correctly (in team with scheme)", func(t *testing.T) {
ts := &model.Scheme{
Name: NewTestId(),
DisplayName: NewTestId(),
Description: NewTestId(),
Scope: model.SchemeScopeTeam,
}
ts, nErr := ss.Scheme().Save(ts)
require.NoError(t, nErr)
team := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &ts.Id,
}
team, nErr = ss.Team().Save(team)
require.NoError(t, nErr)
member := &model.TeamMember{TeamId: team.Id, UserId: u1.Id}
otherMember := &model.TeamMember{TeamId: team.Id, UserId: u2.Id}
members, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{member, otherMember}, -1)
require.NoError(t, nErr)
require.Len(t, members, 2)
member = members[0]
otherMember = members[1]
testCases := []struct {
Name string
SchemeGuest bool
SchemeUser bool
SchemeAdmin bool
ExplicitRoles string
ExpectedRoles string
ExpectedExplicitRoles string
ExpectedSchemeGuest bool
ExpectedSchemeUser bool
ExpectedSchemeAdmin bool
}{
{
Name: "team user implicit",
SchemeUser: true,
ExpectedRoles: ts.DefaultTeamUserRole,
ExpectedSchemeUser: true,
},
{
Name: "team user explicit",
ExplicitRoles: "team_user",
ExpectedRoles: ts.DefaultTeamUserRole,
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit",
SchemeGuest: true,
ExpectedRoles: ts.DefaultTeamGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit",
ExplicitRoles: "team_guest",
ExpectedRoles: ts.DefaultTeamGuestRole,
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit",
SchemeUser: true,
SchemeAdmin: true,
ExpectedRoles: ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit",
ExplicitRoles: "team_user team_admin",
ExpectedRoles: ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team user implicit and explicit custom role",
SchemeUser: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team user explicit and explicit custom role",
ExplicitRoles: "team_user test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
},
{
Name: "team guest implicit and explicit custom role",
SchemeGuest: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team guest explicit and explicit custom role",
ExplicitRoles: "team_guest test",
ExpectedRoles: "test " + ts.DefaultTeamGuestRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeGuest: true,
},
{
Name: "team admin implicit and explicit custom role",
SchemeUser: true,
SchemeAdmin: true,
ExplicitRoles: "test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team admin explicit and explicit custom role",
ExplicitRoles: "team_user team_admin test",
ExpectedRoles: "test " + ts.DefaultTeamUserRole + " " + ts.DefaultTeamAdminRole,
ExpectedExplicitRoles: "test",
ExpectedSchemeUser: true,
ExpectedSchemeAdmin: true,
},
{
Name: "team member with only explicit custom roles",
ExplicitRoles: "test test2",
ExpectedRoles: "test test2",
ExpectedExplicitRoles: "test test2",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
member.SchemeGuest = tc.SchemeGuest
member.SchemeUser = tc.SchemeUser
member.SchemeAdmin = tc.SchemeAdmin
member.ExplicitRoles = tc.ExplicitRoles
members, err := ss.Team().UpdateMultipleMembers([]*model.TeamMember{member, otherMember})
require.NoError(t, err)
require.Len(t, members, 2)
member = members[0]
assert.Equal(t, tc.ExpectedRoles, member.Roles)
assert.Equal(t, tc.ExpectedExplicitRoles, member.ExplicitRoles)
assert.Equal(t, tc.ExpectedSchemeGuest, member.SchemeGuest)
assert.Equal(t, tc.ExpectedSchemeUser, member.SchemeUser)
assert.Equal(t, tc.ExpectedSchemeAdmin, member.SchemeAdmin)
})
}
})
}
func testTeamRemoveMember(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u3, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u4, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
teamID := model.NewId()
m1 := &model.TeamMember{TeamId: teamID, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: teamID, UserId: u2.Id}
m3 := &model.TeamMember{TeamId: teamID, UserId: u3.Id}
m4 := &model.TeamMember{TeamId: teamID, UserId: u4.Id}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3, m4}, -1)
require.NoError(t, nErr)
t.Run("remove member from not existing team", func(t *testing.T) {
nErr = ss.Team().RemoveMember("not-existing-team", u1.Id)
require.NoError(t, nErr)
var membersOtherTeam []*model.TeamMember
membersOtherTeam, nErr = ss.Team().GetMembers(teamID, 0, 100, nil)
require.NoError(t, nErr)
require.Len(t, membersOtherTeam, 4)
})
t.Run("remove not existing member from an existing team", func(t *testing.T) {
nErr = ss.Team().RemoveMember(teamID, model.NewId())
require.NoError(t, nErr)
var membersOtherTeam []*model.TeamMember
membersOtherTeam, nErr = ss.Team().GetMembers(teamID, 0, 100, nil)
require.NoError(t, nErr)
require.Len(t, membersOtherTeam, 4)
})
t.Run("remove existing member from an existing team", func(t *testing.T) {
nErr = ss.Team().RemoveMember(teamID, u1.Id)
require.NoError(t, nErr)
defer ss.Team().SaveMember(m1, -1)
var membersOtherTeam []*model.TeamMember
membersOtherTeam, nErr = ss.Team().GetMembers(teamID, 0, 100, nil)
require.NoError(t, nErr)
require.Len(t, membersOtherTeam, 3)
})
}
func testTeamRemoveMembers(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u3, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
u4, err := ss.User().Save(&model.User{Username: model.NewId(), Email: MakeEmail()})
require.NoError(t, err)
teamID := model.NewId()
m1 := &model.TeamMember{TeamId: teamID, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: teamID, UserId: u2.Id}
m3 := &model.TeamMember{TeamId: teamID, UserId: u3.Id}
m4 := &model.TeamMember{TeamId: teamID, UserId: u4.Id}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3, m4}, -1)
require.NoError(t, nErr)
t.Run("remove members from not existing team", func(t *testing.T) {
nErr = ss.Team().RemoveMembers("not-existing-team", []string{u1.Id, u2.Id, u3.Id, u4.Id})
require.NoError(t, nErr)
var membersOtherTeam []*model.TeamMember
membersOtherTeam, nErr = ss.Team().GetMembers(teamID, 0, 100, nil)
require.NoError(t, nErr)
require.Len(t, membersOtherTeam, 4)
})
t.Run("remove not existing members from an existing team", func(t *testing.T) {
nErr = ss.Team().RemoveMembers(teamID, []string{model.NewId(), model.NewId()})
require.NoError(t, nErr)
var membersOtherTeam []*model.TeamMember
membersOtherTeam, nErr = ss.Team().GetMembers(teamID, 0, 100, nil)
require.NoError(t, nErr)
require.Len(t, membersOtherTeam, 4)
})
t.Run("remove not existing and not existing members from an existing team", func(t *testing.T) {
nErr = ss.Team().RemoveMembers(teamID, []string{u1.Id, u2.Id, model.NewId(), model.NewId()})
require.NoError(t, nErr)
defer ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2}, -1)
var membersOtherTeam []*model.TeamMember
membersOtherTeam, nErr = ss.Team().GetMembers(teamID, 0, 100, nil)
require.NoError(t, nErr)
require.Len(t, membersOtherTeam, 2)
})
t.Run("remove existing members from an existing team", func(t *testing.T) {
nErr = ss.Team().RemoveMembers(teamID, []string{u1.Id, u2.Id, u3.Id})
require.NoError(t, nErr)
defer ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3}, -1)
var membersOtherTeam []*model.TeamMember
membersOtherTeam, nErr = ss.Team().GetMembers(teamID, 0, 100, nil)
require.NoError(t, nErr)
require.Len(t, membersOtherTeam, 1)
})
}
func testTeamMembersWithPagination(t *testing.T, ss store.Store) {
teamId1 := model.NewId()
teamId2 := model.NewId()
m1 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
m2 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
m3 := &model.TeamMember{TeamId: teamId2, UserId: model.NewId()}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3}, -1)
require.NoError(t, nErr)
ms, errTeam := ss.Team().GetTeamsForUserWithPagination(m1.UserId, 0, 1)
require.NoError(t, errTeam)
require.Len(t, ms, 1)
require.Equal(t, m1.TeamId, ms[0].TeamId)
e := ss.Team().RemoveMember(teamId1, m1.UserId)
require.NoError(t, e)
ms, err := ss.Team().GetMembers(teamId1, 0, 100, nil)
require.NoError(t, err)
require.Len(t, ms, 1)
require.Equal(t, m2.UserId, ms[0].UserId)
_, nErr = ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
err = ss.Team().RemoveAllMembersByTeam(teamId1)
require.NoError(t, err)
uid := model.NewId()
m4 := &model.TeamMember{TeamId: teamId1, UserId: uid}
m5 := &model.TeamMember{TeamId: teamId2, UserId: uid}
_, nErr = ss.Team().SaveMultipleMembers([]*model.TeamMember{m4, m5}, -1)
require.NoError(t, nErr)
result, err := ss.Team().GetTeamsForUserWithPagination(uid, 0, 1)
require.NoError(t, err)
require.Len(t, result, 1)
nErr = ss.Team().RemoveAllMembersByUser(uid)
require.NoError(t, nErr)
result, err = ss.Team().GetTeamsForUserWithPagination(uid, 1, 1)
require.NoError(t, err)
require.Empty(t, result)
}
func testSaveTeamMemberMaxMembers(t *testing.T, ss store.Store) {
maxUsersPerTeam := 5
team, errSave := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Type: model.TeamOpen,
})
require.NoError(t, errSave)
defer func() {
ss.Team().PermanentDelete(team.Id)
}()
userIds := make([]string, maxUsersPerTeam)
for i := 0; i < maxUsersPerTeam; i++ {
user, err := ss.User().Save(&model.User{
Username: NewTestId(),
Email: MakeEmail(),
})
require.NoError(t, err)
userIds[i] = user.Id
defer func(userId string) {
ss.User().PermanentDelete(userId)
}(userIds[i])
_, nErr := ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: userIds[i],
}, maxUsersPerTeam)
require.NoError(t, nErr)
defer func(userId string) {
ss.Team().RemoveMember(team.Id, userId)
}(userIds[i])
}
totalMemberCount, err := ss.Team().GetTotalMemberCount(team.Id, nil)
require.NoError(t, err)
require.Equal(t, int(totalMemberCount), maxUsersPerTeam, "should start with 5 team members, had %v instead", totalMemberCount)
user, nErr := ss.User().Save(&model.User{
Username: NewTestId(),
Email: MakeEmail(),
})
require.NoError(t, nErr)
newUserId := user.Id
defer func() {
ss.User().PermanentDelete(newUserId)
}()
_, nErr = ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: newUserId,
}, maxUsersPerTeam)
require.Error(t, nErr, "shouldn't be able to save member when at maximum members per team")
totalMemberCount, teamErr := ss.Team().GetTotalMemberCount(team.Id, nil)
require.NoError(t, teamErr)
require.Equal(t, maxUsersPerTeam, int(totalMemberCount), "should still have 5 team members, had %v instead", totalMemberCount)
// Leaving the team from the UI sets DeleteAt instead of using TeamStore.RemoveMember
_, teamErr = ss.Team().UpdateMember(&model.TeamMember{
TeamId: team.Id,
UserId: userIds[0],
DeleteAt: 1234,
})
require.NoError(t, teamErr)
totalMemberCount, teamErr = ss.Team().GetTotalMemberCount(team.Id, nil)
require.NoError(t, teamErr)
require.Equal(t, maxUsersPerTeam-1, int(totalMemberCount), "should now only have 4 team members, had %v instead", totalMemberCount)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: team.Id, UserId: newUserId}, maxUsersPerTeam)
require.NoError(t, nErr, "should've been able to save new member after deleting one")
defer ss.Team().RemoveMember(team.Id, newUserId)
totalMemberCount, teamErr = ss.Team().GetTotalMemberCount(team.Id, nil)
require.NoError(t, teamErr)
require.Equal(t, maxUsersPerTeam, int(totalMemberCount), "should have 5 team members again, had %v instead", totalMemberCount)
// Deactivating a user should make them stop counting against max members
user2, nErr := ss.User().Get(context.Background(), userIds[1])
require.NoError(t, nErr)
user2.DeleteAt = 1234
_, nErr = ss.User().Update(user2, true)
require.NoError(t, nErr)
user, nErr = ss.User().Save(&model.User{
Username: NewTestId(),
Email: MakeEmail(),
})
require.NoError(t, nErr)
newUserId2 := user.Id
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: team.Id, UserId: newUserId2}, maxUsersPerTeam)
require.NoError(t, nErr, "should've been able to save new member after deleting one")
defer ss.Team().RemoveMember(team.Id, newUserId2)
}
func testGetTeamMember(t *testing.T, ss store.Store) {
teamId1 := model.NewId()
m1 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
_, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
var rm1 *model.TeamMember
rm1, err := ss.Team().GetMember(context.Background(), m1.TeamId, m1.UserId)
require.NoError(t, err)
require.Equal(t, rm1.TeamId, m1.TeamId, "bad team id")
require.Equal(t, rm1.UserId, m1.UserId, "bad user id")
_, err = ss.Team().GetMember(context.Background(), m1.TeamId, "")
require.Error(t, err, "empty user id - should have failed")
_, err = ss.Team().GetMember(context.Background(), "", m1.UserId)
require.Error(t, err, "empty team id - should have failed")
// Test with a custom team scheme.
s2 := &model.Scheme{
Name: NewTestId(),
DisplayName: NewTestId(),
Description: NewTestId(),
Scope: model.SchemeScopeTeam,
}
s2, nErr = ss.Scheme().Save(s2)
require.NoError(t, nErr)
t.Log(s2)
t2, nErr := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: NewTestId(),
Type: model.TeamOpen,
SchemeId: &s2.Id,
})
require.NoError(t, nErr)
defer func() {
ss.Team().PermanentDelete(t2.Id)
}()
m2 := &model.TeamMember{TeamId: t2.Id, UserId: model.NewId(), SchemeUser: true}
_, nErr = ss.Team().SaveMember(m2, -1)
require.NoError(t, nErr)
m3, err := ss.Team().GetMember(context.Background(), m2.TeamId, m2.UserId)
require.NoError(t, err)
t.Log(m3)
assert.Equal(t, s2.DefaultTeamUserRole, m3.Roles)
m4 := &model.TeamMember{TeamId: t2.Id, UserId: model.NewId(), SchemeGuest: true}
_, nErr = ss.Team().SaveMember(m4, -1)
require.NoError(t, nErr)
m5, err := ss.Team().GetMember(context.Background(), m4.TeamId, m4.UserId)
require.NoError(t, err)
assert.Equal(t, s2.DefaultTeamGuestRole, m5.Roles)
}
func testGetTeamMembersByIds(t *testing.T, ss store.Store) {
teamId1 := model.NewId()
m1 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
_, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
var r []*model.TeamMember
r, err := ss.Team().GetMembersByIds(m1.TeamId, []string{m1.UserId}, nil)
require.NoError(t, err)
rm1 := r[0]
require.Equal(t, rm1.TeamId, m1.TeamId, "bad team id")
require.Equal(t, rm1.UserId, m1.UserId, "bad user id")
m2 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
_, nErr = ss.Team().SaveMember(m2, -1)
require.NoError(t, nErr)
rm, err := ss.Team().GetMembersByIds(m1.TeamId, []string{m1.UserId, m2.UserId, model.NewId()}, nil)
require.NoError(t, err)
require.Len(t, rm, 2, "return wrong number of results")
_, err = ss.Team().GetMembersByIds(m1.TeamId, []string{}, nil)
require.Error(t, err, "empty user ids - should have failed")
}
func testTeamStoreMemberCount(t *testing.T, ss store.Store) {
u1 := &model.User{}
u1.Email = MakeEmail()
_, err := ss.User().Save(u1)
require.NoError(t, err)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.DeleteAt = 1
_, err = ss.User().Save(u2)
require.NoError(t, err)
teamId1 := model.NewId()
m1 := &model.TeamMember{TeamId: teamId1, UserId: u1.Id}
_, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
m2 := &model.TeamMember{TeamId: teamId1, UserId: u2.Id}
_, nErr = ss.Team().SaveMember(m2, -1)
require.NoError(t, nErr)
var totalMemberCount int64
totalMemberCount, nErr = ss.Team().GetTotalMemberCount(teamId1, nil)
require.NoError(t, nErr)
require.Equal(t, int(totalMemberCount), 2, "wrong count")
var result int64
result, nErr = ss.Team().GetActiveMemberCount(teamId1, nil)
require.NoError(t, nErr)
require.Equal(t, 1, int(result), "wrong count")
m3 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
_, nErr = ss.Team().SaveMember(m3, -1)
require.NoError(t, nErr)
totalMemberCount, nErr = ss.Team().GetTotalMemberCount(teamId1, nil)
require.NoError(t, nErr)
require.Equal(t, 2, int(totalMemberCount), "wrong count")
result, nErr = ss.Team().GetActiveMemberCount(teamId1, nil)
require.NoError(t, nErr)
require.Equal(t, 1, int(result), "wrong count")
}
func testGetChannelUnreadsForAllTeams(t *testing.T, ss store.Store) {
teamId1 := model.NewId()
teamId2 := model.NewId()
uid := model.NewId()
m1 := &model.TeamMember{TeamId: teamId1, UserId: uid}
m2 := &model.TeamMember{TeamId: teamId2, UserId: uid}
_, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(m2, -1)
require.NoError(t, nErr)
c1 := &model.Channel{TeamId: m1.TeamId, Name: model.NewId(), DisplayName: "Town Square", Type: model.ChannelTypeOpen, TotalMsgCount: 100}
_, nErr = ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
c2 := &model.Channel{TeamId: m2.TeamId, Name: model.NewId(), DisplayName: "Town Square", Type: model.ChannelTypeOpen, TotalMsgCount: 100}
_, nErr = ss.Channel().Save(c2, -1)
require.NoError(t, nErr)
cm1 := &model.ChannelMember{ChannelId: c1.Id, UserId: m1.UserId, NotifyProps: model.GetDefaultChannelNotifyProps(), MsgCount: 90}
_, err := ss.Channel().SaveMember(cm1)
require.NoError(t, err)
cm2 := &model.ChannelMember{ChannelId: c2.Id, UserId: m2.UserId, NotifyProps: model.GetDefaultChannelNotifyProps(), MsgCount: 90}
_, err = ss.Channel().SaveMember(cm2)
require.NoError(t, err)
ms1, nErr := ss.Team().GetChannelUnreadsForAllTeams("", uid)
require.NoError(t, nErr)
membersMap := make(map[string]bool)
for i := range ms1 {
id := ms1[i].TeamId
if _, ok := membersMap[id]; !ok {
membersMap[id] = true
}
}
require.Len(t, membersMap, 2, "Should be the unreads for all the teams")
require.Equal(t, 10, int(ms1[0].MsgCount), "subtraction failed")
ms2, nErr := ss.Team().GetChannelUnreadsForAllTeams(teamId1, uid)
require.NoError(t, nErr)
membersMap = make(map[string]bool)
for i := range ms2 {
id := ms2[i].TeamId
if _, ok := membersMap[id]; !ok {
membersMap[id] = true
}
}
require.Len(t, membersMap, 1, "Should be the unreads for just one team")
require.Equal(t, 10, int(ms2[0].MsgCount), "subtraction failed")
nErr = ss.Team().RemoveAllMembersByUser(uid)
require.NoError(t, nErr)
}
func testGetChannelUnreadsForTeam(t *testing.T, ss store.Store) {
teamId1 := model.NewId()
uid := model.NewId()
m1 := &model.TeamMember{TeamId: teamId1, UserId: uid}
_, nErr := ss.Team().SaveMember(m1, -1)
require.NoError(t, nErr)
c1 := &model.Channel{TeamId: m1.TeamId, Name: model.NewId(), DisplayName: "Town Square", Type: model.ChannelTypeOpen, TotalMsgCount: 100}
_, nErr = ss.Channel().Save(c1, -1)
require.NoError(t, nErr)
c2 := &model.Channel{TeamId: m1.TeamId, Name: model.NewId(), DisplayName: "Town Square", Type: model.ChannelTypeOpen, TotalMsgCount: 100}
_, nErr = ss.Channel().Save(c2, -1)
require.NoError(t, nErr)
cm1 := &model.ChannelMember{ChannelId: c1.Id, UserId: m1.UserId, NotifyProps: model.GetDefaultChannelNotifyProps(), MsgCount: 90}
_, nErr = ss.Channel().SaveMember(cm1)
require.NoError(t, nErr)
cm2 := &model.ChannelMember{ChannelId: c2.Id, UserId: m1.UserId, NotifyProps: model.GetDefaultChannelNotifyProps(), MsgCount: 90}
_, nErr = ss.Channel().SaveMember(cm2)
require.NoError(t, nErr)
ms, err := ss.Team().GetChannelUnreadsForTeam(m1.TeamId, m1.UserId)
require.NoError(t, err)
require.Len(t, ms, 2, "wrong length")
require.Equal(t, 10, int(ms[0].MsgCount), "subtraction failed")
}
func testUpdateLastTeamIconUpdate(t *testing.T, ss store.Store) {
// team icon initially updated a second ago
lastTeamIconUpdateInitial := model.GetMillis() - 1000
o1 := &model.Team{}
o1.DisplayName = "Display Name"
o1.Name = "z-z-z" + model.NewId() + "b"
o1.Email = MakeEmail()
o1.Type = model.TeamOpen
o1.LastTeamIconUpdate = lastTeamIconUpdateInitial
o1, err := ss.Team().Save(o1)
require.NoError(t, err)
curTime := model.GetMillis()
err = ss.Team().UpdateLastTeamIconUpdate(o1.Id, curTime)
require.NoError(t, err)
ro1, err := ss.Team().Get(o1.Id)
require.NoError(t, err)
require.Greater(t, ro1.LastTeamIconUpdate, lastTeamIconUpdateInitial, "LastTeamIconUpdate not updated")
}
func testGetTeamsByScheme(t *testing.T, ss store.Store) {
// Create some schemes.
s1 := &model.Scheme{
DisplayName: NewTestId(),
Name: NewTestId(),
Description: NewTestId(),
Scope: model.SchemeScopeTeam,
}
s2 := &model.Scheme{
DisplayName: NewTestId(),
Name: NewTestId(),
Description: NewTestId(),
Scope: model.SchemeScopeTeam,
}
s1, err := ss.Scheme().Save(s1)
require.NoError(t, err)
s2, err = ss.Scheme().Save(s2)
require.NoError(t, err)
// Create and save some teams.
t1 := &model.Team{
Name: NewTestId(),
DisplayName: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &s1.Id,
}
t2 := &model.Team{
Name: NewTestId(),
DisplayName: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &s1.Id,
}
t3 := &model.Team{
Name: NewTestId(),
DisplayName: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
_, err = ss.Team().Save(t1)
require.NoError(t, err)
_, err = ss.Team().Save(t2)
require.NoError(t, err)
_, err = ss.Team().Save(t3)
require.NoError(t, err)
// Get the teams by a valid Scheme ID.
d, err := ss.Team().GetTeamsByScheme(s1.Id, 0, 100)
assert.NoError(t, err)
assert.Len(t, d, 2)
// Get the teams by a valid Scheme ID where there aren't any matching Teams.
d, err = ss.Team().GetTeamsByScheme(s2.Id, 0, 100)
assert.NoError(t, err)
assert.Empty(t, d)
// Get the teams by an invalid Scheme ID.
d, err = ss.Team().GetTeamsByScheme(model.NewId(), 0, 100)
assert.NoError(t, err)
assert.Empty(t, d)
}
func testTeamStoreMigrateTeamMembers(t *testing.T, ss store.Store) {
s1 := model.NewId()
t1 := &model.Team{
DisplayName: "Name",
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
InviteId: model.NewId(),
SchemeId: &s1,
}
t1, err := ss.Team().Save(t1)
require.NoError(t, err)
tm1 := &model.TeamMember{
TeamId: t1.Id,
UserId: NewTestId(),
ExplicitRoles: "team_admin team_user",
}
tm2 := &model.TeamMember{
TeamId: t1.Id,
UserId: NewTestId(),
ExplicitRoles: "team_user",
}
tm3 := &model.TeamMember{
TeamId: t1.Id,
UserId: NewTestId(),
ExplicitRoles: "something_else",
}
memberships, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{tm1, tm2, tm3}, -1)
require.NoError(t, nErr)
require.Len(t, memberships, 3)
tm1 = memberships[0]
tm2 = memberships[1]
tm3 = memberships[2]
lastDoneTeamId := strings.Repeat("0", 26)
lastDoneUserId := strings.Repeat("0", 26)
for {
res, e := ss.Team().MigrateTeamMembers(lastDoneTeamId, lastDoneUserId)
if assert.NoError(t, e) {
if res == nil {
break
}
lastDoneTeamId = res["TeamId"]
lastDoneUserId = res["UserId"]
}
}
tm1b, err := ss.Team().GetMember(context.Background(), tm1.TeamId, tm1.UserId)
assert.NoError(t, err)
assert.Equal(t, "", tm1b.ExplicitRoles)
assert.True(t, tm1b.SchemeUser)
assert.True(t, tm1b.SchemeAdmin)
tm2b, err := ss.Team().GetMember(context.Background(), tm2.TeamId, tm2.UserId)
assert.NoError(t, err)
assert.Equal(t, "", tm2b.ExplicitRoles)
assert.True(t, tm2b.SchemeUser)
assert.False(t, tm2b.SchemeAdmin)
tm3b, err := ss.Team().GetMember(context.Background(), tm3.TeamId, tm3.UserId)
assert.NoError(t, err)
assert.Equal(t, "something_else", tm3b.ExplicitRoles)
assert.False(t, tm3b.SchemeUser)
assert.False(t, tm3b.SchemeAdmin)
}
func testResetAllTeamSchemes(t *testing.T, ss store.Store) {
s1 := &model.Scheme{
Name: NewTestId(),
DisplayName: NewTestId(),
Description: NewTestId(),
Scope: model.SchemeScopeTeam,
}
s1, err := ss.Scheme().Save(s1)
require.NoError(t, err)
t1 := &model.Team{
Name: NewTestId(),
DisplayName: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &s1.Id,
}
t2 := &model.Team{
Name: NewTestId(),
DisplayName: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &s1.Id,
}
t1, err = ss.Team().Save(t1)
require.NoError(t, err)
t2, err = ss.Team().Save(t2)
require.NoError(t, err)
assert.Equal(t, s1.Id, *t1.SchemeId)
assert.Equal(t, s1.Id, *t2.SchemeId)
res := ss.Team().ResetAllTeamSchemes()
assert.NoError(t, res)
t1, err = ss.Team().Get(t1.Id)
require.NoError(t, err)
t2, err = ss.Team().Get(t2.Id)
require.NoError(t, err)
assert.Equal(t, "", *t1.SchemeId)
assert.Equal(t, "", *t2.SchemeId)
}
func testTeamStoreClearAllCustomRoleAssignments(t *testing.T, ss store.Store) {
m1 := &model.TeamMember{
TeamId: model.NewId(),
UserId: model.NewId(),
ExplicitRoles: "team_post_all_public team_user team_admin",
}
m2 := &model.TeamMember{
TeamId: model.NewId(),
UserId: model.NewId(),
ExplicitRoles: "team_user custom_role team_admin another_custom_role",
}
m3 := &model.TeamMember{
TeamId: model.NewId(),
UserId: model.NewId(),
ExplicitRoles: "team_user",
}
m4 := &model.TeamMember{
TeamId: model.NewId(),
UserId: model.NewId(),
ExplicitRoles: "custom_only",
}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3, m4}, -1)
require.NoError(t, nErr)
require.NoError(t, (ss.Team().ClearAllCustomRoleAssignments()))
r1, err := ss.Team().GetMember(context.Background(), m1.TeamId, m1.UserId)
require.NoError(t, err)
assert.Equal(t, m1.ExplicitRoles, r1.Roles)
r2, err := ss.Team().GetMember(context.Background(), m2.TeamId, m2.UserId)
require.NoError(t, err)
assert.Equal(t, "team_user team_admin", r2.Roles)
r3, err := ss.Team().GetMember(context.Background(), m3.TeamId, m3.UserId)
require.NoError(t, err)
assert.Equal(t, m3.ExplicitRoles, r3.Roles)
r4, err := ss.Team().GetMember(context.Background(), m4.TeamId, m4.UserId)
require.NoError(t, err)
assert.Equal(t, "", r4.Roles)
}
func testTeamStoreAnalyticsGetTeamCountForScheme(t *testing.T, ss store.Store) {
s1 := &model.Scheme{
DisplayName: NewTestId(),
Name: NewTestId(),
Description: NewTestId(),
Scope: model.SchemeScopeTeam,
}
s1, err := ss.Scheme().Save(s1)
require.NoError(t, err)
count1, err := ss.Team().AnalyticsGetTeamCountForScheme(s1.Id)
assert.NoError(t, err)
assert.Equal(t, int64(0), count1)
t1 := &model.Team{
Name: NewTestId(),
DisplayName: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &s1.Id,
}
_, err = ss.Team().Save(t1)
require.NoError(t, err)
count2, err := ss.Team().AnalyticsGetTeamCountForScheme(s1.Id)
assert.NoError(t, err)
assert.Equal(t, int64(1), count2)
t2 := &model.Team{
Name: NewTestId(),
DisplayName: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &s1.Id,
}
_, err = ss.Team().Save(t2)
require.NoError(t, err)
count3, err := ss.Team().AnalyticsGetTeamCountForScheme(s1.Id)
assert.NoError(t, err)
assert.Equal(t, int64(2), count3)
t3 := &model.Team{
Name: NewTestId(),
DisplayName: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
}
_, err = ss.Team().Save(t3)
require.NoError(t, err)
count4, err := ss.Team().AnalyticsGetTeamCountForScheme(s1.Id)
assert.NoError(t, err)
assert.Equal(t, int64(2), count4)
t4 := &model.Team{
Name: NewTestId(),
DisplayName: NewTestId(),
Email: MakeEmail(),
Type: model.TeamOpen,
SchemeId: &s1.Id,
DeleteAt: model.GetMillis(),
}
_, err = ss.Team().Save(t4)
require.NoError(t, err)
count5, err := ss.Team().AnalyticsGetTeamCountForScheme(s1.Id)
assert.NoError(t, err)
assert.Equal(t, int64(2), count5)
}
func testTeamStoreGetAllForExportAfter(t *testing.T, ss store.Store) {
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
d1, err := ss.Team().GetAllForExportAfter(10000, strings.Repeat("0", 26))
assert.NoError(t, err)
found := false
for _, team := range d1 {
if team.Id == t1.Id {
found = true
assert.Equal(t, t1.Id, team.Id)
assert.Nil(t, team.SchemeId)
assert.Equal(t, t1.Name, team.Name)
}
}
assert.True(t, found)
}
func testTeamStoreGetTeamMembersForExport(t *testing.T, ss store.Store) {
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
u1 := model.User{}
u1.Email = MakeEmail()
u1.Nickname = NewTestId()
_, err = ss.User().Save(&u1)
require.NoError(t, err)
u2 := model.User{}
u2.Email = MakeEmail()
u2.Nickname = NewTestId()
_, err = ss.User().Save(&u2)
require.NoError(t, err)
m1 := &model.TeamMember{TeamId: t1.Id, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: t1.Id, UserId: u2.Id}
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2}, -1)
require.NoError(t, nErr)
d1, err := ss.Team().GetTeamMembersForExport(u1.Id)
assert.NoError(t, err)
assert.Len(t, d1, 1)
tmfe1 := d1[0]
assert.Equal(t, t1.Id, tmfe1.TeamId)
assert.Equal(t, u1.Id, tmfe1.UserId)
assert.Equal(t, t1.Name, tmfe1.TeamName)
}
func testGroupSyncedTeamCount(t *testing.T, ss store.Store) {
team1, err := ss.Team().Save(&model.Team{
DisplayName: NewTestId(),
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamInvite,
GroupConstrained: model.NewBool(true),
})
require.NoError(t, err)
require.True(t, team1.IsGroupConstrained())
defer ss.Team().PermanentDelete(team1.Id)
team2, err := ss.Team().Save(&model.Team{
DisplayName: NewTestId(),
Name: "zz" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamInvite,
})
require.NoError(t, err)
require.False(t, team2.IsGroupConstrained())
defer ss.Team().PermanentDelete(team2.Id)
count, err := ss.Team().GroupSyncedTeamCount()
require.NoError(t, err)
require.GreaterOrEqual(t, count, int64(1))
team2.GroupConstrained = model.NewBool(true)
team2, err = ss.Team().Update(team2)
require.NoError(t, err)
require.True(t, team2.IsGroupConstrained())
countAfter, err := ss.Team().GroupSyncedTeamCount()
require.NoError(t, err)
require.GreaterOrEqual(t, countAfter, count+1)
}
func testGetNewTeamMembersSince(t *testing.T, ss store.Store) {
team, err := ss.Team().Save(&model.Team{
DisplayName: NewTestId(),
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamInvite,
GroupConstrained: model.NewBool(true),
})
require.NoError(t, err)
_, _, err = ss.Team().GetNewTeamMembersSince(team.Id, 0, 0, 1000)
require.NoError(t, err)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestTermsOfServiceStore(t *testing.T, ss store.Store) {
t.Run("TestSaveTermsOfService", func(t *testing.T) { testSaveTermsOfService(t, ss) })
t.Run("TestGetLatestTermsOfService", func(t *testing.T) { testGetLatestTermsOfService(t, ss) })
t.Run("TestGetTermsOfService", func(t *testing.T) { testGetTermsOfService(t, ss) })
}
func cleanUpTOS(ss store.Store) {
// Clearing out the table before starting the test.
// Otherwise the row inserted by the previous Save call from testSaveTermsOfService
// gets picked up.
// We call DropAllTables but we actually need to delete only TermsOfService.
// However, there is no straightforward way to just clear that table without introducing
// new methods. So we use the hammer.
ss.DropAllTables()
}
func testSaveTermsOfService(t *testing.T, ss store.Store) {
t.Cleanup(func() { cleanUpTOS(ss) })
u1 := model.User{}
u1.Username = model.NewId()
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(&u1)
require.NoError(t, err)
termsOfService := &model.TermsOfService{Text: "terms of service", UserId: u1.Id}
savedTermsOfService, err := ss.TermsOfService().Save(termsOfService)
require.NoError(t, err)
require.Len(t, savedTermsOfService.Id, 26, "Id should have been populated")
require.NotEqual(t, savedTermsOfService.CreateAt, 0, "Create at should have been populated")
}
func testGetLatestTermsOfService(t *testing.T, ss store.Store) {
t.Cleanup(func() { cleanUpTOS(ss) })
u1 := model.User{}
u1.Username = model.NewId()
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(&u1)
require.NoError(t, err)
termsOfService := &model.TermsOfService{Text: "terms of service 2", UserId: u1.Id}
_, err = ss.TermsOfService().Save(termsOfService)
require.NoError(t, err)
fetchedTermsOfService, err := ss.TermsOfService().GetLatest(true)
require.NoError(t, err)
assert.Equal(t, termsOfService.Text, fetchedTermsOfService.Text)
assert.Equal(t, termsOfService.UserId, fetchedTermsOfService.UserId)
}
func testGetTermsOfService(t *testing.T, ss store.Store) {
t.Cleanup(func() { cleanUpTOS(ss) })
u1 := model.User{}
u1.Username = model.NewId()
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(&u1)
require.NoError(t, err)
termsOfService := &model.TermsOfService{Text: "terms of service", UserId: u1.Id}
_, err = ss.TermsOfService().Save(termsOfService)
require.NoError(t, err)
r1, err := ss.TermsOfService().Get("an_invalid_id", true)
assert.Error(t, err)
assert.Nil(t, r1)
receivedTermsOfService, err := ss.TermsOfService().Get(termsOfService.Id, true)
assert.NoError(t, err)
assert.Equal(t, "terms of service", receivedTermsOfService.Text)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"sort"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestThreadStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("ThreadStorePopulation", func(t *testing.T) { testThreadStorePopulation(t, ss) })
t.Run("ThreadStorePermanentDeleteBatchForRetentionPolicies", func(t *testing.T) {
testThreadStorePermanentDeleteBatchForRetentionPolicies(t, ss)
})
t.Run("ThreadStorePermanentDeleteBatchThreadMembershipsForRetentionPolicies", func(t *testing.T) {
testThreadStorePermanentDeleteBatchThreadMembershipsForRetentionPolicies(t, ss, s)
})
t.Run("GetTeamsUnreadForUser", func(t *testing.T) { testGetTeamsUnreadForUser(t, ss) })
t.Run("GetVarious", func(t *testing.T) { testVarious(t, ss) })
t.Run("MarkAllAsReadByChannels", func(t *testing.T) { testMarkAllAsReadByChannels(t, ss) })
t.Run("GetTopThreads", func(t *testing.T) { testGetTopThreads(t, ss) })
t.Run("MarkAllAsReadByTeam", func(t *testing.T) { testMarkAllAsReadByTeam(t, ss) })
}
func testThreadStorePopulation(t *testing.T, ss store.Store) {
makeSomePosts := func(urgent bool) []*model.Post {
u1 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
u, err := ss.User().Save(&u1)
require.NoError(t, err)
c, err2 := ss.Channel().Save(&model.Channel{
DisplayName: model.NewId(),
Type: model.ChannelTypeOpen,
Name: model.NewId(),
}, 999)
require.NoError(t, err2)
_, err44 := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
MsgCount: 0,
})
require.NoError(t, err44)
o := model.Post{}
o.ChannelId = c.Id
o.UserId = u.Id
o.Message = NewTestId()
if urgent {
o.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(false),
},
}
}
otmp, err3 := ss.Post().Save(&o)
require.NoError(t, err3)
o2 := model.Post{}
o2.ChannelId = c.Id
o2.UserId = model.NewId()
o2.RootId = otmp.Id
o2.Message = NewTestId()
o3 := model.Post{}
o3.ChannelId = c.Id
o3.UserId = u.Id
o3.RootId = otmp.Id
o3.Message = NewTestId()
o4 := model.Post{}
o4.ChannelId = c.Id
o4.UserId = model.NewId()
o4.Message = NewTestId()
newPosts, errIdx, err3 := ss.Post().SaveMultiple([]*model.Post{&o2, &o3, &o4})
opts := model.GetPostsOptions{
SkipFetchThreads: true,
}
olist, _ := ss.Post().Get(context.Background(), otmp.Id, opts, "", map[string]bool{})
o1 := olist.Posts[olist.Order[0]]
newPosts = append([]*model.Post{o1}, newPosts...)
require.NoError(t, err3, "couldn't save item")
require.Equal(t, -1, errIdx)
require.Len(t, newPosts, 4)
require.Equal(t, int64(2), newPosts[0].ReplyCount)
require.Equal(t, int64(2), newPosts[1].ReplyCount)
require.Equal(t, int64(2), newPosts[2].ReplyCount)
require.Equal(t, int64(0), newPosts[3].ReplyCount)
return newPosts
}
t.Run("Save replies creates a thread", func(t *testing.T) {
newPosts := makeSomePosts(false)
thread, err := ss.Thread().Get(newPosts[0].Id)
require.NoError(t, err, "couldn't get thread")
require.NotNil(t, thread)
require.Equal(t, int64(2), thread.ReplyCount)
require.ElementsMatch(t, model.StringArray{newPosts[0].UserId, newPosts[1].UserId}, thread.Participants)
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o5 := model.Post{}
o5.ChannelId = channel.Id
o5.UserId = model.NewId()
o5.RootId = newPosts[0].Id
o5.Message = NewTestId()
_, _, err = ss.Post().SaveMultiple([]*model.Post{&o5})
require.NoError(t, err, "couldn't save item")
thread, err = ss.Thread().Get(newPosts[0].Id)
require.NoError(t, err, "couldn't get thread")
require.NotNil(t, thread)
require.Equal(t, int64(3), thread.ReplyCount)
require.ElementsMatch(t, model.StringArray{newPosts[0].UserId, newPosts[1].UserId, o5.UserId}, thread.Participants)
})
t.Run("Delete a reply updates count on a thread", func(t *testing.T) {
newPosts := makeSomePosts(false)
thread, err := ss.Thread().Get(newPosts[0].Id)
require.NoError(t, err, "couldn't get thread")
require.NotNil(t, thread)
require.Equal(t, int64(2), thread.ReplyCount)
require.ElementsMatch(t, model.StringArray{newPosts[0].UserId, newPosts[1].UserId}, thread.Participants)
err = ss.Post().Delete(newPosts[1].Id, 1234, model.NewId())
require.NoError(t, err, "couldn't delete post")
thread, err = ss.Thread().Get(newPosts[0].Id)
require.NoError(t, err, "couldn't get thread")
require.NotNil(t, thread)
require.Equal(t, int64(1), thread.ReplyCount)
require.ElementsMatch(t, model.StringArray{newPosts[0].UserId}, thread.Participants)
})
t.Run("Update reply should update the UpdateAt of the thread", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
rootPost := model.Post{}
rootPost.RootId = model.NewId()
rootPost.ChannelId = channel.Id
rootPost.UserId = model.NewId()
rootPost.Message = NewTestId()
replyPost := model.Post{}
replyPost.ChannelId = rootPost.ChannelId
replyPost.UserId = model.NewId()
replyPost.Message = NewTestId()
replyPost.RootId = rootPost.RootId
newPosts, _, err := ss.Post().SaveMultiple([]*model.Post{&rootPost, &replyPost})
require.NoError(t, err)
thread1, err := ss.Thread().Get(newPosts[0].RootId)
require.NoError(t, err)
rrootPost, err := ss.Post().GetSingle(rootPost.Id, false)
require.NoError(t, err)
require.Equal(t, rrootPost.UpdateAt, rootPost.UpdateAt)
replyPost2 := model.Post{}
replyPost2.ChannelId = rootPost.ChannelId
replyPost2.UserId = model.NewId()
replyPost2.Message = NewTestId()
replyPost2.RootId = rootPost.Id
replyPost3 := model.Post{}
replyPost3.ChannelId = rootPost.ChannelId
replyPost3.UserId = model.NewId()
replyPost3.Message = NewTestId()
replyPost3.RootId = rootPost.Id
_, _, err = ss.Post().SaveMultiple([]*model.Post{&replyPost2, &replyPost3})
require.NoError(t, err)
rrootPost2, err := ss.Post().GetSingle(rootPost.Id, false)
require.NoError(t, err)
require.Greater(t, rrootPost2.UpdateAt, rrootPost.UpdateAt)
thread2, err := ss.Thread().Get(rootPost.Id)
require.NoError(t, err)
require.Greater(t, thread2.LastReplyAt, thread1.LastReplyAt)
})
t.Run("Deleting reply should update the thread", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := model.Post{}
o1.ChannelId = channel.Id
o1.UserId = model.NewId()
o1.Message = NewTestId()
rootPost, err := ss.Post().Save(&o1)
require.NoError(t, err)
o2 := model.Post{}
o2.RootId = rootPost.Id
o2.ChannelId = rootPost.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestId()
replyPost, err := ss.Post().Save(&o2)
require.NoError(t, err)
o3 := model.Post{}
o3.RootId = rootPost.Id
o3.ChannelId = rootPost.ChannelId
o3.UserId = o2.UserId
o3.Message = NewTestId()
replyPost2, err := ss.Post().Save(&o3)
require.NoError(t, err)
o4 := model.Post{}
o4.RootId = rootPost.Id
o4.ChannelId = rootPost.ChannelId
o4.UserId = model.NewId()
o4.Message = NewTestId()
replyPost3, err := ss.Post().Save(&o4)
require.NoError(t, err)
thread, err := ss.Thread().Get(rootPost.Id)
require.NoError(t, err)
require.EqualValues(t, thread.ReplyCount, 3)
require.EqualValues(t, thread.Participants, model.StringArray{replyPost.UserId, replyPost3.UserId})
err = ss.Post().Delete(replyPost2.Id, 123, model.NewId())
require.NoError(t, err)
thread, err = ss.Thread().Get(rootPost.Id)
require.NoError(t, err)
require.EqualValues(t, thread.ReplyCount, 2)
require.EqualValues(t, thread.Participants, model.StringArray{replyPost.UserId, replyPost3.UserId})
err = ss.Post().Delete(replyPost.Id, 123, model.NewId())
require.NoError(t, err)
thread, err = ss.Thread().Get(rootPost.Id)
require.NoError(t, err)
require.EqualValues(t, thread.ReplyCount, 1)
require.EqualValues(t, thread.Participants, model.StringArray{replyPost3.UserId})
})
t.Run("Deleting root post should delete the thread", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
rootPost := model.Post{}
rootPost.ChannelId = channel.Id
rootPost.UserId = model.NewId()
rootPost.Message = NewTestId()
newPosts1, _, err := ss.Post().SaveMultiple([]*model.Post{&rootPost})
require.NoError(t, err)
replyPost := model.Post{}
replyPost.ChannelId = rootPost.ChannelId
replyPost.UserId = model.NewId()
replyPost.Message = NewTestId()
replyPost.RootId = newPosts1[0].Id
_, _, err = ss.Post().SaveMultiple([]*model.Post{&replyPost})
require.NoError(t, err)
thread1, err := ss.Thread().Get(newPosts1[0].Id)
require.NoError(t, err)
require.EqualValues(t, thread1.ReplyCount, 1)
require.Len(t, thread1.Participants, 1)
err = ss.Post().PermanentDeleteByUser(rootPost.UserId)
require.NoError(t, err)
thread2, _ := ss.Thread().Get(rootPost.Id)
require.Nil(t, thread2)
})
t.Run("Thread membership 'viewed' timestamp is updated properly", func(t *testing.T) {
newPosts := makeSomePosts(false)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: true,
}
tm, e := ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, opts)
require.NoError(t, e)
require.Equal(t, int64(0), tm.LastViewed)
// No update since array has same elements.
th, e := ss.Thread().Get(newPosts[0].Id)
require.NoError(t, e)
assert.ElementsMatch(t, model.StringArray{newPosts[0].UserId, newPosts[1].UserId}, th.Participants)
opts.UpdateViewedTimestamp = true
_, e = ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, opts)
require.NoError(t, e)
m2, err2 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
require.NoError(t, err2)
require.Greater(t, m2.LastViewed, int64(0))
// Adding a new participant
_, e = ss.Thread().MaintainMembership("newuser", newPosts[0].Id, opts)
require.NoError(t, e)
th, e = ss.Thread().Get(newPosts[0].Id)
require.NoError(t, e)
assert.ElementsMatch(t, model.StringArray{newPosts[0].UserId, newPosts[1].UserId, "newuser"}, th.Participants)
})
t.Run("Thread membership 'viewed' timestamp is updated properly for new membership", func(t *testing.T) {
newPosts := makeSomePosts(false)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: false,
UpdateViewedTimestamp: true,
UpdateParticipants: false,
}
tm, e := ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, opts)
require.NoError(t, e)
require.NotEqual(t, int64(0), tm.LastViewed)
})
t.Run("Updating post does not make thread unread", func(t *testing.T) {
newPosts := makeSomePosts(false)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
m, err := ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, opts)
require.NoError(t, err)
th, err := ss.Thread().GetThreadForUser(m, false, false)
require.NoError(t, err)
require.Equal(t, int64(2), th.UnreadReplies)
m.LastViewed = newPosts[2].UpdateAt + 1
_, err = ss.Thread().UpdateMembership(m)
require.NoError(t, err)
th, err = ss.Thread().GetThreadForUser(m, false, false)
require.NoError(t, err)
require.Equal(t, int64(0), th.UnreadReplies)
editedPost := newPosts[2].Clone()
editedPost.Message = "This is an edited post"
_, err = ss.Post().Update(editedPost, newPosts[2])
require.NoError(t, err)
th, err = ss.Thread().GetThreadForUser(m, false, false)
require.NoError(t, err)
require.Equal(t, int64(0), th.UnreadReplies)
})
t.Run("Empty participantID should not appear in thread response", func(t *testing.T) {
newPosts := makeSomePosts(false)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: true,
}
m, err := ss.Thread().MaintainMembership("", newPosts[0].Id, opts)
require.NoError(t, err)
m.UserId = newPosts[0].UserId
th, err := ss.Thread().GetThreadForUser(m, true, false)
require.NoError(t, err)
for _, user := range th.Participants {
require.NotNil(t, user)
}
})
t.Run("Get unread reply counts for thread", func(t *testing.T) {
t.Skip("MM-41797")
newPosts := makeSomePosts(false)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: true,
UpdateParticipants: false,
}
_, e := ss.Thread().MaintainMembership(newPosts[0].UserId, newPosts[0].Id, opts)
require.NoError(t, e)
m, err1 := ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
require.NoError(t, err1)
unreads, err := ss.Thread().GetThreadUnreadReplyCount(m)
require.NoError(t, err)
require.Equal(t, int64(0), unreads)
err = ss.Thread().MarkAsRead(newPosts[0].UserId, newPosts[0].Id, newPosts[0].CreateAt)
require.NoError(t, err)
m, err = ss.Thread().GetMembershipForUser(newPosts[0].UserId, newPosts[0].Id)
require.NoError(t, err)
unreads, err = ss.Thread().GetThreadUnreadReplyCount(m)
require.NoError(t, err)
require.Equal(t, int64(2), unreads)
})
testCases := []bool{true, false}
for _, isUrgent := range testCases {
t.Run("Return is urgent for user thread/s", func(t *testing.T) {
newPosts := makeSomePosts(isUrgent)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: true,
UpdateParticipants: false,
}
userID := newPosts[0].UserId
_, e := ss.Thread().MaintainMembership(userID, newPosts[0].Id, opts)
require.NoError(t, e)
m, e := ss.Thread().GetMembershipForUser(userID, newPosts[0].Id)
require.NoError(t, e)
th, e := ss.Thread().GetThreadForUser(m, false, true)
require.NoError(t, e)
require.Equal(t, isUrgent, th.IsUrgent)
threads, e := ss.Thread().GetThreadsForUser(userID, "", model.GetUserThreadsOpts{IncludeIsUrgent: true})
require.NoError(t, e)
require.Equal(t, isUrgent, threads[0].IsUrgent)
})
}
}
func threadStoreCreateReply(t *testing.T, ss store.Store, channelID, postID, userID string, createAt int64) *model.Post {
t.Helper()
reply, err := ss.Post().Save(&model.Post{
ChannelId: channelID,
UserId: userID,
CreateAt: createAt,
RootId: postID,
Message: model.NewRandomString(10),
})
require.NoError(t, err)
return reply
}
func testThreadStorePermanentDeleteBatchForRetentionPolicies(t *testing.T, ss store.Store) {
const limit = 1000
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel.Id, post.Id, post.UserId, 2000)
thread, err := ss.Thread().Get(post.Id)
require.NoError(t, err)
channelPolicy, err := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewInt64(30),
},
ChannelIDs: []string{channel.Id},
})
require.NoError(t, err)
nowMillis := thread.LastReplyAt + *channelPolicy.PostDurationDays*model.DayInMilliseconds + 1
_, _, err = ss.Thread().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
thread, err = ss.Thread().Get(post.Id)
assert.NoError(t, err)
assert.Nil(t, thread, "thread should have been deleted by channel policy")
// create a new thread
threadStoreCreateReply(t, ss, channel.Id, post.Id, post.UserId, 2000)
thread, err = ss.Thread().Get(post.Id)
require.NoError(t, err)
// Create a team policy which is stricter than the channel policy
teamPolicy, err := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewInt64(20),
},
TeamIDs: []string{team.Id},
})
require.NoError(t, err)
nowMillis = thread.LastReplyAt + *teamPolicy.PostDurationDays*model.DayInMilliseconds + 1
_, _, err = ss.Thread().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
_, err = ss.Thread().Get(post.Id)
require.NoError(t, err, "channel policy should have overridden team policy")
// Delete channel policy and re-run team policy
err = ss.RetentionPolicy().Delete(channelPolicy.ID)
require.NoError(t, err)
_, _, err = ss.Thread().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
thread, err = ss.Thread().Get(post.Id)
assert.NoError(t, err)
assert.Nil(t, thread, "thread should have been deleted by team policy")
}
func testThreadStorePermanentDeleteBatchThreadMembershipsForRetentionPolicies(t *testing.T, ss store.Store, s SqlStore) {
const limit = 1000
userID := model.NewId()
createThreadMembership := func(userID, postID string) *model.ThreadMembership {
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
_, err := ss.Thread().MaintainMembership(userID, postID, opts)
require.NoError(t, err)
threadMembership, err := ss.Thread().GetMembershipForUser(userID, postID)
require.NoError(t, err)
return threadMembership
}
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel.Id, post.Id, post.UserId, 2000)
threadMembership := createThreadMembership(userID, post.Id)
channelPolicy, err := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewInt64(30),
},
ChannelIDs: []string{channel.Id},
})
require.NoError(t, err)
nowMillis := threadMembership.LastUpdated + *channelPolicy.PostDurationDays*model.DayInMilliseconds + 1
_, _, err = ss.Thread().PermanentDeleteBatchThreadMembershipsForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
_, err = ss.Thread().GetMembershipForUser(userID, post.Id)
require.Error(t, err, "thread membership should have been deleted by channel policy")
// create a new thread membership
threadMembership = createThreadMembership(userID, post.Id)
// Create a team policy which is stricter than the channel policy
teamPolicy, err := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewInt64(20),
},
TeamIDs: []string{team.Id},
})
require.NoError(t, err)
nowMillis = threadMembership.LastUpdated + *teamPolicy.PostDurationDays*model.DayInMilliseconds + 1
_, _, err = ss.Thread().PermanentDeleteBatchThreadMembershipsForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
_, err = ss.Thread().GetMembershipForUser(userID, post.Id)
require.NoError(t, err, "channel policy should have overridden team policy")
// Delete channel policy and re-run team policy
err = ss.RetentionPolicy().Delete(channelPolicy.ID)
require.NoError(t, err)
_, _, err = ss.Thread().PermanentDeleteBatchThreadMembershipsForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{})
require.NoError(t, err)
_, err = ss.Thread().GetMembershipForUser(userID, post.Id)
require.Error(t, err, "thread membership should have been deleted by team policy")
// create a new thread membership
createThreadMembership(userID, post.Id)
// Delete team policy and thread
err = ss.RetentionPolicy().Delete(teamPolicy.ID)
require.NoError(t, err)
_, err = s.GetMasterX().Exec("DELETE FROM Threads WHERE PostId='" + post.Id + "'")
require.NoError(t, err)
deleted, err := ss.Thread().DeleteOrphanedRows(1000)
require.NoError(t, err)
require.NotZero(t, deleted)
_, err = ss.Thread().GetMembershipForUser(userID, post.Id)
require.Error(t, err, "thread membership should have been deleted because thread no longer exists")
}
func testGetTeamsUnreadForUser(t *testing.T, ss store.Store) {
userID := model.NewId()
createThreadMembership := func(userID, postID string) {
t.Helper()
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
_, err := ss.Thread().MaintainMembership(userID, postID, opts)
require.NoError(t, err)
}
team1, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: team1.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post, err := ss.Post().Save(&model.Post{
ChannelId: channel1.Id,
UserId: userID,
Message: model.NewRandomString(10),
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel1.Id, post.Id, post.UserId, model.GetMillis())
createThreadMembership(userID, post.Id)
teamsUnread, err := ss.Thread().GetTeamsUnreadForUser(userID, []string{team1.Id}, true)
require.NoError(t, err)
assert.Len(t, teamsUnread, 1)
assert.Equal(t, int64(1), teamsUnread[team1.Id].ThreadCount)
post, err = ss.Post().Save(&model.Post{
ChannelId: channel1.Id,
UserId: userID,
Message: model.NewRandomString(10),
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel1.Id, post.Id, post.UserId, model.GetMillis())
createThreadMembership(userID, post.Id)
teamsUnread, err = ss.Thread().GetTeamsUnreadForUser(userID, []string{team1.Id}, true)
require.NoError(t, err)
assert.Len(t, teamsUnread, 1)
assert.Equal(t, int64(2), teamsUnread[team1.Id].ThreadCount)
team2, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: team2.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: userID,
Message: model.NewRandomString(10),
Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(false),
},
},
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel2.Id, post2.Id, post2.UserId, model.GetMillis())
createThreadMembership(userID, post2.Id)
teamsUnread, err = ss.Thread().GetTeamsUnreadForUser(userID, []string{team1.Id, team2.Id}, true)
require.NoError(t, err)
assert.Len(t, teamsUnread, 2)
assert.Equal(t, int64(2), teamsUnread[team1.Id].ThreadCount)
assert.Equal(t, int64(1), teamsUnread[team2.Id].ThreadCount)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: true,
}
_, err = ss.Thread().MaintainMembership(userID, post2.Id, opts)
require.NoError(t, err)
teamsUnread, err = ss.Thread().GetTeamsUnreadForUser(userID, []string{team2.Id}, true)
require.NoError(t, err)
assert.Len(t, teamsUnread, 1)
assert.Equal(t, int64(1), teamsUnread[team2.Id].ThreadCount)
assert.Equal(t, int64(1), teamsUnread[team2.Id].ThreadMentionCount)
assert.Equal(t, int64(1), teamsUnread[team2.Id].ThreadUrgentMentionCount)
}
type byPostId []*model.Post
func (a byPostId) Len() int { return len(a) }
func (a byPostId) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byPostId) Less(i, j int) bool { return a[i].Id < a[j].Id }
func testVarious(t *testing.T, ss store.Store) {
createThreadMembership := func(userID, postID string, isMention bool) {
t.Helper()
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: isMention,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
_, err := ss.Thread().MaintainMembership(userID, postID, opts)
require.NoError(t, err)
}
viewThread := func(userID, postID string) {
t.Helper()
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: true,
UpdateParticipants: false,
}
_, err := ss.Thread().MaintainMembership(userID, postID, opts)
require.NoError(t, err)
}
user1, err := ss.User().Save(&model.User{
Username: "user1" + model.NewId(),
Email: MakeEmail(),
})
require.NoError(t, err)
user2, err := ss.User().Save(&model.User{
Username: "user2" + model.NewId(),
Email: MakeEmail(),
})
require.NoError(t, err)
user1ID := user1.Id
user2ID := user2.Id
team1, err := ss.Team().Save(&model.Team{
DisplayName: "Team1",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
team2, err := ss.Team().Save(&model.Team{
DisplayName: "Team2",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
team1channel1, err := ss.Channel().Save(&model.Channel{
TeamId: team1.Id,
DisplayName: "Channel1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
team2channel1, err := ss.Channel().Save(&model.Channel{
TeamId: team2.Id,
DisplayName: "Channel2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
dm1, err := ss.Channel().CreateDirectChannel(&model.User{Id: user1ID}, &model.User{Id: user2ID})
require.NoError(t, err)
gm1, err := ss.Channel().Save(&model.Channel{
DisplayName: "GM",
Name: "gm" + model.NewId(),
Type: model.ChannelTypeGroup,
}, -1)
require.NoError(t, err)
team1channel1post1, err := ss.Post().Save(&model.Post{
ChannelId: team1channel1.Id,
UserId: user1ID,
Message: model.NewRandomString(10),
})
require.NoError(t, err)
team1channel1post2, err := ss.Post().Save(&model.Post{
ChannelId: team1channel1.Id,
UserId: user1ID,
Message: model.NewRandomString(10),
})
require.NoError(t, err)
team1channel1post3, err := ss.Post().Save(&model.Post{
ChannelId: team1channel1.Id,
UserId: user1ID,
Message: model.NewRandomString(10),
Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(model.PostPriorityUrgent),
RequestedAck: model.NewBool(false),
PersistentNotifications: model.NewBool(false),
},
},
})
require.NoError(t, err)
team2channel1post1, err := ss.Post().Save(&model.Post{
ChannelId: team2channel1.Id,
UserId: user1ID,
Message: model.NewRandomString(10),
})
require.NoError(t, err)
team2channel1post2deleted, err := ss.Post().Save(&model.Post{
ChannelId: team2channel1.Id,
UserId: user1ID,
Message: model.NewRandomString(10),
})
require.NoError(t, err)
dm1post1, err := ss.Post().Save(&model.Post{
ChannelId: dm1.Id,
UserId: user1ID,
Message: model.NewRandomString(10),
})
require.NoError(t, err)
gm1post1, err := ss.Post().Save(&model.Post{
ChannelId: gm1.Id,
UserId: user1ID,
Message: model.NewRandomString(10),
})
require.NoError(t, err)
postNames := map[string]string{
team1channel1post1.Id: "team1channel1post1",
team1channel1post2.Id: "team1channel1post2",
team1channel1post3.Id: "team1channel1post3",
team2channel1post1.Id: "team2channel1post1",
team2channel1post2deleted.Id: "team2channel1post2deleted",
dm1post1.Id: "dm1post1",
gm1post1.Id: "gm1post1",
}
threadStoreCreateReply(t, ss, team1channel1.Id, team1channel1post1.Id, user2ID, model.GetMillis())
threadStoreCreateReply(t, ss, team1channel1.Id, team1channel1post2.Id, user2ID, model.GetMillis())
threadStoreCreateReply(t, ss, team1channel1.Id, team1channel1post3.Id, user2ID, model.GetMillis())
threadStoreCreateReply(t, ss, team2channel1.Id, team2channel1post1.Id, user2ID, model.GetMillis())
threadStoreCreateReply(t, ss, team2channel1.Id, team2channel1post2deleted.Id, user2ID, model.GetMillis())
threadStoreCreateReply(t, ss, dm1.Id, dm1post1.Id, user2ID, model.GetMillis())
threadStoreCreateReply(t, ss, gm1.Id, gm1post1.Id, user2ID, model.GetMillis())
// Create thread memberships, with simulated unread mentions.
createThreadMembership(user1ID, team1channel1post1.Id, false)
createThreadMembership(user1ID, team1channel1post2.Id, false)
createThreadMembership(user1ID, team1channel1post3.Id, true)
createThreadMembership(user1ID, team2channel1post1.Id, false)
createThreadMembership(user1ID, team2channel1post2deleted.Id, false)
createThreadMembership(user1ID, dm1post1.Id, false)
createThreadMembership(user1ID, gm1post1.Id, true)
// Have user1 view a subset of the threads
viewThread(user1ID, team1channel1post1.Id)
viewThread(user2ID, team1channel1post2.Id)
viewThread(user1ID, team2channel1post1.Id)
viewThread(user1ID, dm1post1.Id)
// Add reply to a viewed thread to confirm it's unread again.
time.Sleep(2 * time.Millisecond)
threadStoreCreateReply(t, ss, team1channel1.Id, team1channel1post2.Id, user2ID, model.GetMillis())
// Actually make team2channel1post2deleted deleted
err = ss.Post().Delete(team2channel1post2deleted.Id, model.GetMillis(), user1ID)
require.NoError(t, err)
// Re-fetch posts to ensure metadata up-to-date
allPosts := []*model.Post{
team1channel1post1,
team1channel1post2,
team1channel1post3,
team2channel1post1,
team2channel1post2deleted,
dm1post1,
gm1post1,
}
for i := range allPosts {
updatedPost, err := ss.Post().GetSingle(allPosts[i].Id, true)
require.NoError(t, err)
// Fix some inconsistencies with how the post store returns posts vs. how the
// thread store returns it.
if updatedPost.RemoteId == nil {
updatedPost.RemoteId = new(string)
}
// Also, we don't populate ReplyCount for posts when querying threads, so don't
// assert same.
updatedPost.ReplyCount = 0
updatedPost.ShallowCopy(allPosts[i])
}
t.Run("GetTotalUnreadThreads", func(t *testing.T) {
testCases := []struct {
Description string
UserID string
TeamID string
Options model.GetUserThreadsOpts
ExpectedThreads []*model.Post
}{
{"all teams, user1", user1ID, "", model.GetUserThreadsOpts{}, []*model.Post{
team1channel1post2, team1channel1post3, gm1post1,
}},
{"team1, user1", user1ID, team1.Id, model.GetUserThreadsOpts{}, []*model.Post{
team1channel1post2, team1channel1post3, gm1post1,
}},
{"team1, user1, deleted", user1ID, team1.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{
team1channel1post2, team1channel1post3, gm1post1, // (no deleted threads in team1)
}},
{"team2, user1", user1ID, team2.Id, model.GetUserThreadsOpts{}, []*model.Post{
gm1post1, // (no unread threads in team2)
}},
{"team2, user1, deleted", user1ID, team2.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{
team2channel1post2deleted, gm1post1,
}},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
totalUnreadThreads, err := ss.Thread().GetTotalUnreadThreads(testCase.UserID, testCase.TeamID, testCase.Options)
require.NoError(t, err)
assert.EqualValues(t, int64(len(testCase.ExpectedThreads)), totalUnreadThreads)
})
}
})
t.Run("GetTotalThreads", func(t *testing.T) {
testCases := []struct {
Description string
UserID string
TeamID string
Options model.GetUserThreadsOpts
ExpectedThreads []*model.Post
}{
{"all teams, user1", user1ID, "", model.GetUserThreadsOpts{}, []*model.Post{
team1channel1post1, team1channel1post2, team1channel1post3, team2channel1post1, dm1post1, gm1post1,
}},
{"team1, user1", user1ID, team1.Id, model.GetUserThreadsOpts{}, []*model.Post{
team1channel1post1, team1channel1post2, team1channel1post3, dm1post1, gm1post1,
}},
{"team1, user1, deleted", user1ID, team1.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{
team1channel1post1, team1channel1post2, team1channel1post3, dm1post1, gm1post1, // (no deleted threads in team1)
}},
{"team2, user1", user1ID, team2.Id, model.GetUserThreadsOpts{}, []*model.Post{
team2channel1post1, dm1post1, gm1post1,
}},
{"team2, user1, deleted", user1ID, team2.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{
team2channel1post1, team2channel1post2deleted, dm1post1, gm1post1,
}},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
totalThreads, err := ss.Thread().GetTotalThreads(testCase.UserID, testCase.TeamID, testCase.Options)
require.NoError(t, err)
assert.EqualValues(t, int64(len(testCase.ExpectedThreads)), totalThreads)
})
}
})
t.Run("GetTotalUnreadMentions", func(t *testing.T) {
testCases := []struct {
Description string
UserID string
TeamID string
Options model.GetUserThreadsOpts
ExpectedThreads []*model.Post
}{
{"all teams, user1", user1ID, "", model.GetUserThreadsOpts{}, []*model.Post{
team1channel1post3, gm1post1,
}},
{"team1, user1", user1ID, team1.Id, model.GetUserThreadsOpts{}, []*model.Post{
team1channel1post3, gm1post1,
}},
{"team2, user1", user1ID, team2.Id, model.GetUserThreadsOpts{}, []*model.Post{
gm1post1,
}},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
totalUnreadMentions, err := ss.Thread().GetTotalUnreadMentions(testCase.UserID, testCase.TeamID, testCase.Options)
require.NoError(t, err)
assert.EqualValues(t, int64(len(testCase.ExpectedThreads)), totalUnreadMentions)
})
}
})
t.Run("GetTotalUnreadUrgentMentions", func(t *testing.T) {
testCases := []struct {
Description string
UserID string
TeamID string
Options model.GetUserThreadsOpts
ExpectedThreads []*model.Post
}{
{"all teams, user1", user1ID, "", model.GetUserThreadsOpts{}, []*model.Post{
team1channel1post3,
}},
{"team1, user1", user1ID, team1.Id, model.GetUserThreadsOpts{}, []*model.Post{
team1channel1post3,
}},
{"team2, user1", user1ID, team2.Id, model.GetUserThreadsOpts{}, []*model.Post{}},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
totalUnreadUrgentMentions, err := ss.Thread().GetTotalUnreadUrgentMentions(testCase.UserID, testCase.TeamID, testCase.Options)
require.NoError(t, err)
assert.EqualValues(t, int64(len(testCase.ExpectedThreads)), totalUnreadUrgentMentions)
})
}
})
assertThreadPosts := func(t *testing.T, threads []*model.ThreadResponse, expectedPosts []*model.Post) {
t.Helper()
actualPosts := make([]*model.Post, 0, len(threads))
actualPostNames := make([]string, 0, len(threads))
for _, thread := range threads {
actualPosts = append(actualPosts, thread.Post)
postName, ok := postNames[thread.PostId]
require.True(t, ok, "failed to find actual %s in post names", thread.PostId)
actualPostNames = append(actualPostNames, postName)
}
sort.Strings(actualPostNames)
expectedPostNames := make([]string, 0, len(expectedPosts))
for _, post := range expectedPosts {
postName, ok := postNames[post.Id]
require.True(t, ok, "failed to find expected %s in post names", post.Id)
expectedPostNames = append(expectedPostNames, postName)
}
sort.Strings(expectedPostNames)
assert.Equal(t, expectedPostNames, actualPostNames)
// Check posts themselves
sort.Sort(byPostId(expectedPosts))
sort.Sort(byPostId(actualPosts))
if assert.Len(t, actualPosts, len(expectedPosts)) {
for i := range actualPosts {
assert.Equal(t, expectedPosts[i], actualPosts[i], "mismatch comparing expected post %s with actual post %s", postNames[expectedPosts[i].Id], postNames[actualPosts[i].Id])
}
} else {
assert.Equal(t, expectedPosts, actualPosts)
}
// Check common fields between threads and posts.
for _, thread := range threads {
assert.Equal(t, thread.DeleteAt, thread.Post.DeleteAt, "expected Thread.DeleteAt == Post.DeleteAt")
}
}
t.Run("GetThreadsForUser", func(t *testing.T) {
testCases := []struct {
Description string
UserID string
TeamID string
Options model.GetUserThreadsOpts
ExpectedThreads []*model.Post
}{
{"all teams, user1", user1ID, "", model.GetUserThreadsOpts{}, []*model.Post{
team1channel1post1, team1channel1post2, team1channel1post3, team2channel1post1, dm1post1, gm1post1,
}},
{"team1, user1", user1ID, team1.Id, model.GetUserThreadsOpts{}, []*model.Post{
team1channel1post1, team1channel1post2, team1channel1post3, dm1post1, gm1post1,
}},
{"team1, user1, unread", user1ID, team1.Id, model.GetUserThreadsOpts{Unread: true}, []*model.Post{
team1channel1post2, team1channel1post3, gm1post1,
}},
{"team1, user1, deleted", user1ID, team1.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{
team1channel1post1, team1channel1post2, team1channel1post3, dm1post1, gm1post1, // (no deleted threads in team1)
}},
{"team1, user1, unread + deleted", user1ID, team1.Id, model.GetUserThreadsOpts{Unread: true, Deleted: true}, []*model.Post{
team1channel1post2, team1channel1post3, gm1post1, // (no deleted threads in team1)
}},
{"team2, user1", user1ID, team2.Id, model.GetUserThreadsOpts{}, []*model.Post{
team2channel1post1, dm1post1, gm1post1,
}},
{"team2, user1, unread", user1ID, team2.Id, model.GetUserThreadsOpts{Unread: true}, []*model.Post{
gm1post1, // (no unread in team2)
}},
{"team2, user1, deleted", user1ID, team2.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{
team2channel1post1, team2channel1post2deleted, dm1post1, gm1post1,
}},
{"team2, user1, unread + deleted", user1ID, team2.Id, model.GetUserThreadsOpts{Unread: true, Deleted: true}, []*model.Post{
team2channel1post2deleted, gm1post1,
}},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
threads, err := ss.Thread().GetThreadsForUser(testCase.UserID, testCase.TeamID, testCase.Options)
require.NoError(t, err)
assertThreadPosts(t, threads, testCase.ExpectedThreads)
})
}
})
}
func testMarkAllAsReadByChannels(t *testing.T, ss store.Store) {
postingUserId := model.NewId()
userAID := model.NewId()
userBID := model.NewId()
team1, err := ss.Team().Save(&model.Team{
DisplayName: "Team1",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: team1.Id,
DisplayName: "Channel1",
Name: "channel1" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: team1.Id,
DisplayName: "Channel2",
Name: "channel2" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
createThreadMembership := func(userID, postID string) {
t.Helper()
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
_, err := ss.Thread().MaintainMembership(userID, postID, opts)
require.NoError(t, err)
}
assertThreadReplyCount := func(t *testing.T, userID string, count int64) {
t.Helper()
teamsUnread, err := ss.Thread().GetTeamsUnreadForUser(userID, []string{team1.Id}, false)
require.NoError(t, err)
require.Len(t, teamsUnread, 1, "unexpected unread teams count")
assert.Equal(t, count, teamsUnread[team1.Id].ThreadCount, "unexpected thread count")
}
t.Run("empty set of channels", func(t *testing.T) {
err := ss.Thread().MarkAllAsReadByChannels(model.NewId(), []string{})
require.NoError(t, err)
})
t.Run("single channel", func(t *testing.T) {
post, err := ss.Post().Save(&model.Post{
ChannelId: channel1.Id,
UserId: postingUserId,
Message: "Root",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: channel1.Id,
UserId: postingUserId,
RootId: post.Id,
Message: "Reply",
})
require.NoError(t, err)
createThreadMembership(userAID, post.Id)
createThreadMembership(userBID, post.Id)
assertThreadReplyCount(t, userAID, 1)
assertThreadReplyCount(t, userBID, 1)
err = ss.Thread().MarkAllAsReadByChannels(userAID, []string{channel1.Id})
require.NoError(t, err)
assertThreadReplyCount(t, userAID, 0)
assertThreadReplyCount(t, userBID, 1)
err = ss.Thread().MarkAllAsReadByChannels(userBID, []string{channel1.Id})
require.NoError(t, err)
assertThreadReplyCount(t, userAID, 0)
assertThreadReplyCount(t, userBID, 0)
})
t.Run("multiple channels", func(t *testing.T) {
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel1.Id,
UserId: postingUserId,
Message: "Root",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: channel1.Id,
UserId: postingUserId,
RootId: post1.Id,
Message: "Reply",
})
require.NoError(t, err)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: postingUserId,
Message: "Root",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: postingUserId,
RootId: post2.Id,
Message: "Reply",
})
require.NoError(t, err)
createThreadMembership(userAID, post1.Id)
createThreadMembership(userBID, post1.Id)
createThreadMembership(userAID, post2.Id)
createThreadMembership(userBID, post2.Id)
assertThreadReplyCount(t, userAID, 2)
assertThreadReplyCount(t, userBID, 2)
err = ss.Thread().MarkAllAsReadByChannels(userAID, []string{channel1.Id, channel2.Id})
require.NoError(t, err)
assertThreadReplyCount(t, userAID, 0)
assertThreadReplyCount(t, userBID, 2)
err = ss.Thread().MarkAllAsReadByChannels(userBID, []string{channel1.Id, channel2.Id})
require.NoError(t, err)
assertThreadReplyCount(t, userAID, 0)
assertThreadReplyCount(t, userBID, 0)
})
}
func testGetTopThreads(t *testing.T, ss store.Store) {
// create two users
u1 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
_, err := ss.User().Save(&u1)
require.NoError(t, err)
u2 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
_, err = ss.User().Save(&u2)
require.NoError(t, err)
u3 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
_, err = ss.User().Save(&u3)
require.NoError(t, err)
t.Run("test get top team threads", func(t *testing.T) {
const limit = 10
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u1.Id,
})
require.NoError(t, err)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u2.Id,
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post2.Id, post1.UserId, 2000)
// get top threads
topThreadsInTeam, err := ss.Thread().GetTopThreadsForTeamSince(team.Id, model.NewId(), 12, 0, limit)
require.NoError(t, err)
// require length of top threads to be 2
require.Len(t, topThreadsInTeam.Items, 2)
// require first element to be post1 with 2 replyCount=2
require.Equal(t, topThreadsInTeam.Items[0].PostId, post1.Id)
require.Equal(t, topThreadsInTeam.Items[0].UserId, post1.UserId)
require.Equal(t, topThreadsInTeam.Items[0].UserInformation.Id, post1.UserId)
require.Equal(t, topThreadsInTeam.Items[0].Post.ReplyCount, int64(2))
require.Equal(t, topThreadsInTeam.Items[0].Post.Message, post1.Message)
// require second element to be post2 with 2 replyCount=2
require.Equal(t, topThreadsInTeam.Items[1].PostId, post2.Id)
require.Equal(t, topThreadsInTeam.Items[1].Post.ReplyCount, int64(1))
require.Equal(t, topThreadsInTeam.Items[1].UserId, post2.UserId)
require.Equal(t, topThreadsInTeam.Items[1].UserInformation.Id, post2.UserId)
require.Equal(t, topThreadsInTeam.Items[1].Post.Message, post2.Message)
// require topThreads[i].Post is not null
require.Equal(t, topThreadsInTeam.Items[0].Post.Id, post1.Id)
require.Equal(t, topThreadsInTeam.Items[1].Post.Id, post2.Id)
})
t.Run("test get top user threads", func(t *testing.T) {
const limit = 10
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u1.Id,
})
require.NoError(t, err)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u2.Id,
})
require.NoError(t, err)
post3, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u3.Id,
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post2.Id, post2.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post2.Id, post2.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post3.Id, post3.UserId, 2000)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
// create threadmemberships entries.
_, err = ss.Thread().MaintainMembership(post1.UserId, post1.Id, opts)
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(post2.UserId, post2.Id, opts)
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(post2.UserId, post3.Id, opts)
require.NoError(t, err)
// get top threads by user
topThreadsByUser1, err := ss.Thread().GetTopThreadsForUserSince(team.Id, post1.UserId, 12, 0, limit)
require.NoError(t, err)
topThreadsByUser2, err := ss.Thread().GetTopThreadsForUserSince(team.Id, post2.UserId, 12, 0, limit)
require.NoError(t, err)
// require length of top threads by users to be 1,2 respectively
require.Len(t, topThreadsByUser1.Items, 1)
require.Len(t, topThreadsByUser2.Items, 2)
// require first element of topThreadsByUser1 to be post1 with 2 replyCount=2
require.Equal(t, topThreadsByUser1.Items[0].PostId, post1.Id)
require.Equal(t, topThreadsByUser1.Items[0].Post.ReplyCount, int64(2))
require.Equal(t, topThreadsByUser1.Items[0].Post.Message, post1.Message)
require.Equal(t, topThreadsByUser1.Items[0].UserId, post1.UserId)
require.Equal(t, topThreadsByUser1.Items[0].UserInformation.Id, post1.UserId)
// require elements of topThreadsByUser2 to be post2 and post3 respectively
require.Equal(t, topThreadsByUser2.Items[0].PostId, post2.Id)
require.Equal(t, topThreadsByUser2.Items[0].Post.ReplyCount, int64(2))
require.Equal(t, topThreadsByUser2.Items[0].Post.Message, post2.Message)
require.Equal(t, topThreadsByUser2.Items[0].UserId, post2.UserId)
require.Equal(t, topThreadsByUser2.Items[0].UserInformation.Id, post2.UserId)
require.Equal(t, topThreadsByUser2.Items[1].PostId, post3.Id)
require.Equal(t, topThreadsByUser2.Items[1].Post.ReplyCount, int64(1))
require.Equal(t, topThreadsByUser2.Items[1].Post.Message, post3.Message)
require.Equal(t, topThreadsByUser2.Items[1].UserId, post3.UserId)
require.Equal(t, topThreadsByUser2.Items[1].UserInformation.Id, post3.UserId)
// require topThreads[i].Post is not null
require.Equal(t, topThreadsByUser1.Items[0].Post.Id, post1.Id)
require.Equal(t, topThreadsByUser2.Items[1].Post.Id, post3.Id)
})
t.Run("test get top threads only from given teamid", func(t *testing.T) {
const limit = 10
team1, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
team2, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: team1.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: team2.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel1.Id,
UserId: u1.Id,
})
require.NoError(t, err)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: u2.Id,
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel1.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel1.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel2.Id, post2.Id, post2.UserId, 2000)
// assert that getting top threads from teamid 1 doesn't have post1.Id
topThreadsTeam2, err := ss.Thread().GetTopThreadsForTeamSince(team2.Id, u1.Id, 12, 0, limit)
require.NoError(t, err)
require.Len(t, topThreadsTeam2.Items, 1)
require.Equal(t, topThreadsTeam2.Items[0].Post.Id, post2.Id)
})
t.Run("test get top threads only from non-direct channels", func(t *testing.T) {
const limit = 10
team1, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel1, err := ss.Channel().CreateDirectChannel(&u1, &u2)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: team1.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel1.Id,
UserId: u1.Id,
})
require.NoError(t, err)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: u2.Id,
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel1.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel1.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel2.Id, post2.Id, u1.Id, 2000)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
// create threadmemberships entries.
_, err = ss.Thread().MaintainMembership(u1.Id, post1.Id, opts)
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(u1.Id, post2.Id, opts)
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(u2.Id, post1.Id, opts)
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(u2.Id, post2.Id, opts)
require.NoError(t, err)
// assert that getting top threads from teamid 1 doesn't have DMs
topThreadsTeam1, err := ss.Thread().GetTopThreadsForTeamSince(team1.Id, u1.Id, 12, 0, limit)
require.NoError(t, err)
require.Len(t, topThreadsTeam1.Items, 1)
require.Equal(t, topThreadsTeam1.Items[0].Post.Id, post2.Id)
// assert that getting top threads from user 1 doesn't contain dm threads.
topUserThreads, err := ss.Thread().GetTopThreadsForUserSince(team1.Id, u1.Id, 12, 0, limit)
require.NoError(t, err)
require.Len(t, topUserThreads.Items, 1)
require.Equal(t, topUserThreads.Items[0].Post.Id, post2.Id)
})
t.Run("test get top threads doesn't exceed duration", func(t *testing.T) {
const limit = 10
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u1.Id,
})
require.NoError(t, err)
// post 2 has replies after 10 ms unix time.
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u2.Id,
CreateAt: 1,
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post2.Id, post1.UserId, 10)
// get top threads
topThreadsInTeamNewer, err := ss.Thread().GetTopThreadsForTeamSince(team.Id, model.NewId(), 12, 0, limit)
require.NoError(t, err)
// require length of top threads to be 2
require.Len(t, topThreadsInTeamNewer.Items, 1)
// require first element to be post1 with 2 replyCount=2
require.Equal(t, topThreadsInTeamNewer.Items[0].PostId, post1.Id)
// get top threads
topThreadsInTeamOlder, err := ss.Thread().GetTopThreadsForTeamSince(team.Id, model.NewId(), 9, 0, limit)
require.NoError(t, err)
// require length of top threads to be 2
require.Len(t, topThreadsInTeamOlder.Items, 2)
// require first element to be post1 with 2 replyCount=2
require.Equal(t, topThreadsInTeamOlder.Items[1].PostId, post2.Id)
})
}
func testMarkAllAsReadByTeam(t *testing.T, ss store.Store) {
createThreadMembership := func(userID, postID string) {
t.Helper()
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
_, err := ss.Thread().MaintainMembership(userID, postID, opts)
require.NoError(t, err)
}
assertThreadReplyCount := func(t *testing.T, userID, teamID string, count int64, message string) {
t.Helper()
teamsUnread, err := ss.Thread().GetTeamsUnreadForUser(userID, []string{teamID}, true)
require.NoError(t, err)
require.Lenf(t, teamsUnread, 1, "unexpected unread teams count: %s", message)
assert.Equalf(t, count, teamsUnread[teamID].ThreadCount, "unexpected thread count: %s", message)
}
postingUserId := model.NewId()
userAID := model.NewId()
userBID := model.NewId()
team1, err := ss.Team().Save(&model.Team{
DisplayName: "Team1",
Name: "team1" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
team1channel1, err := ss.Channel().Save(&model.Channel{
TeamId: team1.Id,
DisplayName: "Team1: Channel1",
Name: "team1channel1" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
team1channel2, err := ss.Channel().Save(&model.Channel{
TeamId: team1.Id,
DisplayName: "Team1: Channel2",
Name: "team1channel2" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
team2, err := ss.Team().Save(&model.Team{
DisplayName: "Team2",
Name: "team2" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
team2channel1, err := ss.Channel().Save(&model.Channel{
TeamId: team2.Id,
DisplayName: "Team2: Channel1",
Name: "team2channel1" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
team2channel2, err := ss.Channel().Save(&model.Channel{
TeamId: team2.Id,
DisplayName: "Team2: Channel2",
Name: "team2channel2" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
team1channel1post1, err := ss.Post().Save(&model.Post{
ChannelId: team1channel1.Id,
UserId: postingUserId,
Message: "Root",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: team1channel1.Id,
UserId: postingUserId,
RootId: team1channel1post1.Id,
Message: "Reply",
})
require.NoError(t, err)
team1channel2post1, err := ss.Post().Save(&model.Post{
ChannelId: team1channel2.Id,
UserId: postingUserId,
Message: "Root",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: team1channel1.Id,
UserId: postingUserId,
RootId: team1channel2post1.Id,
Message: "Reply",
})
require.NoError(t, err)
team2channel1post1, err := ss.Post().Save(&model.Post{
ChannelId: team2channel1.Id,
UserId: postingUserId,
Message: "Root",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: team2channel1.Id,
UserId: postingUserId,
RootId: team2channel1post1.Id,
Message: "Reply",
})
require.NoError(t, err)
team2channel2post1, err := ss.Post().Save(&model.Post{
ChannelId: team2channel2.Id,
UserId: postingUserId,
Message: "Root",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: team2channel1.Id,
UserId: postingUserId,
RootId: team2channel2post1.Id,
Message: "Reply",
})
require.NoError(t, err)
gm1, err := ss.Channel().Save(&model.Channel{
DisplayName: "GM1",
Name: "gm1" + model.NewId(),
Type: model.ChannelTypeGroup,
}, -1)
require.NoError(t, err)
gm1post1, err := ss.Post().Save(&model.Post{
ChannelId: gm1.Id,
UserId: postingUserId,
Message: "Root",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: gm1.Id,
UserId: postingUserId,
RootId: gm1post1.Id,
Message: "Reply",
})
require.NoError(t, err)
gm2, err := ss.Channel().Save(&model.Channel{
DisplayName: "GM1",
Name: "gm1" + model.NewId(),
Type: model.ChannelTypeGroup,
}, -1)
require.NoError(t, err)
gm2post1, err := ss.Post().Save(&model.Post{
ChannelId: gm2.Id,
UserId: postingUserId,
Message: "Root",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: gm2.Id,
UserId: postingUserId,
RootId: gm2post1.Id,
Message: "Reply",
})
require.NoError(t, err)
t.Run("empty team", func(t *testing.T) {
err = ss.Thread().MarkAllAsReadByTeam(model.NewId(), "")
require.NoError(t, err)
})
t.Run("unknown team", func(t *testing.T) {
err = ss.Thread().MarkAllAsReadByTeam(model.NewId(), model.NewId())
require.NoError(t, err)
})
t.Run("team1", func(t *testing.T) {
createThreadMembership(userAID, team1channel1post1.Id)
createThreadMembership(userBID, team1channel1post1.Id)
createThreadMembership(userAID, team1channel2post1.Id)
createThreadMembership(userBID, team1channel2post1.Id)
createThreadMembership(userAID, team2channel1post1.Id)
createThreadMembership(userBID, team2channel1post1.Id)
// Note that GMs (and similarly, DMs) don't count towards this API.
createThreadMembership(userAID, gm1.Id)
createThreadMembership(userBID, gm1.Id)
createThreadMembership(userAID, gm2.Id)
createThreadMembership(userBID, gm2.Id)
assertThreadReplyCount(t, userAID, team1.Id, 2, "expected 2 unread messages in team1 for userA")
assertThreadReplyCount(t, userBID, team1.Id, 2, "expected 2 unread messages in team1 for userB")
assertThreadReplyCount(t, userAID, team2.Id, 1, "expected 1 unread message in team2 for userA")
assertThreadReplyCount(t, userBID, team2.Id, 1, "expected 1 unread message in team2 for userB")
err = ss.Thread().MarkAllAsReadByTeam(userAID, team1.Id)
require.NoError(t, err)
assertThreadReplyCount(t, userAID, team1.Id, 0, "expected 0 unread messages in team1 for userA")
assertThreadReplyCount(t, userBID, team1.Id, 2, "expected 2 unread messages in team1 for userB")
assertThreadReplyCount(t, userAID, team2.Id, 1, "expected 1 unread message in team2 for userA")
assertThreadReplyCount(t, userBID, team2.Id, 1, "expected 1 unread message in team2 for userB")
err = ss.Thread().MarkAllAsReadByTeam(userBID, team1.Id)
require.NoError(t, err)
assertThreadReplyCount(t, userAID, team1.Id, 0, "expected 0 unread messages in team1 for userA")
assertThreadReplyCount(t, userBID, team1.Id, 0, "expected 0 unread messages in team1 for userB")
assertThreadReplyCount(t, userAID, team2.Id, 1, "expected 1 unread message in team2 for userA")
assertThreadReplyCount(t, userBID, team2.Id, 1, "expected 1 unread message in team2 for userB")
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestTokensStore(t *testing.T, ss store.Store) {
t.Run("TokensCleanup", func(t *testing.T) { testTokensCleanup(t, ss) })
}
func testTokensCleanup(t *testing.T, ss store.Store) {
now := model.GetMillis()
for i := 0; i < 10; i++ {
err := ss.Token().Save(&model.Token{
Token: model.NewRandomString(model.TokenSize),
CreateAt: now - int64(i),
Type: model.TokenTypeOAuth,
Extra: "",
})
require.NoError(t, err)
}
tokens, err := ss.Token().GetAllTokensByType(model.TokenTypeOAuth)
require.NoError(t, err)
assert.Len(t, tokens, 10)
ss.Token().Cleanup(now + int64(1))
tokens, err = ss.Token().GetAllTokensByType(model.TokenTypeOAuth)
require.NoError(t, err)
assert.Len(t, tokens, 0)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
func TestTrueUpReviewStatusStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("CreateTrueUpReviewStatusRecord", func(t *testing.T) { testCreateTrueUpReviewStatus(t, ss) })
t.Run("GetTrueUpReviewStatus", func(t *testing.T) { testGetTrueUpReviewStatus(t, ss) })
t.Run("Update", func(t *testing.T) { testUpdateTrueUpReviewStatus(t, ss) })
}
func testCreateTrueUpReviewStatus(t *testing.T, ss store.Store) {
now := time.Date(time.Now().Year(), time.January, 1, 0, 0, 0, 0, time.Local)
reviewStatus := model.TrueUpReviewStatus{
Completed: true,
DueDate: utils.GetNextTrueUpReviewDueDate(now).UnixMilli(),
}
t.Run("create true up review status", func(t *testing.T) {
resp, err := ss.TrueUpReview().CreateTrueUpReviewStatusRecord(&reviewStatus)
assert.NoError(t, err)
assert.Equal(t, reviewStatus.Completed, resp.Completed)
assert.Equal(t, reviewStatus.DueDate, resp.DueDate)
})
}
func testGetTrueUpReviewStatus(t *testing.T, ss store.Store) {
now := time.Date(time.Now().Year(), time.August, 1, 0, 0, 0, 0, time.Local)
dueDate := utils.GetNextTrueUpReviewDueDate(now).UnixMilli()
reviewStatus := model.TrueUpReviewStatus{
Completed: true,
DueDate: dueDate,
}
_, err := ss.TrueUpReview().CreateTrueUpReviewStatusRecord(&reviewStatus)
assert.NoError(t, err)
t.Run("get true up review status", func(t *testing.T) {
resp, err := ss.TrueUpReview().GetTrueUpReviewStatus(dueDate)
assert.NoError(t, err)
assert.Equal(t, resp.Completed, resp.Completed)
assert.Equal(t, resp.DueDate, resp.DueDate)
})
}
func testUpdateTrueUpReviewStatus(t *testing.T, ss store.Store) {
now := time.Date(time.Now().Year(), time.April, 1, 0, 0, 0, 0, time.Local)
reviewStatus := model.TrueUpReviewStatus{
Completed: false,
DueDate: utils.GetNextTrueUpReviewDueDate(now).UnixMilli(),
}
_, err := ss.TrueUpReview().CreateTrueUpReviewStatusRecord(&reviewStatus)
assert.NoError(t, err)
t.Run("save ", func(t *testing.T) {
reviewStatus.Completed = true
resp, err := ss.TrueUpReview().Update(&reviewStatus)
assert.NoError(t, err)
assert.Equal(t, resp.Completed, resp.Completed)
assert.Equal(t, resp.DueDate, resp.DueDate)
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestUploadSessionStore(t *testing.T, ss store.Store) {
t.Run("UploadSessionStoreSaveGet", func(t *testing.T) { testUploadSessionStoreSaveGet(t, ss) })
t.Run("UploadSessionStoreUpdate", func(t *testing.T) { testUploadSessionStoreUpdate(t, ss) })
t.Run("UploadSessionStoreGetForUser", func(t *testing.T) { testUploadSessionStoreGetForUser(t, ss) })
t.Run("UploadSessionStoreDelete", func(t *testing.T) { testUploadSessionStoreDelete(t, ss) })
}
func testUploadSessionStoreSaveGet(t *testing.T, ss store.Store) {
var session *model.UploadSession
t.Run("saving nil session should fail", func(t *testing.T) {
us, err := ss.UploadSession().Save(nil)
require.Error(t, err)
require.Nil(t, us)
})
t.Run("saving empty session should fail", func(t *testing.T) {
session = &model.UploadSession{}
us, err := ss.UploadSession().Save(session)
require.Error(t, err)
require.Nil(t, us)
})
t.Run("saving valid session should succeed", func(t *testing.T) {
session = &model.UploadSession{
Type: model.UploadTypeAttachment,
UserId: model.NewId(),
ChannelId: model.NewId(),
Filename: "test",
FileSize: 1024,
Path: "/tmp/test",
}
us, err := ss.UploadSession().Save(session)
require.NoError(t, err)
require.NotNil(t, us)
require.NotEmpty(t, us)
})
t.Run("getting non-existing session should fail", func(t *testing.T) {
us, err := ss.UploadSession().Get(context.Background(), "fake")
require.Error(t, err)
require.Nil(t, us)
})
t.Run("getting existing session should succeed", func(t *testing.T) {
us, err := ss.UploadSession().Get(context.Background(), session.Id)
require.NoError(t, err)
require.NotNil(t, us)
require.Equal(t, session, us)
})
}
func testUploadSessionStoreUpdate(t *testing.T, ss store.Store) {
session := &model.UploadSession{
Type: model.UploadTypeAttachment,
UserId: model.NewId(),
ChannelId: model.NewId(),
Filename: "test",
FileSize: 1024,
Path: "/tmp/test",
}
t.Run("updating nil session should fail", func(t *testing.T) {
err := ss.UploadSession().Update(nil)
require.Error(t, err)
})
t.Run("updating invalid session should fail", func(t *testing.T) {
err := ss.UploadSession().Update(&model.UploadSession{})
require.Error(t, err)
})
t.Run("updating non-existing session should fail", func(t *testing.T) {
err := ss.UploadSession().Update(&model.UploadSession{})
require.Error(t, err)
})
t.Run("updating existing session should succeed", func(t *testing.T) {
us, err := ss.UploadSession().Save(session)
require.NoError(t, err)
require.NotNil(t, us)
require.NotEmpty(t, us)
us.FileOffset = 512
err = ss.UploadSession().Update(us)
require.NoError(t, err)
updated, err := ss.UploadSession().Get(context.Background(), us.Id)
require.NoError(t, err)
require.NotNil(t, us)
require.Equal(t, us, updated)
})
}
func testUploadSessionStoreGetForUser(t *testing.T, ss store.Store) {
userId := model.NewId()
sessions := []*model.UploadSession{
{
Type: model.UploadTypeAttachment,
UserId: userId,
ChannelId: model.NewId(),
Filename: "test0",
FileSize: 1024,
Path: "/tmp/test0",
},
{
Type: model.UploadTypeAttachment,
UserId: model.NewId(),
ChannelId: model.NewId(),
Filename: "test1",
FileSize: 1024,
Path: "/tmp/test1",
},
{
Type: model.UploadTypeAttachment,
UserId: userId,
ChannelId: model.NewId(),
Filename: "test2",
FileSize: 1024,
Path: "/tmp/test2",
},
{
Type: model.UploadTypeAttachment,
UserId: userId,
ChannelId: model.NewId(),
Filename: "test3",
FileSize: 1024,
Path: "/tmp/test3",
},
}
t.Run("should return no sessions", func(t *testing.T) {
us, err := ss.UploadSession().GetForUser(userId)
require.NoError(t, err)
require.NotNil(t, us)
require.Empty(t, us)
})
for i := 0; i < len(sessions); i++ {
us, err := ss.UploadSession().Save(sessions[i])
require.NoError(t, err)
require.NotNil(t, us)
require.NotEmpty(t, us)
// We need this to make sure the ordering is consistent.
time.Sleep(2 * time.Millisecond)
}
t.Run("should return existing sessions", func(t *testing.T) {
us, err := ss.UploadSession().GetForUser(userId)
require.NoError(t, err)
require.NotNil(t, us)
require.NotEmpty(t, us)
require.Len(t, us, 3)
require.Equal(t, sessions[0], us[0])
require.Equal(t, sessions[2], us[1])
require.Equal(t, sessions[3], us[2])
})
}
func testUploadSessionStoreDelete(t *testing.T, ss store.Store) {
session := &model.UploadSession{
Id: model.NewId(),
Type: model.UploadTypeAttachment,
UserId: model.NewId(),
ChannelId: model.NewId(),
Filename: "test",
FileSize: 1024,
Path: "/tmp/test",
}
t.Run("deleting invalid id should fail", func(t *testing.T) {
err := ss.UploadSession().Delete("invalidId")
require.Error(t, err)
})
t.Run("deleting existing session should succeed", func(t *testing.T) {
us, err := ss.UploadSession().Save(session)
require.NoError(t, err)
require.NotNil(t, us)
require.NotEmpty(t, us)
err = ss.UploadSession().Delete(session.Id)
require.NoError(t, err)
us, err = ss.UploadSession().Get(context.Background(), us.Id)
require.Error(t, err)
require.Nil(t, us)
require.IsType(t, &store.ErrNotFound{}, err)
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestUserAccessTokenStore(t *testing.T, ss store.Store) {
t.Run("UserAccessTokenSaveGetDelete", func(t *testing.T) { testUserAccessTokenSaveGetDelete(t, ss) })
t.Run("UserAccessTokenDisableEnable", func(t *testing.T) { testUserAccessTokenDisableEnable(t, ss) })
t.Run("UserAccessTokenSearch", func(t *testing.T) { testUserAccessTokenSearch(t, ss) })
}
func testUserAccessTokenSaveGetDelete(t *testing.T, ss store.Store) {
uat := &model.UserAccessToken{
Token: model.NewId(),
UserId: model.NewId(),
Description: "testtoken",
}
s1 := &model.Session{}
s1.UserId = uat.UserId
s1.Token = uat.Token
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
_, nErr := ss.UserAccessToken().Save(uat)
require.NoError(t, nErr)
result, terr := ss.UserAccessToken().Get(uat.Id)
require.NoError(t, terr)
require.Equal(t, result.Token, uat.Token, "received incorrect token after save")
received, err2 := ss.UserAccessToken().GetByToken(uat.Token)
require.NoError(t, err2)
require.Equal(t, received.Token, uat.Token, "received incorrect token after save")
_, nErr = ss.UserAccessToken().GetByToken("notarealtoken")
require.Error(t, nErr, "should have failed on bad token")
received2, err2 := ss.UserAccessToken().GetByUser(uat.UserId, 0, 100)
require.NoError(t, err2)
require.Equal(t, 1, len(received2), "received incorrect number of tokens after save")
result2, err := ss.UserAccessToken().GetAll(0, 100)
require.NoError(t, err)
require.Equal(t, 1, len(result2), "received incorrect number of tokens after save")
nErr = ss.UserAccessToken().Delete(uat.Id)
require.NoError(t, nErr)
_, err = ss.Session().Get(context.Background(), s1.Token)
require.Error(t, err, "should error - session should be deleted")
_, nErr = ss.UserAccessToken().GetByToken(s1.Token)
require.Error(t, nErr, "should error - access token should be deleted")
s2 := &model.Session{}
s2.UserId = uat.UserId
s2.Token = uat.Token
s2, err = ss.Session().Save(s2)
require.NoError(t, err)
_, nErr = ss.UserAccessToken().Save(uat)
require.NoError(t, nErr)
nErr = ss.UserAccessToken().DeleteAllForUser(uat.UserId)
require.NoError(t, nErr)
_, err = ss.Session().Get(context.Background(), s2.Token)
require.Error(t, err, "should error - session should be deleted")
_, nErr = ss.UserAccessToken().GetByToken(s2.Token)
require.Error(t, nErr, "should error - access token should be deleted")
}
func testUserAccessTokenDisableEnable(t *testing.T, ss store.Store) {
uat := &model.UserAccessToken{
Token: model.NewId(),
UserId: model.NewId(),
Description: "testtoken",
}
s1 := &model.Session{}
s1.UserId = uat.UserId
s1.Token = uat.Token
s1, err := ss.Session().Save(s1)
require.NoError(t, err)
_, nErr := ss.UserAccessToken().Save(uat)
require.NoError(t, nErr)
nErr = ss.UserAccessToken().UpdateTokenDisable(uat.Id)
require.NoError(t, nErr)
_, err = ss.Session().Get(context.Background(), s1.Token)
require.Error(t, err, "should error - session should be deleted")
s2 := &model.Session{}
s2.UserId = uat.UserId
s2.Token = uat.Token
_, err = ss.Session().Save(s2)
require.NoError(t, err)
nErr = ss.UserAccessToken().UpdateTokenEnable(uat.Id)
require.NoError(t, nErr)
}
func testUserAccessTokenSearch(t *testing.T, ss store.Store) {
u1 := model.User{}
u1.Email = MakeEmail()
u1.Username = model.NewId()
_, err := ss.User().Save(&u1)
require.NoError(t, err)
uat := &model.UserAccessToken{
Token: model.NewId(),
UserId: u1.Id,
Description: "testtoken",
}
s1 := &model.Session{}
s1.UserId = uat.UserId
s1.Token = uat.Token
_, nErr := ss.Session().Save(s1)
require.NoError(t, nErr)
_, nErr = ss.UserAccessToken().Save(uat)
require.NoError(t, nErr)
received, nErr := ss.UserAccessToken().Search(uat.Id)
require.NoError(t, nErr)
require.Equal(t, 1, len(received), "received incorrect number of tokens after search")
received, nErr = ss.UserAccessToken().Search(uat.UserId)
require.NoError(t, nErr)
require.Equal(t, 1, len(received), "received incorrect number of tokens after search")
received, nErr = ss.UserAccessToken().Search(u1.Username)
require.NoError(t, nErr)
require.Equal(t, 1, len(received), "received incorrect number of tokens after search")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"context"
"errors"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
const (
DayMilliseconds = 24 * 60 * 60 * 1000
MonthMilliseconds = 31 * DayMilliseconds
)
func cleanupStatusStore(t *testing.T, s SqlStore) {
_, execerr := s.GetMasterX().Exec(`DELETE FROM Status`)
require.NoError(t, execerr)
}
func TestUserStore(t *testing.T, ss store.Store, s SqlStore) {
users, err := ss.User().GetAll()
require.NoError(t, err, "failed cleaning up test users")
for _, u := range users {
err := ss.User().PermanentDelete(u.Id)
require.NoError(t, err, "failed cleaning up test user %s", u.Username)
}
t.Run("IsEmpty", func(t *testing.T) { testIsEmpty(t, ss) })
t.Run("Count", func(t *testing.T) { testCount(t, ss) })
t.Run("AnalyticsActiveCount", func(t *testing.T) { testUserStoreAnalyticsActiveCount(t, ss, s) })
t.Run("AnalyticsActiveCountForPeriod", func(t *testing.T) { testUserStoreAnalyticsActiveCountForPeriod(t, ss, s) })
t.Run("AnalyticsGetInactiveUsersCount", func(t *testing.T) { testUserStoreAnalyticsGetInactiveUsersCount(t, ss) })
t.Run("AnalyticsGetSystemAdminCount", func(t *testing.T) { testUserStoreAnalyticsGetSystemAdminCount(t, ss) })
t.Run("AnalyticsGetGuestCount", func(t *testing.T) { testUserStoreAnalyticsGetGuestCount(t, ss) })
t.Run("AnalyticsGetExternalUsers", func(t *testing.T) { testUserStoreAnalyticsGetExternalUsers(t, ss) })
t.Run("Save", func(t *testing.T) { testUserStoreSave(t, ss) })
t.Run("Update", func(t *testing.T) { testUserStoreUpdate(t, ss) })
t.Run("UpdateUpdateAt", func(t *testing.T) { testUserStoreUpdateUpdateAt(t, ss) })
t.Run("UpdateFailedPasswordAttempts", func(t *testing.T) { testUserStoreUpdateFailedPasswordAttempts(t, ss) })
t.Run("Get", func(t *testing.T) { testUserStoreGet(t, ss) })
t.Run("GetAllUsingAuthService", func(t *testing.T) { testGetAllUsingAuthService(t, ss) })
t.Run("GetAllProfiles", func(t *testing.T) { testUserStoreGetAllProfiles(t, ss) })
t.Run("GetProfiles", func(t *testing.T) { testUserStoreGetProfiles(t, ss) })
t.Run("GetProfilesInChannel", func(t *testing.T) { testUserStoreGetProfilesInChannel(t, ss) })
t.Run("GetProfilesInChannelByStatus", func(t *testing.T) { testUserStoreGetProfilesInChannelByStatus(t, ss, s) })
t.Run("GetProfilesInChannelByAdmin", func(t *testing.T) { testUserStoreGetProfilesInChannelByAdmin(t, ss, s) })
t.Run("GetProfilesWithoutTeam", func(t *testing.T) { testUserStoreGetProfilesWithoutTeam(t, ss) })
t.Run("GetAllProfilesInChannel", func(t *testing.T) { testUserStoreGetAllProfilesInChannel(t, ss) })
t.Run("GetProfilesNotInChannel", func(t *testing.T) { testUserStoreGetProfilesNotInChannel(t, ss) })
t.Run("GetProfilesByIds", func(t *testing.T) { testUserStoreGetProfilesByIds(t, ss) })
t.Run("GetProfileByGroupChannelIdsForUser", func(t *testing.T) { testUserStoreGetProfileByGroupChannelIdsForUser(t, ss) })
t.Run("GetProfilesByUsernames", func(t *testing.T) { testUserStoreGetProfilesByUsernames(t, ss) })
t.Run("GetSystemAdminProfiles", func(t *testing.T) { testUserStoreGetSystemAdminProfiles(t, ss) })
t.Run("GetByEmail", func(t *testing.T) { testUserStoreGetByEmail(t, ss) })
t.Run("GetByAuthData", func(t *testing.T) { testUserStoreGetByAuthData(t, ss) })
t.Run("GetByUsername", func(t *testing.T) { testUserStoreGetByUsername(t, ss) })
t.Run("GetForLogin", func(t *testing.T) { testUserStoreGetForLogin(t, ss) })
t.Run("UpdatePassword", func(t *testing.T) { testUserStoreUpdatePassword(t, ss) })
t.Run("Delete", func(t *testing.T) { testUserStoreDelete(t, ss) })
t.Run("UpdateAuthData", func(t *testing.T) { testUserStoreUpdateAuthData(t, ss) })
t.Run("ResetAuthDataToEmailForUsers", func(t *testing.T) { testUserStoreResetAuthDataToEmailForUsers(t, ss) })
t.Run("UserUnreadCount", func(t *testing.T) { testUserUnreadCount(t, ss) })
t.Run("UpdateMfaSecret", func(t *testing.T) { testUserStoreUpdateMfaSecret(t, ss) })
t.Run("UpdateMfaActive", func(t *testing.T) { testUserStoreUpdateMfaActive(t, ss) })
t.Run("GetRecentlyActiveUsersForTeam", func(t *testing.T) { testUserStoreGetRecentlyActiveUsersForTeam(t, ss, s) })
t.Run("GetNewUsersForTeam", func(t *testing.T) { testUserStoreGetNewUsersForTeam(t, ss) })
t.Run("Search", func(t *testing.T) { testUserStoreSearch(t, ss) })
t.Run("SearchNotInChannel", func(t *testing.T) { testUserStoreSearchNotInChannel(t, ss) })
t.Run("SearchInChannel", func(t *testing.T) { testUserStoreSearchInChannel(t, ss) })
t.Run("SearchNotInTeam", func(t *testing.T) { testUserStoreSearchNotInTeam(t, ss) })
t.Run("SearchWithoutTeam", func(t *testing.T) { testUserStoreSearchWithoutTeam(t, ss) })
t.Run("SearchInGroup", func(t *testing.T) { testUserStoreSearchInGroup(t, ss) })
t.Run("SearchNotInGroup", func(t *testing.T) { testUserStoreSearchNotInGroup(t, ss) })
t.Run("GetProfilesNotInTeam", func(t *testing.T) { testUserStoreGetProfilesNotInTeam(t, ss) })
t.Run("ClearAllCustomRoleAssignments", func(t *testing.T) { testUserStoreClearAllCustomRoleAssignments(t, ss) })
t.Run("GetAllAfter", func(t *testing.T) { testUserStoreGetAllAfter(t, ss) })
t.Run("GetUsersBatchForIndexing", func(t *testing.T) { testUserStoreGetUsersBatchForIndexing(t, ss) })
t.Run("GetTeamGroupUsers", func(t *testing.T) { testUserStoreGetTeamGroupUsers(t, ss) })
t.Run("GetChannelGroupUsers", func(t *testing.T) { testUserStoreGetChannelGroupUsers(t, ss) })
t.Run("PromoteGuestToUser", func(t *testing.T) { testUserStorePromoteGuestToUser(t, ss) })
t.Run("DemoteUserToGuest", func(t *testing.T) { testUserStoreDemoteUserToGuest(t, ss) })
t.Run("DeactivateGuests", func(t *testing.T) { testDeactivateGuests(t, ss) })
t.Run("ResetLastPictureUpdate", func(t *testing.T) { testUserStoreResetLastPictureUpdate(t, ss) })
t.Run("GetKnownUsers", func(t *testing.T) { testGetKnownUsers(t, ss) })
t.Run("GetUsersWithInvalidEmails", func(t *testing.T) { testGetUsersWithInvalidEmails(t, ss) })
t.Run("GetFirstSystemAdminID", func(t *testing.T) { testUserStoreGetFirstSystemAdminID(t, ss) })
}
func testUserStoreSave(t *testing.T, ss store.Store) {
teamId := model.NewId()
maxUsersPerTeam := 50
u1 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
_, err := ss.User().Save(&u1)
require.NoError(t, err, "couldn't save user")
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, maxUsersPerTeam)
require.NoError(t, nErr)
_, err = ss.User().Save(&u1)
require.Error(t, err, "shouldn't be able to update user from save")
u2 := model.User{
Email: u1.Email,
Username: model.NewId(),
}
_, err = ss.User().Save(&u2)
require.Error(t, err, "should be unique email")
u2.Email = MakeEmail()
u2.Username = u1.Username
_, err = ss.User().Save(&u2)
require.Error(t, err, "should be unique username")
u2.Username = ""
_, err = ss.User().Save(&u2)
require.Error(t, err, "should be non-empty username")
u3 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
NotifyProps: make(map[string]string, 1),
}
maxPostSize := ss.Post().GetMaxPostSize()
u3.NotifyProps[model.AutoResponderMessageNotifyProp] = strings.Repeat("a", maxPostSize+1)
_, err = ss.User().Save(&u3)
require.Error(t, err, "auto responder message size should not be greater than maxPostSize")
for i := 0; i < 49; i++ {
u := model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
_, err = ss.User().Save(&u)
require.NoError(t, err, "couldn't save item")
defer func() { require.NoError(t, ss.User().PermanentDelete(u.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u.Id}, maxUsersPerTeam)
require.NoError(t, nErr)
}
u2.Id = ""
u2.Email = MakeEmail()
u2.Username = model.NewId()
_, err = ss.User().Save(&u2)
require.NoError(t, err, "couldn't save item")
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, maxUsersPerTeam)
require.Error(t, nErr, "should be the limit")
}
func testUserStoreUpdate(t *testing.T, ss store.Store) {
u1 := &model.User{
Email: MakeEmail(),
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{
Email: MakeEmail(),
AuthService: "ldap",
}
_, err = ss.User().Save(u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
_, err = ss.User().Update(u1, false)
require.NoError(t, err)
missing := &model.User{}
_, err = ss.User().Update(missing, false)
require.Error(t, err, "Update should have failed because of missing key")
newId := &model.User{
Id: model.NewId(),
}
_, err = ss.User().Update(newId, false)
require.Error(t, err, "Update should have failed because id change")
u2.Email = MakeEmail()
_, err = ss.User().Update(u2, false)
require.Error(t, err, "Update should have failed because you can't modify AD/LDAP fields")
u3 := &model.User{
Email: MakeEmail(),
AuthService: "gitlab",
}
oldEmail := u3.Email
_, err = ss.User().Save(u3)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u3.Id}, -1)
require.NoError(t, nErr)
u3.Email = MakeEmail()
userUpdate, err := ss.User().Update(u3, false)
require.NoError(t, err, "Update should not have failed")
assert.Equal(t, oldEmail, userUpdate.New.Email, "Email should not have been updated as the update is not trusted")
u3.Email = MakeEmail()
userUpdate, err = ss.User().Update(u3, true)
require.NoError(t, err, "Update should not have failed")
assert.NotEqual(t, oldEmail, userUpdate.New.Email, "Email should have been updated as the update is trusted")
err = ss.User().UpdateLastPictureUpdate(u1.Id)
require.NoError(t, err, "Update should not have failed")
// Test UpdateNotifyProps
u1, err = ss.User().Get(context.Background(), u1.Id)
require.NoError(t, err)
props := u1.NotifyProps
props["hello"] = "world"
err = ss.User().UpdateNotifyProps(u1.Id, props)
require.NoError(t, err)
ss.User().InvalidateProfileCacheForUser(u1.Id)
uNew, err := ss.User().Get(context.Background(), u1.Id)
require.NoError(t, err)
assert.Equal(t, props, uNew.NotifyProps)
u4 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
NotifyProps: make(map[string]string, 1),
}
maxPostSize := ss.Post().GetMaxPostSize()
u4.NotifyProps[model.AutoResponderMessageNotifyProp] = strings.Repeat("a", maxPostSize+1)
_, err = ss.User().Update(&u4, false)
require.Error(t, err, "auto responder message size should not be greater than maxPostSize")
err = ss.User().UpdateNotifyProps(u4.Id, u4.NotifyProps)
require.Error(t, err, "auto responder message size should not be greater than maxPostSize")
}
func testUserStoreUpdateUpdateAt(t *testing.T, ss store.Store) {
u1 := &model.User{}
u1.Email = MakeEmail()
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
// Ensure UpdateAt has a change to be different below.
time.Sleep(2 * time.Millisecond)
_, err = ss.User().UpdateUpdateAt(u1.Id)
require.NoError(t, err)
user, err := ss.User().Get(context.Background(), u1.Id)
require.NoError(t, err)
require.Less(t, u1.UpdateAt, user.UpdateAt, "UpdateAt not updated correctly")
}
func testUserStoreUpdateFailedPasswordAttempts(t *testing.T, ss store.Store) {
u1 := &model.User{}
u1.Email = MakeEmail()
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
err = ss.User().UpdateFailedPasswordAttempts(u1.Id, 3)
require.NoError(t, err)
user, err := ss.User().Get(context.Background(), u1.Id)
require.NoError(t, err)
require.Equal(t, 3, user.FailedAttempts, "FailedAttempts not updated correctly")
}
func testUserStoreGet(t *testing.T, ss store.Store) {
u1 := &model.User{
Email: MakeEmail(),
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2, _ := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: model.NewId(),
})
_, nErr := ss.Bot().Save(&model.Bot{
UserId: u2.Id,
Username: u2.Username,
Description: "bot description",
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u2.IsBot = true
u2.BotDescription = "bot description"
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u2.Id)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
t.Run("fetch empty id", func(t *testing.T) {
_, err := ss.User().Get(context.Background(), "")
require.Error(t, err)
})
t.Run("fetch user 1", func(t *testing.T) {
actual, err := ss.User().Get(context.Background(), u1.Id)
require.NoError(t, err)
require.Equal(t, u1, actual)
require.False(t, actual.IsBot)
})
t.Run("fetch user 2, also a bot", func(t *testing.T) {
actual, err := ss.User().Get(context.Background(), u2.Id)
require.NoError(t, err)
require.Equal(t, u2, actual)
require.True(t, actual.IsBot)
require.Equal(t, "bot description", actual.BotDescription)
})
}
func testGetAllUsingAuthService(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
AuthService: "service",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
AuthService: "service",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
AuthService: "service2",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
t.Run("get by unknown auth service", func(t *testing.T) {
users, err := ss.User().GetAllUsingAuthService("unknown")
require.NoError(t, err)
assert.Equal(t, []*model.User{}, users)
})
t.Run("get by auth service", func(t *testing.T) {
users, err := ss.User().GetAllUsingAuthService("service")
require.NoError(t, err)
assert.Equal(t, []*model.User{u1, u2}, users)
})
t.Run("get by other auth service", func(t *testing.T) {
users, err := ss.User().GetAllUsingAuthService("service2")
require.NoError(t, err)
assert.Equal(t, []*model.User{u3}, users)
})
}
func sanitized(user *model.User) *model.User {
clonedUser := user.DeepCopy()
clonedUser.Sanitize(map[string]bool{})
return clonedUser
}
func testUserStoreGetAllProfiles(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
Roles: model.SystemUserRoleId,
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
Roles: model.SystemUserRoleId,
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
_, nErr := ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
Roles: "system_user some-other-role",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
u5, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u5" + model.NewId(),
Roles: "system_admin",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u5.Id)) }()
u6, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u6" + model.NewId(),
DeleteAt: model.GetMillis(),
Roles: "system_admin",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u6.Id)) }()
u7, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u7" + model.NewId(),
DeleteAt: model.GetMillis(),
Roles: model.SystemUserRoleId,
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u7.Id)) }()
t.Run("get offset 0, limit 100", func(t *testing.T) {
options := &model.UserGetOptions{Page: 0, PerPage: 100}
actual, userErr := ss.User().GetAllProfiles(options)
require.NoError(t, userErr)
require.Equal(t, []*model.User{
sanitized(u1),
sanitized(u2),
sanitized(u3),
sanitized(u4),
sanitized(u5),
sanitized(u6),
sanitized(u7),
}, actual)
})
t.Run("get offset 0, limit 1", func(t *testing.T) {
actual, userErr := ss.User().GetAllProfiles(&model.UserGetOptions{
Page: 0,
PerPage: 1,
})
require.NoError(t, userErr)
require.Equal(t, []*model.User{
sanitized(u1),
}, actual)
})
t.Run("get all", func(t *testing.T) {
actual, userErr := ss.User().GetAll()
require.NoError(t, userErr)
require.Equal(t, []*model.User{
u1,
u2,
u3,
u4,
u5,
u6,
u7,
}, actual)
})
t.Run("etag changes for all after user creation", func(t *testing.T) {
etag := ss.User().GetEtagForAllProfiles()
uNew := &model.User{}
uNew.Email = MakeEmail()
_, userErr := ss.User().Save(uNew)
require.NoError(t, userErr)
defer func() { require.NoError(t, ss.User().PermanentDelete(uNew.Id)) }()
updatedEtag := ss.User().GetEtagForAllProfiles()
require.NotEqual(t, etag, updatedEtag)
})
t.Run("filter to system_admin role", func(t *testing.T) {
actual, userErr := ss.User().GetAllProfiles(&model.UserGetOptions{
Page: 0,
PerPage: 10,
Role: "system_admin",
})
require.NoError(t, userErr)
require.Equal(t, []*model.User{
sanitized(u5),
sanitized(u6),
}, actual)
})
t.Run("filter to system_admin role, inactive", func(t *testing.T) {
actual, userErr := ss.User().GetAllProfiles(&model.UserGetOptions{
Page: 0,
PerPage: 10,
Role: "system_admin",
Inactive: true,
})
require.NoError(t, userErr)
require.Equal(t, []*model.User{
sanitized(u6),
}, actual)
})
t.Run("filter to inactive", func(t *testing.T) {
actual, userErr := ss.User().GetAllProfiles(&model.UserGetOptions{
Page: 0,
PerPage: 10,
Inactive: true,
})
require.NoError(t, userErr)
require.Equal(t, []*model.User{
sanitized(u6),
sanitized(u7),
}, actual)
})
t.Run("filter to active", func(t *testing.T) {
actual, userErr := ss.User().GetAllProfiles(&model.UserGetOptions{
Page: 0,
PerPage: 10,
Active: true,
})
require.NoError(t, userErr)
require.Equal(t, []*model.User{
sanitized(u1),
sanitized(u2),
sanitized(u3),
sanitized(u4),
sanitized(u5),
}, actual)
})
t.Run("try to filter to active and inactive", func(t *testing.T) {
actual, userErr := ss.User().GetAllProfiles(&model.UserGetOptions{
Page: 0,
PerPage: 10,
Inactive: true,
Active: true,
})
require.NoError(t, userErr)
require.Equal(t, []*model.User{
sanitized(u6),
sanitized(u7),
}, actual)
})
u8, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u8" + model.NewId(),
DeleteAt: model.GetMillis(),
Roles: "system_user_manager system_user",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u8.Id)) }()
u9, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u9" + model.NewId(),
DeleteAt: model.GetMillis(),
Roles: "system_manager system_user",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u9.Id)) }()
u10, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u10" + model.NewId(),
DeleteAt: model.GetMillis(),
Roles: "system_read_only_admin system_user",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u10.Id)) }()
t.Run("filter by system_user_manager role", func(t *testing.T) {
actual, userErr := ss.User().GetAllProfiles(&model.UserGetOptions{
Page: 0,
PerPage: 10,
Roles: []string{"system_user_manager"},
})
require.NoError(t, userErr)
require.Equal(t, []*model.User{
sanitized(u8),
}, actual)
})
t.Run("filter by multiple system roles", func(t *testing.T) {
actual, userErr := ss.User().GetAllProfiles(&model.UserGetOptions{
Page: 0,
PerPage: 10,
Roles: []string{"system_manager", "system_user_manager", "system_read_only_admin", "system_admin"},
})
require.NoError(t, userErr)
require.Equal(t, []*model.User{
sanitized(u10),
sanitized(u5),
sanitized(u6),
sanitized(u8),
sanitized(u9),
}, actual)
})
t.Run("filter by system_user only", func(t *testing.T) {
actual, userErr := ss.User().GetAllProfiles(&model.UserGetOptions{
Page: 0,
PerPage: 10,
Roles: []string{"system_user"},
})
require.NoError(t, userErr)
require.Equal(t, []*model.User{
sanitized(u1),
sanitized(u2),
sanitized(u7),
}, actual)
})
}
func testUserStoreGetProfiles(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
Roles: "system_admin",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u4.Id}, -1)
require.NoError(t, nErr)
u5, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u5" + model.NewId(),
DeleteAt: model.GetMillis(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u5.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u5.Id}, -1)
require.NoError(t, nErr)
t.Run("get page 0, perPage 100", func(t *testing.T) {
actual, err := ss.User().GetProfiles(&model.UserGetOptions{
InTeamId: teamId,
Page: 0,
PerPage: 100,
})
require.NoError(t, err)
require.Equal(t, []*model.User{
sanitized(u1),
sanitized(u2),
sanitized(u3),
sanitized(u4),
sanitized(u5),
}, actual)
})
t.Run("get page 0, perPage 1", func(t *testing.T) {
actual, err := ss.User().GetProfiles(&model.UserGetOptions{
InTeamId: teamId,
Page: 0,
PerPage: 1,
})
require.NoError(t, err)
require.Equal(t, []*model.User{sanitized(u1)}, actual)
})
t.Run("get unknown team id", func(t *testing.T) {
actual, err := ss.User().GetProfiles(&model.UserGetOptions{
InTeamId: "123",
Page: 0,
PerPage: 100,
})
require.NoError(t, err)
require.Equal(t, []*model.User{}, actual)
})
t.Run("etag changes for all after user creation", func(t *testing.T) {
etag := ss.User().GetEtagForProfiles(teamId)
uNew := &model.User{}
uNew.Email = MakeEmail()
_, err := ss.User().Save(uNew)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(uNew.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: uNew.Id}, -1)
require.NoError(t, nErr)
updatedEtag := ss.User().GetEtagForProfiles(teamId)
require.NotEqual(t, etag, updatedEtag)
})
t.Run("filter to system_admin role", func(t *testing.T) {
actual, err := ss.User().GetProfiles(&model.UserGetOptions{
InTeamId: teamId,
Page: 0,
PerPage: 10,
Role: "system_admin",
})
require.NoError(t, err)
require.Equal(t, []*model.User{
sanitized(u4),
}, actual)
})
t.Run("filter to inactive", func(t *testing.T) {
actual, err := ss.User().GetProfiles(&model.UserGetOptions{
InTeamId: teamId,
Page: 0,
PerPage: 10,
Inactive: true,
})
require.NoError(t, err)
require.Equal(t, []*model.User{
sanitized(u5),
}, actual)
})
t.Run("filter to active", func(t *testing.T) {
actual, err := ss.User().GetProfiles(&model.UserGetOptions{
InTeamId: teamId,
Page: 0,
PerPage: 10,
Active: true,
})
require.NoError(t, err)
require.Equal(t, []*model.User{
sanitized(u1),
sanitized(u2),
sanitized(u3),
sanitized(u4),
}, actual)
})
t.Run("try to filter to active and inactive", func(t *testing.T) {
actual, err := ss.User().GetProfiles(&model.UserGetOptions{
InTeamId: teamId,
Page: 0,
PerPage: 10,
Inactive: true,
Active: true,
})
require.NoError(t, err)
require.Equal(t, []*model.User{
sanitized(u5),
}, actual)
})
}
func testUserStoreGetProfilesInChannel(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u4.Id}, -1)
require.NoError(t, nErr)
ch1 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in channel",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypeOpen,
}
c1, nErr := ss.Channel().Save(ch1, -1)
require.NoError(t, nErr)
ch2 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in private",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypePrivate,
}
c2, nErr := ss.Channel().Save(ch2, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u4.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
u4.DeleteAt = 1
_, err = ss.User().Update(u4, true)
require.NoError(t, err)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c2.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
t.Run("get all users in channel 1, offset 0, limit 100", func(t *testing.T) {
users, err := ss.User().GetProfilesInChannel(&model.UserGetOptions{
InChannelId: c1.Id,
Page: 0,
PerPage: 100,
})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u1), sanitized(u2), sanitized(u3), sanitized(u4)}, users)
})
t.Run("get only active users in channel 1, offset 0, limit 100", func(t *testing.T) {
users, err := ss.User().GetProfilesInChannel(&model.UserGetOptions{
InChannelId: c1.Id,
Page: 0,
PerPage: 100,
Active: true,
})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u1), sanitized(u2), sanitized(u3)}, users)
})
t.Run("get inactive users in channel 1, offset 0, limit 100", func(t *testing.T) {
users, err := ss.User().GetProfilesInChannel(&model.UserGetOptions{
InChannelId: c1.Id,
Page: 0,
PerPage: 100,
Inactive: true,
})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u4)}, users)
})
t.Run("get in channel 1, offset 1, limit 2", func(t *testing.T) {
users, err := ss.User().GetProfilesInChannel(&model.UserGetOptions{
InChannelId: c1.Id,
Page: 1,
PerPage: 1,
})
require.NoError(t, err)
users_p2, err2 := ss.User().GetProfilesInChannel(&model.UserGetOptions{
InChannelId: c1.Id,
Page: 2,
PerPage: 1,
})
require.NoError(t, err2)
users = append(users, users_p2...)
assert.Equal(t, []*model.User{sanitized(u2), sanitized(u3)}, users)
})
t.Run("get in channel 2, offset 0, limit 1", func(t *testing.T) {
users, err := ss.User().GetProfilesInChannel(&model.UserGetOptions{
InChannelId: c2.Id,
Page: 0,
PerPage: 1,
})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u1)}, users)
})
t.Run("Filter by channel members and channel admins", func(t *testing.T) {
// save admin for c1
user2Admin, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "bbb" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user2Admin.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user2Admin.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: user2Admin.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
ExplicitRoles: "channel_admin",
})
require.NoError(t, nErr)
ss.Channel().UpdateMembersRole(c1.Id, []string{user2Admin.Id})
users, err := ss.User().GetProfilesInChannel(&model.UserGetOptions{
InChannelId: c1.Id,
ChannelRoles: []string{model.ChannelAdminRoleId},
Page: 0,
PerPage: 5,
})
require.NoError(t, err)
assert.Equal(t, user2Admin.Id, users[0].Id)
})
}
func testUserStoreGetProfilesInChannelByAdmin(t *testing.T, ss store.Store, s SqlStore) {
cleanupStatusStore(t, s)
teamId := model.NewId()
user1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "aaa" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user1.Id}, -1)
require.NoError(t, nErr)
user2Admin, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "bbb" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user2Admin.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user2Admin.Id}, -1)
require.NoError(t, nErr)
user3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "ccc" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user3.Id}, -1)
require.NoError(t, nErr)
ch1 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in channel by admin",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypeOpen,
}
c1, nErr := ss.Channel().Save(ch1, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: user1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: user2Admin.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
ExplicitRoles: "channel_admin",
})
require.NoError(t, nErr)
ss.Channel().UpdateMembersRole(c1.Id, []string{user2Admin.Id})
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: user3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
t.Run("get users in admin, offset 0, limit 100", func(t *testing.T) {
users, err := ss.User().GetProfilesInChannelByAdmin(&model.UserGetOptions{
InChannelId: c1.Id,
Page: 0,
PerPage: 100,
})
require.NoError(t, err)
require.Len(t, users, 3)
require.Equal(t, user2Admin.Username, users[0].Username)
require.Equal(t, user1.Username, users[1].Username)
require.Equal(t, user3.Username, users[2].Username)
})
}
func testUserStoreGetProfilesInChannelByStatus(t *testing.T, ss store.Store, s SqlStore) {
cleanupStatusStore(t, s)
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u4.Id}, -1)
require.NoError(t, nErr)
ch1 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in channel",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypeOpen,
}
c1, nErr := ss.Channel().Save(ch1, -1)
require.NoError(t, nErr)
ch2 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in private",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypePrivate,
}
c2, nErr := ss.Channel().Save(ch2, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u4.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
u4.DeleteAt = 1
_, err = ss.User().Update(u4, true)
require.NoError(t, err)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c2.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{
UserId: u1.Id,
Status: model.StatusDnd,
}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{
UserId: u2.Id,
Status: model.StatusAway,
}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{
UserId: u3.Id,
Status: model.StatusOnline,
}))
t.Run("get all users in channel 1, offset 0, limit 100", func(t *testing.T) {
users, err := ss.User().GetProfilesInChannel(&model.UserGetOptions{
InChannelId: c1.Id,
Page: 0,
PerPage: 100,
})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u1), sanitized(u2), sanitized(u3), sanitized(u4)}, users)
})
t.Run("get active in channel 1 by status, offset 0, limit 100", func(t *testing.T) {
users, err := ss.User().GetProfilesInChannelByStatus(&model.UserGetOptions{
InChannelId: c1.Id,
Page: 0,
PerPage: 100,
Active: true,
})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u3), sanitized(u2), sanitized(u1)}, users)
})
t.Run("get inactive users in channel 1, offset 0, limit 100", func(t *testing.T) {
users, err := ss.User().GetProfilesInChannel(&model.UserGetOptions{
InChannelId: c1.Id,
Page: 0,
PerPage: 100,
Inactive: true,
})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u4)}, users)
})
t.Run("get in channel 2 by status, offset 0, limit 1", func(t *testing.T) {
users, err := ss.User().GetProfilesInChannelByStatus(&model.UserGetOptions{
InChannelId: c2.Id,
Page: 0,
PerPage: 1,
})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u1)}, users)
})
}
func testUserStoreGetProfilesWithoutTeam(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
DeleteAt: 1,
Roles: "system_admin",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
t.Run("get, page 0, per_page 100", func(t *testing.T) {
users, err := ss.User().GetProfilesWithoutTeam(&model.UserGetOptions{Page: 0, PerPage: 100})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u2), sanitized(u3)}, users)
})
t.Run("get, page 1, per_page 1", func(t *testing.T) {
users, err := ss.User().GetProfilesWithoutTeam(&model.UserGetOptions{Page: 1, PerPage: 1})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u3)}, users)
})
t.Run("get, page 2, per_page 1", func(t *testing.T) {
users, err := ss.User().GetProfilesWithoutTeam(&model.UserGetOptions{Page: 2, PerPage: 1})
require.NoError(t, err)
assert.Equal(t, []*model.User{}, users)
})
t.Run("get, page 0, per_page 100, inactive", func(t *testing.T) {
users, err := ss.User().GetProfilesWithoutTeam(&model.UserGetOptions{Page: 0, PerPage: 100, Inactive: true})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u3)}, users)
})
t.Run("get, page 0, per_page 100, role", func(t *testing.T) {
users, err := ss.User().GetProfilesWithoutTeam(&model.UserGetOptions{Page: 0, PerPage: 100, Role: "system_admin"})
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u3)}, users)
})
}
func testUserStoreGetAllProfilesInChannel(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
ch1 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in channel",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypeOpen,
}
c1, nErr := ss.Channel().Save(ch1, -1)
require.NoError(t, nErr)
ch2 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in private",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypePrivate,
}
c2, nErr := ss.Channel().Save(ch2, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c2.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
t.Run("all profiles in channel 1, no caching", func(t *testing.T) {
var profiles map[string]*model.User
profiles, err = ss.User().GetAllProfilesInChannel(context.Background(), c1.Id, false)
require.NoError(t, err)
assert.Equal(t, map[string]*model.User{
u1.Id: sanitized(u1),
u2.Id: sanitized(u2),
u3.Id: sanitized(u3),
}, profiles)
})
t.Run("all profiles in channel 2, no caching", func(t *testing.T) {
var profiles map[string]*model.User
profiles, err = ss.User().GetAllProfilesInChannel(context.Background(), c2.Id, false)
require.NoError(t, err)
assert.Equal(t, map[string]*model.User{
u1.Id: sanitized(u1),
}, profiles)
})
t.Run("all profiles in channel 2, caching", func(t *testing.T) {
var profiles map[string]*model.User
profiles, err = ss.User().GetAllProfilesInChannel(context.Background(), c2.Id, true)
require.NoError(t, err)
assert.Equal(t, map[string]*model.User{
u1.Id: sanitized(u1),
}, profiles)
})
t.Run("all profiles in channel 2, caching [repeated]", func(t *testing.T) {
var profiles map[string]*model.User
profiles, err = ss.User().GetAllProfilesInChannel(context.Background(), c2.Id, true)
require.NoError(t, err)
assert.Equal(t, map[string]*model.User{
u1.Id: sanitized(u1),
}, profiles)
})
ss.User().InvalidateProfilesInChannelCacheByUser(u1.Id)
ss.User().InvalidateProfilesInChannelCache(c2.Id)
}
func testUserStoreGetProfilesNotInChannel(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
ch1 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in channel",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypeOpen,
}
c1, nErr := ss.Channel().Save(ch1, -1)
require.NoError(t, nErr)
ch2 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in private",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypePrivate,
}
c2, nErr := ss.Channel().Save(ch2, -1)
require.NoError(t, nErr)
t.Run("get team 1, channel 1, offset 0, limit 100", func(t *testing.T) {
var profiles []*model.User
profiles, err = ss.User().GetProfilesNotInChannel(teamId, c1.Id, false, 0, 100, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u1),
sanitized(u2),
sanitized(u3),
}, profiles)
})
t.Run("get team 1, channel 2, offset 0, limit 100", func(t *testing.T) {
var profiles []*model.User
profiles, err = ss.User().GetProfilesNotInChannel(teamId, c2.Id, false, 0, 100, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u1),
sanitized(u2),
sanitized(u3),
}, profiles)
})
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c2.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
t.Run("get team 1, channel 1, offset 0, limit 100, after update", func(t *testing.T) {
var profiles []*model.User
profiles, err = ss.User().GetProfilesNotInChannel(teamId, c1.Id, false, 0, 100, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{}, profiles)
})
t.Run("get team 1, channel 2, offset 0, limit 100, after update", func(t *testing.T) {
var profiles []*model.User
profiles, err = ss.User().GetProfilesNotInChannel(teamId, c2.Id, false, 0, 100, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u2),
sanitized(u3),
}, profiles)
})
t.Run("get team 1, channel 2, offset 0, limit 0, setting group constrained when it's not", func(t *testing.T) {
var profiles []*model.User
profiles, err = ss.User().GetProfilesNotInChannel(teamId, c2.Id, true, 0, 100, nil)
require.NoError(t, err)
assert.Empty(t, profiles)
})
// create a group
group, err := ss.Group().Create(&model.Group{
Name: model.NewString("n_" + model.NewId()),
DisplayName: "dn_" + model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString("ri_" + model.NewId()),
})
require.NoError(t, err)
// add two members to the group
for _, u := range []*model.User{u1, u2} {
_, err = ss.Group().UpsertMember(group.Id, u.Id)
require.NoError(t, err)
}
// associate the group with the channel
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
GroupId: group.Id,
SyncableId: c2.Id,
Type: model.GroupSyncableTypeChannel,
})
require.NoError(t, err)
t.Run("get team 1, channel 2, offset 0, limit 0, setting group constrained", func(t *testing.T) {
profiles, err := ss.User().GetProfilesNotInChannel(teamId, c2.Id, true, 0, 100, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u2),
}, profiles)
})
}
func testUserStoreGetProfilesByIds(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
time.Sleep(time.Millisecond)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
t.Run("get u1 by id, no caching", func(t *testing.T) {
users, err := ss.User().GetProfileByIds(context.Background(), []string{u1.Id}, nil, false)
require.NoError(t, err)
assert.Equal(t, []*model.User{u1}, users)
})
t.Run("get u1 by id, caching", func(t *testing.T) {
users, err := ss.User().GetProfileByIds(context.Background(), []string{u1.Id}, nil, true)
require.NoError(t, err)
assert.Equal(t, []*model.User{u1}, users)
})
t.Run("get u1, u2, u3 by id, no caching", func(t *testing.T) {
users, err := ss.User().GetProfileByIds(context.Background(), []string{u1.Id, u2.Id, u3.Id}, nil, false)
require.NoError(t, err)
assert.Equal(t, []*model.User{u1, u2, u3}, users)
})
t.Run("get u1, u2, u3 by id, caching", func(t *testing.T) {
users, err := ss.User().GetProfileByIds(context.Background(), []string{u1.Id, u2.Id, u3.Id}, nil, true)
require.NoError(t, err)
assert.Equal(t, []*model.User{u1, u2, u3}, users)
})
t.Run("get unknown id, caching", func(t *testing.T) {
users, err := ss.User().GetProfileByIds(context.Background(), []string{"123"}, nil, true)
require.NoError(t, err)
assert.Equal(t, []*model.User{}, users)
})
t.Run("should only return users with UpdateAt greater than the since time", func(t *testing.T) {
users, err := ss.User().GetProfileByIds(context.Background(), []string{u1.Id, u2.Id, u3.Id, u4.Id}, &store.UserGetByIdsOpts{
Since: u2.CreateAt,
}, true)
require.NoError(t, err)
// u3 comes from the cache, and u4 does not
assert.Equal(t, []*model.User{u3, u4}, users)
})
}
func testUserStoreGetProfileByGroupChannelIdsForUser(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
gc1, nErr := ss.Channel().Save(&model.Channel{
DisplayName: "Profiles in private",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypeGroup,
}, -1)
require.NoError(t, nErr)
for _, uId := range []string{u1.Id, u2.Id, u3.Id} {
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: gc1.Id,
UserId: uId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
}
gc2, nErr := ss.Channel().Save(&model.Channel{
DisplayName: "Profiles in private",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypeGroup,
}, -1)
require.NoError(t, nErr)
for _, uId := range []string{u1.Id, u3.Id, u4.Id} {
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: gc2.Id,
UserId: uId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
}
testCases := []struct {
Name string
UserId string
ChannelIds []string
ExpectedUserIdsByChannel map[string][]string
EnsureChannelsNotInResults []string
}{
{
Name: "Get group 1 as user 1",
UserId: u1.Id,
ChannelIds: []string{gc1.Id},
ExpectedUserIdsByChannel: map[string][]string{
gc1.Id: {u2.Id, u3.Id},
},
EnsureChannelsNotInResults: []string{},
},
{
Name: "Get groups 1 and 2 as user 1",
UserId: u1.Id,
ChannelIds: []string{gc1.Id, gc2.Id},
ExpectedUserIdsByChannel: map[string][]string{
gc1.Id: {u2.Id, u3.Id},
gc2.Id: {u3.Id, u4.Id},
},
EnsureChannelsNotInResults: []string{},
},
{
Name: "Get groups 1 and 2 as user 2",
UserId: u2.Id,
ChannelIds: []string{gc1.Id, gc2.Id},
ExpectedUserIdsByChannel: map[string][]string{
gc1.Id: {u1.Id, u3.Id},
},
EnsureChannelsNotInResults: []string{gc2.Id},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
res, err := ss.User().GetProfileByGroupChannelIdsForUser(tc.UserId, tc.ChannelIds)
require.NoError(t, err)
for channelId, expectedUsers := range tc.ExpectedUserIdsByChannel {
users, ok := res[channelId]
require.True(t, ok)
var userIds []string
for _, user := range users {
userIds = append(userIds, user.Id)
}
require.ElementsMatch(t, expectedUsers, userIds)
}
for _, channelId := range tc.EnsureChannelsNotInResults {
_, ok := res[channelId]
require.False(t, ok)
}
})
}
}
func testUserStoreGetProfilesByUsernames(t *testing.T, ss store.Store) {
teamId := model.NewId()
team2Id := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: team2Id, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
t.Run("get by u1 and u2 usernames, team id 1", func(t *testing.T) {
users, err := ss.User().GetProfilesByUsernames([]string{u1.Username, u2.Username}, &model.ViewUsersRestrictions{Teams: []string{teamId}})
require.NoError(t, err)
assert.Equal(t, []*model.User{u1, u2}, users)
})
t.Run("get by u1 username, team id 1", func(t *testing.T) {
users, err := ss.User().GetProfilesByUsernames([]string{u1.Username}, &model.ViewUsersRestrictions{Teams: []string{teamId}})
require.NoError(t, err)
assert.Equal(t, []*model.User{u1}, users)
})
t.Run("get by u1 and u3 usernames, no team id", func(t *testing.T) {
users, err := ss.User().GetProfilesByUsernames([]string{u1.Username, u3.Username}, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{u1, u3}, users)
})
t.Run("get by u1 and u3 usernames, team id 1", func(t *testing.T) {
users, err := ss.User().GetProfilesByUsernames([]string{u1.Username, u3.Username}, &model.ViewUsersRestrictions{Teams: []string{teamId}})
require.NoError(t, err)
assert.Equal(t, []*model.User{u1}, users)
})
t.Run("get by u1 and u3 usernames, team id 2", func(t *testing.T) {
users, err := ss.User().GetProfilesByUsernames([]string{u1.Username, u3.Username}, &model.ViewUsersRestrictions{Teams: []string{team2Id}})
require.NoError(t, err)
assert.Equal(t, []*model.User{u3}, users)
})
}
func testUserStoreGetSystemAdminProfiles(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Roles: model.SystemUserRoleId + " " + model.SystemAdminRoleId,
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Roles: model.SystemUserRoleId + " " + model.SystemAdminRoleId,
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
t.Run("all system admin profiles", func(t *testing.T) {
result, userError := ss.User().GetSystemAdminProfiles()
require.NoError(t, userError)
assert.Equal(t, map[string]*model.User{
u1.Id: sanitized(u1),
u3.Id: sanitized(u3),
}, result)
})
}
func testUserStoreGetByEmail(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
t.Run("get u1 by email", func(t *testing.T) {
u, err := ss.User().GetByEmail(u1.Email)
require.NoError(t, err)
assert.Equal(t, u1, u)
})
t.Run("get u2 by email", func(t *testing.T) {
u, err := ss.User().GetByEmail(u2.Email)
require.NoError(t, err)
assert.Equal(t, u2, u)
})
t.Run("get u3 by email", func(t *testing.T) {
u, err := ss.User().GetByEmail(u3.Email)
require.NoError(t, err)
assert.Equal(t, u3, u)
})
t.Run("get by empty email", func(t *testing.T) {
_, err := ss.User().GetByEmail("")
require.Error(t, err)
})
t.Run("get by unknown", func(t *testing.T) {
_, err := ss.User().GetByEmail("unknown")
require.Error(t, err)
})
}
func testUserStoreGetByAuthData(t *testing.T, ss store.Store) {
teamId := model.NewId()
auth1 := model.NewId()
auth3 := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
AuthData: &auth1,
AuthService: "service",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
AuthData: &auth3,
AuthService: "service2",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
t.Run("get by u1 auth", func(t *testing.T) {
u, err := ss.User().GetByAuth(u1.AuthData, u1.AuthService)
require.NoError(t, err)
assert.Equal(t, u1, u)
})
t.Run("get by u3 auth", func(t *testing.T) {
u, err := ss.User().GetByAuth(u3.AuthData, u3.AuthService)
require.NoError(t, err)
assert.Equal(t, u3, u)
})
t.Run("get by u1 auth, unknown service", func(t *testing.T) {
_, err := ss.User().GetByAuth(u1.AuthData, "unknown")
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
})
t.Run("get by unknown auth, u1 service", func(t *testing.T) {
unknownAuth := ""
_, err := ss.User().GetByAuth(&unknownAuth, u1.AuthService)
require.Error(t, err)
var invErr *store.ErrInvalidInput
require.True(t, errors.As(err, &invErr))
})
t.Run("get by unknown auth, unknown service", func(t *testing.T) {
unknownAuth := ""
_, err := ss.User().GetByAuth(&unknownAuth, "unknown")
require.Error(t, err)
var invErr *store.ErrInvalidInput
require.True(t, errors.As(err, &invErr))
})
}
func testUserStoreGetByUsername(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
t.Run("get u1 by username", func(t *testing.T) {
result, err := ss.User().GetByUsername(u1.Username)
require.NoError(t, err)
assert.Equal(t, u1, result)
})
t.Run("get u2 by username", func(t *testing.T) {
result, err := ss.User().GetByUsername(u2.Username)
require.NoError(t, err)
assert.Equal(t, u2, result)
})
t.Run("get u3 by username", func(t *testing.T) {
result, err := ss.User().GetByUsername(u3.Username)
require.NoError(t, err)
assert.Equal(t, u3, result)
})
t.Run("get by empty username", func(t *testing.T) {
_, err := ss.User().GetByUsername("")
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
})
t.Run("get by unknown", func(t *testing.T) {
_, err := ss.User().GetByUsername("unknown")
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
})
}
func testUserStoreGetForLogin(t *testing.T, ss store.Store) {
teamId := model.NewId()
auth := model.NewId()
auth2 := model.NewId()
auth3 := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
AuthService: model.UserAuthServiceGitlab,
AuthData: &auth,
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
AuthService: model.UserAuthServiceLdap,
AuthData: &auth2,
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
AuthService: model.UserAuthServiceLdap,
AuthData: &auth3,
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
t.Run("get u1 by username, allow both", func(t *testing.T) {
user, err := ss.User().GetForLogin(u1.Username, true, true)
require.NoError(t, err)
assert.Equal(t, u1, user)
})
t.Run("get u1 by username, check for case issues", func(t *testing.T) {
user, err := ss.User().GetForLogin(strings.ToUpper(u1.Username), true, true)
require.NoError(t, err)
assert.Equal(t, u1, user)
})
t.Run("get u1 by username, allow only email", func(t *testing.T) {
_, err := ss.User().GetForLogin(u1.Username, false, true)
require.Error(t, err)
require.Equal(t, "user not found", err.Error())
})
t.Run("get u1 by email, allow both", func(t *testing.T) {
user, err := ss.User().GetForLogin(u1.Email, true, true)
require.NoError(t, err)
assert.Equal(t, u1, user)
})
t.Run("get u1 by email, check for case issues", func(t *testing.T) {
user, err := ss.User().GetForLogin(strings.ToUpper(u1.Email), true, true)
require.NoError(t, err)
assert.Equal(t, u1, user)
})
t.Run("get u1 by email, allow only username", func(t *testing.T) {
_, err := ss.User().GetForLogin(u1.Email, true, false)
require.Error(t, err)
require.Equal(t, "user not found", err.Error())
})
t.Run("get u2 by username, allow both", func(t *testing.T) {
user, err := ss.User().GetForLogin(u2.Username, true, true)
require.NoError(t, err)
assert.Equal(t, u2, user)
})
t.Run("get u2 by email, allow both", func(t *testing.T) {
user, err := ss.User().GetForLogin(u2.Email, true, true)
require.NoError(t, err)
assert.Equal(t, u2, user)
})
t.Run("get u2 by username, allow neither", func(t *testing.T) {
_, err := ss.User().GetForLogin(u2.Username, false, false)
require.Error(t, err)
require.Equal(t, "sign in with username and email are disabled", err.Error())
})
}
func testUserStoreUpdatePassword(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1 := &model.User{}
u1.Email = MakeEmail()
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
hashedPassword := model.HashPassword("newpwd")
err = ss.User().UpdatePassword(u1.Id, hashedPassword)
require.NoError(t, err)
user, err := ss.User().GetByEmail(u1.Email)
require.NoError(t, err)
require.Equal(t, user.Password, hashedPassword, "Password was not updated correctly")
}
func testUserStoreDelete(t *testing.T, ss store.Store) {
u1 := &model.User{}
u1.Email = MakeEmail()
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
err = ss.User().PermanentDelete(u1.Id)
require.NoError(t, err)
}
func testUserStoreUpdateAuthData(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1 := &model.User{}
u1.Email = MakeEmail()
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
service := "someservice"
authData := model.NewId()
_, err = ss.User().UpdateAuthData(u1.Id, service, &authData, "", true)
require.NoError(t, err)
user, err := ss.User().GetByEmail(u1.Email)
require.NoError(t, err)
require.Equal(t, service, user.AuthService, "AuthService was not updated correctly")
require.Equal(t, authData, *user.AuthData, "AuthData was not updated correctly")
require.Equal(t, "", user.Password, "Password was not cleared properly")
}
func testUserStoreResetAuthDataToEmailForUsers(t *testing.T, ss store.Store) {
user := &model.User{}
user.Username = "user1" + model.NewId()
user.Email = MakeEmail()
_, err := ss.User().Save(user)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
resetAuthDataToID := func() {
_, err = ss.User().UpdateAuthData(
user.Id, model.UserAuthServiceSaml, model.NewString("some-id"), "", false)
require.NoError(t, err)
}
resetAuthDataToID()
// dry run
numAffected, err := ss.User().ResetAuthDataToEmailForUsers(model.UserAuthServiceSaml, nil, false, true)
require.NoError(t, err)
require.Equal(t, 1, numAffected)
// real run
numAffected, err = ss.User().ResetAuthDataToEmailForUsers(model.UserAuthServiceSaml, nil, false, false)
require.NoError(t, err)
require.Equal(t, 1, numAffected)
user, appErr := ss.User().Get(context.Background(), user.Id)
require.NoError(t, appErr)
require.Equal(t, *user.AuthData, user.Email)
resetAuthDataToID()
// with specific user IDs
numAffected, err = ss.User().ResetAuthDataToEmailForUsers(model.UserAuthServiceSaml, []string{model.NewId()}, false, true)
require.NoError(t, err)
require.Equal(t, 0, numAffected)
numAffected, err = ss.User().ResetAuthDataToEmailForUsers(model.UserAuthServiceSaml, []string{user.Id}, false, true)
require.NoError(t, err)
require.Equal(t, 1, numAffected)
// delete user
user.DeleteAt = model.GetMillisForTime(time.Now())
ss.User().Update(user, true)
// without deleted user
numAffected, err = ss.User().ResetAuthDataToEmailForUsers(model.UserAuthServiceSaml, nil, false, true)
require.NoError(t, err)
require.Equal(t, 0, numAffected)
// with deleted user
numAffected, err = ss.User().ResetAuthDataToEmailForUsers(model.UserAuthServiceSaml, nil, true, true)
require.NoError(t, err)
require.Equal(t, 1, numAffected)
}
func testUserUnreadCount(t *testing.T, ss store.Store) {
teamId := model.NewId()
c1 := model.Channel{}
c1.TeamId = teamId
c1.DisplayName = "Unread Messages"
c1.Name = "unread-messages-" + model.NewId()
c1.Type = model.ChannelTypeOpen
c2 := model.Channel{}
c2.TeamId = teamId
c2.DisplayName = "Unread Direct"
c2.Name = "unread-direct-" + model.NewId()
c2.Type = model.ChannelTypeDirect
u1 := &model.User{}
u1.Username = "user1" + model.NewId()
u1.Email = MakeEmail()
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Username = "user2" + model.NewId()
_, err = ss.User().Save(u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3 := &model.User{}
u3.Email = MakeEmail()
u3.Username = "user3" + model.NewId()
_, err = ss.User().Save(u3)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().Save(&c1, -1)
require.NoError(t, nErr, "couldn't save item")
m1 := model.ChannelMember{}
m1.ChannelId = c1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = c1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, nErr = ss.Channel().SaveMember(&m2)
require.NoError(t, nErr)
m3 := model.ChannelMember{}
m3.ChannelId = c1.Id
m3.UserId = u3.Id
m3.NotifyProps = model.GetDefaultChannelNotifyProps()
_, nErr = ss.Channel().SaveMember(&m3)
require.NoError(t, nErr)
m1.ChannelId = c2.Id
m2.ChannelId = c2.Id
_, nErr = ss.Channel().SaveDirectChannel(&c2, &m1, &m2)
require.NoError(t, nErr, "couldn't save direct channel")
p1 := model.Post{}
p1.ChannelId = c1.Id
p1.UserId = u1.Id
p1.Message = "this is a message for @" + u2.Username + " and " + "@" + u3.Username
// Post one message with mention to open channel
_, nErr = ss.Post().Save(&p1)
require.NoError(t, nErr)
nErr = ss.Channel().IncrementMentionCount(c1.Id, []string{u2.Id, u3.Id}, false, false)
require.NoError(t, nErr)
// Post 2 messages without mention to direct channel
p2 := model.Post{}
p2.ChannelId = c2.Id
p2.UserId = u1.Id
p2.Message = "first message"
_, nErr = ss.Post().Save(&p2)
require.NoError(t, nErr)
nErr = ss.Channel().IncrementMentionCount(c2.Id, []string{u2.Id}, false, false)
require.NoError(t, nErr)
p3 := model.Post{}
p3.ChannelId = c2.Id
p3.UserId = u1.Id
p3.Message = "second message"
_, nErr = ss.Post().Save(&p3)
require.NoError(t, nErr)
nErr = ss.Channel().IncrementMentionCount(c2.Id, []string{u2.Id}, false, false)
require.NoError(t, nErr)
badge, unreadCountErr := ss.User().GetUnreadCount(u2.Id, false)
require.NoError(t, unreadCountErr)
require.Equal(t, int64(3), badge, "should have 3 unread messages")
badge, unreadCountErr = ss.User().GetUnreadCount(u3.Id, false)
require.NoError(t, unreadCountErr)
require.Equal(t, int64(1), badge, "should have 1 unread message")
// Increment root mentions by 1
nErr = ss.Channel().IncrementMentionCount(c1.Id, []string{u3.Id}, true, false)
require.NoError(t, nErr)
// CRT is enabled, only root mentions are counted
badge, unreadCountErr = ss.User().GetUnreadCount(u3.Id, true)
require.NoError(t, unreadCountErr)
require.Equal(t, int64(1), badge, "should have 1 unread message with CRT")
badge, unreadCountErr = ss.User().GetUnreadCountForChannel(u2.Id, c1.Id)
require.NoError(t, unreadCountErr)
require.Equal(t, int64(1), badge, "should have 1 unread messages for that channel")
badge, unreadCountErr = ss.User().GetUnreadCountForChannel(u2.Id, c2.Id)
require.NoError(t, unreadCountErr)
require.Equal(t, int64(2), badge, "should have 2 unread messages for that channel")
}
func testUserStoreUpdateMfaSecret(t *testing.T, ss store.Store) {
u1 := model.User{}
u1.Email = MakeEmail()
_, err := ss.User().Save(&u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
err = ss.User().UpdateMfaSecret(u1.Id, "12345")
require.NoError(t, err)
// should pass, no update will occur though
err = ss.User().UpdateMfaSecret("junk", "12345")
require.NoError(t, err)
}
func testUserStoreUpdateMfaActive(t *testing.T, ss store.Store) {
u1 := model.User{}
u1.Email = MakeEmail()
_, err := ss.User().Save(&u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
time.Sleep(time.Millisecond)
err = ss.User().UpdateMfaActive(u1.Id, true)
require.NoError(t, err)
err = ss.User().UpdateMfaActive(u1.Id, false)
require.NoError(t, err)
// should pass, no update will occur though
err = ss.User().UpdateMfaActive("junk", true)
require.NoError(t, err)
}
func testUserStoreGetRecentlyActiveUsersForTeam(t *testing.T, ss store.Store, s SqlStore) {
cleanupStatusStore(t, s)
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
millis := model.GetMillis()
u3.LastActivityAt = millis
u2.LastActivityAt = millis - 1
u1.LastActivityAt = millis - 1
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u1.Id, Status: model.StatusOnline, Manual: false, LastActivityAt: u1.LastActivityAt, ActiveChannel: ""}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u2.Id, Status: model.StatusOnline, Manual: false, LastActivityAt: u2.LastActivityAt, ActiveChannel: ""}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u3.Id, Status: model.StatusOnline, Manual: false, LastActivityAt: u3.LastActivityAt, ActiveChannel: ""}))
t.Run("get team 1, offset 0, limit 100", func(t *testing.T) {
users, err := ss.User().GetRecentlyActiveUsersForTeam(teamId, 0, 100, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u3),
sanitized(u1),
sanitized(u2),
}, users)
})
t.Run("get team 1, offset 0, limit 1", func(t *testing.T) {
users, err := ss.User().GetRecentlyActiveUsersForTeam(teamId, 0, 1, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u3),
}, users)
})
t.Run("get team 1, offset 2, limit 1", func(t *testing.T) {
users, err := ss.User().GetRecentlyActiveUsersForTeam(teamId, 2, 1, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u2),
}, users)
})
}
func testUserStoreGetNewUsersForTeam(t *testing.T, ss store.Store) {
teamId := model.NewId()
teamId2 := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "Yuka",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "Leia",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "Ali",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId2, UserId: u4.Id}, -1)
require.NoError(t, nErr)
t.Run("get team 1, offset 0, limit 100", func(t *testing.T) {
result, err := ss.User().GetNewUsersForTeam(teamId, 0, 100, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u3),
sanitized(u2),
sanitized(u1),
}, result)
})
t.Run("get team 1, offset 0, limit 1", func(t *testing.T) {
result, err := ss.User().GetNewUsersForTeam(teamId, 0, 1, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u3),
}, result)
})
t.Run("get team 1, offset 2, limit 1", func(t *testing.T) {
result, err := ss.User().GetNewUsersForTeam(teamId, 2, 1, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u1),
}, result)
})
t.Run("get team 2, offset 0, limit 100", func(t *testing.T) {
result, err := ss.User().GetNewUsersForTeam(teamId2, 0, 100, nil)
require.NoError(t, err)
assert.Equal(t, []*model.User{
sanitized(u4),
}, result)
})
}
func assertUsers(t *testing.T, expected, actual []*model.User) {
expectedUsernames := make([]string, 0, len(expected))
for _, user := range expected {
expectedUsernames = append(expectedUsernames, user.Username)
}
actualUsernames := make([]string, 0, len(actual))
for _, user := range actual {
actualUsernames = append(actualUsernames, user.Username)
}
if assert.Equal(t, expectedUsernames, actualUsernames) {
assert.Equal(t, expected, actual)
}
}
func testUserStoreSearch(t *testing.T, ss store.Store) {
u1 := &model.User{
Username: "jimbo1" + model.NewId(),
FirstName: "Tim",
LastName: "Bill",
Nickname: "Rob",
Email: "harold" + model.NewId() + "@simulator.amazonses.com",
Roles: "system_user system_admin",
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2 := &model.User{
Username: "jim2-bobby" + model.NewId(),
Email: MakeEmail(),
Roles: "system_user system_user_manager",
}
_, err = ss.User().Save(u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
u3 := &model.User{
Username: "jimbo3" + model.NewId(),
Email: MakeEmail(),
Roles: "system_guest",
}
_, err = ss.User().Save(u3)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
// The users returned from the database will have AuthData as an empty string.
nilAuthData := new(string)
*nilAuthData = ""
u1.AuthData = nilAuthData
u2.AuthData = nilAuthData
u3.AuthData = nilAuthData
t1id := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: t1id, UserId: u1.Id, SchemeAdmin: true, SchemeUser: true}, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: t1id, UserId: u2.Id, SchemeAdmin: true, SchemeUser: true}, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: t1id, UserId: u3.Id, SchemeAdmin: false, SchemeUser: false, SchemeGuest: true}, -1)
require.NoError(t, nErr)
testCases := []struct {
Description string
TeamId string
Term string
Options *model.UserSearchOptions
Expected []*model.User
}{
{
"search jimb, team 1",
t1id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1, u3},
},
{
"search jimb, team 1 with team guest and team admin filters without sys admin filter",
t1id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
TeamRoles: []string{model.TeamGuestRoleId, model.TeamAdminRoleId},
},
[]*model.User{u3},
},
{
"search jimb, team 1 with team admin filter and sys admin filter",
t1id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
Roles: []string{model.SystemAdminRoleId},
TeamRoles: []string{model.TeamAdminRoleId},
},
[]*model.User{u1},
},
{
"search jim, team 1 with team admin filter",
t1id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
TeamRoles: []string{model.TeamAdminRoleId},
},
[]*model.User{u2},
},
{
"search jim, team 1 with team admin and team guest filter",
t1id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
TeamRoles: []string{model.TeamAdminRoleId, model.TeamGuestRoleId},
},
[]*model.User{u2, u3},
},
{
"search jim, team 1 with team admin and system admin filters",
t1id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
Roles: []string{model.SystemAdminRoleId},
TeamRoles: []string{model.TeamAdminRoleId},
},
[]*model.User{u2, u1},
},
{
"search jim, team 1 with system guest filter",
t1id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
Roles: []string{model.SystemGuestRoleId},
TeamRoles: []string{},
},
[]*model.User{u3},
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
users, err := ss.User().Search(
testCase.TeamId,
testCase.Term,
testCase.Options,
)
require.NoError(t, err)
assertUsers(t, testCase.Expected, users)
})
}
}
func testUserStoreSearchNotInChannel(t *testing.T, ss store.Store) {
u1 := &model.User{
Username: "jimbo1" + model.NewId(),
FirstName: "Tim",
LastName: "Bill",
Nickname: "Rob",
Email: "harold" + model.NewId() + "@simulator.amazonses.com",
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2 := &model.User{
Username: "jim2-bobby" + model.NewId(),
Email: MakeEmail(),
}
_, err = ss.User().Save(u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
u3 := &model.User{
Username: "jimbo3" + model.NewId(),
Email: MakeEmail(),
DeleteAt: 1,
}
_, err = ss.User().Save(u3)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr := ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
tid := model.NewId()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u1.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u2.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u3.Id}, -1)
require.NoError(t, nErr)
// The users returned from the database will have AuthData as an empty string.
nilAuthData := new(string)
*nilAuthData = ""
u1.AuthData = nilAuthData
u2.AuthData = nilAuthData
u3.AuthData = nilAuthData
ch1 := model.Channel{
TeamId: tid,
DisplayName: "NameName",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
c1, nErr := ss.Channel().Save(&ch1, -1)
require.NoError(t, nErr)
ch2 := model.Channel{
TeamId: tid,
DisplayName: "NameName",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
c2, nErr := ss.Channel().Save(&ch2, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c2.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c2.Id,
UserId: u2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
testCases := []struct {
Description string
TeamId string
ChannelId string
Term string
Options *model.UserSearchOptions
Expected []*model.User
}{
{
"search jimb, channel 1",
tid,
c1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1},
},
{
"search jimb, allow inactive, channel 1",
tid,
c1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1},
},
{
"search jimb, channel 1, no team id",
"",
c1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1},
},
{
"search jimb, channel 1, junk team id",
"junk",
c1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search jimb, channel 2",
tid,
c2.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search jimb, allow inactive, channel 2",
tid,
c2.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u3},
},
{
"search jimb, channel 2, no team id",
"",
c2.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search jimb, channel 2, junk team id",
"junk",
c2.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search jim, channel 1",
tid,
c1.Id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u2, u1},
},
{
"search jim, channel 1, limit 1",
tid,
c1.Id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: 1,
},
[]*model.User{u2},
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
users, err := ss.User().SearchNotInChannel(
testCase.TeamId,
testCase.ChannelId,
testCase.Term,
testCase.Options,
)
require.NoError(t, err)
assertUsers(t, testCase.Expected, users)
})
}
}
func testUserStoreSearchInChannel(t *testing.T, ss store.Store) {
u1 := &model.User{
Username: "jimbo1" + model.NewId(),
FirstName: "Tim",
LastName: "Bill",
Nickname: "Rob",
Email: "harold" + model.NewId() + "@simulator.amazonses.com",
Roles: "system_user system_admin",
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2 := &model.User{
Username: "jim-bobby" + model.NewId(),
Email: MakeEmail(),
Roles: "system_user",
}
_, err = ss.User().Save(u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
u3 := &model.User{
Username: "jimbo3" + model.NewId(),
Email: MakeEmail(),
DeleteAt: 1,
Roles: "system_user",
}
_, err = ss.User().Save(u3)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr := ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
tid := model.NewId()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u1.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u2.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u3.Id}, -1)
require.NoError(t, nErr)
// The users returned from the database will have AuthData as an empty string.
nilAuthData := new(string)
*nilAuthData = ""
u1.AuthData = nilAuthData
u2.AuthData = nilAuthData
u3.AuthData = nilAuthData
ch1 := model.Channel{
TeamId: tid,
DisplayName: "NameName",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
c1, nErr := ss.Channel().Save(&ch1, -1)
require.NoError(t, nErr)
ch2 := model.Channel{
TeamId: tid,
DisplayName: "NameName",
Name: NewTestId(),
Type: model.ChannelTypeOpen,
}
c2, nErr := ss.Channel().Save(&ch2, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeAdmin: true,
SchemeUser: true,
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c2.Id,
UserId: u2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeAdmin: false,
SchemeUser: true,
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeAdmin: false,
SchemeUser: true,
})
require.NoError(t, nErr)
testCases := []struct {
Description string
ChannelId string
Term string
Options *model.UserSearchOptions
Expected []*model.User
}{
{
"search jimb, channel 1",
c1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1},
},
{
"search jimb, allow inactive, channel 1",
c1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1, u3},
},
{
"search jimb, allow inactive, channel 1, limit 1",
c1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: 1,
},
[]*model.User{u1},
},
{
"search jimb, channel 2",
c2.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search jimb, allow inactive, channel 2",
c2.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search jim, allow inactive, channel 1 with system admin filter",
c1.Id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
Roles: []string{model.SystemAdminRoleId},
},
[]*model.User{u1},
},
{
"search jim, allow inactive, channel 1 with system admin and system user filter",
c1.Id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
Roles: []string{model.SystemAdminRoleId, model.SystemUserRoleId},
},
[]*model.User{u1, u3},
},
{
"search jim, allow inactive, channel 1 with channel user filter",
c1.Id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
ChannelRoles: []string{model.ChannelUserRoleId},
},
[]*model.User{u3},
},
{
"search jim, allow inactive, channel 1 with channel user and channel admin filter",
c1.Id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
ChannelRoles: []string{model.ChannelUserRoleId, model.ChannelAdminRoleId},
},
[]*model.User{u3},
},
{
"search jim, allow inactive, channel 2 with channel user filter",
c2.Id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
ChannelRoles: []string{model.ChannelUserRoleId},
},
[]*model.User{u2},
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
users, err := ss.User().SearchInChannel(
testCase.ChannelId,
testCase.Term,
testCase.Options,
)
require.NoError(t, err)
assertUsers(t, testCase.Expected, users)
})
}
}
func testUserStoreSearchNotInTeam(t *testing.T, ss store.Store) {
u1 := &model.User{
Username: "jimbo1" + model.NewId(),
FirstName: "Tim",
LastName: "Bill",
Nickname: "Rob",
Email: "harold" + model.NewId() + "@simulator.amazonses.com",
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2 := &model.User{
Username: "jim-bobby" + model.NewId(),
Email: MakeEmail(),
}
_, err = ss.User().Save(u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
u3 := &model.User{
Username: "jimbo3" + model.NewId(),
Email: MakeEmail(),
DeleteAt: 1,
}
_, err = ss.User().Save(u3)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr := ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
u4 := &model.User{
Username: "simon" + model.NewId(),
Email: MakeEmail(),
DeleteAt: 0,
}
_, err = ss.User().Save(u4)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
u5 := &model.User{
Username: "yu" + model.NewId(),
FirstName: "En",
LastName: "Yu",
Nickname: "enyu",
Email: MakeEmail(),
}
_, err = ss.User().Save(u5)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u5.Id)) }()
u6 := &model.User{
Username: "underscore" + model.NewId(),
FirstName: "Du_",
LastName: "_DE",
Nickname: "lodash",
Email: MakeEmail(),
}
_, err = ss.User().Save(u6)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u6.Id)) }()
teamId1 := model.NewId()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId1, UserId: u1.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId1, UserId: u2.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId1, UserId: u3.Id}, -1)
require.NoError(t, nErr)
// u4 is not in team 1
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId1, UserId: u5.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId1, UserId: u6.Id}, -1)
require.NoError(t, nErr)
teamId2 := model.NewId()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId2, UserId: u4.Id}, -1)
require.NoError(t, nErr)
// The users returned from the database will have AuthData as an empty string.
nilAuthData := new(string)
*nilAuthData = ""
u1.AuthData = nilAuthData
u2.AuthData = nilAuthData
u3.AuthData = nilAuthData
u4.AuthData = nilAuthData
u5.AuthData = nilAuthData
u6.AuthData = nilAuthData
testCases := []struct {
Description string
TeamId string
Term string
Options *model.UserSearchOptions
Expected []*model.User
}{
{
"search simo, team 1",
teamId1,
"simo",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u4},
},
{
"search jimb, team 1",
teamId1,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search jimb, allow inactive, team 1",
teamId1,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search simo, team 2",
teamId2,
"simo",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search jimb, team2",
teamId2,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1},
},
{
"search jimb, allow inactive, team 2",
teamId2,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1, u3},
},
{
"search jimb, allow inactive, team 2, limit 1",
teamId2,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: 1,
},
[]*model.User{u1},
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
users, err := ss.User().SearchNotInTeam(
testCase.TeamId,
testCase.Term,
testCase.Options,
)
require.NoError(t, err)
assertUsers(t, testCase.Expected, users)
})
}
}
func testUserStoreSearchWithoutTeam(t *testing.T, ss store.Store) {
u1 := &model.User{
Username: "jimbo1" + model.NewId(),
FirstName: "Tim",
LastName: "Bill",
Nickname: "Rob",
Email: "harold" + model.NewId() + "@simulator.amazonses.com",
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2 := &model.User{
Username: "jim2-bobby" + model.NewId(),
Email: MakeEmail(),
}
_, err = ss.User().Save(u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
u3 := &model.User{
Username: "jimbo3" + model.NewId(),
Email: MakeEmail(),
DeleteAt: 1,
}
_, err = ss.User().Save(u3)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr := ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
tid := model.NewId()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u3.Id}, -1)
require.NoError(t, nErr)
// The users returned from the database will have AuthData as an empty string.
nilAuthData := new(string)
*nilAuthData = ""
u1.AuthData = nilAuthData
u2.AuthData = nilAuthData
u3.AuthData = nilAuthData
testCases := []struct {
Description string
Term string
Options *model.UserSearchOptions
Expected []*model.User
}{
{
"empty string",
"",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u2, u1},
},
{
"jim",
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u2, u1},
},
{
"PLT-8354",
"* ",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u2, u1},
},
{
"jim, limit 1",
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: 1,
},
[]*model.User{u2},
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
users, err := ss.User().SearchWithoutTeam(
testCase.Term,
testCase.Options,
)
require.NoError(t, err)
assertUsers(t, testCase.Expected, users)
})
}
}
func testUserStoreSearchInGroup(t *testing.T, ss store.Store) {
u1 := &model.User{
Username: "jimbo1" + model.NewId(),
FirstName: "Tim",
LastName: "Bill",
Nickname: "Rob",
Email: "harold" + model.NewId() + "@simulator.amazonses.com",
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2 := &model.User{
Username: "jim-bobby" + model.NewId(),
Email: MakeEmail(),
}
_, err = ss.User().Save(u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
u3 := &model.User{
Username: "jimbo3" + model.NewId(),
Email: MakeEmail(),
}
_, err = ss.User().Save(u3)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
// The users returned from the database will have AuthData as an empty string.
nilAuthData := model.NewString("")
u1.AuthData = nilAuthData
u2.AuthData = nilAuthData
u3.AuthData = nilAuthData
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
_, err = ss.Group().Create(g1)
require.NoError(t, err)
g2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString(model.NewId()),
}
_, err = ss.Group().Create(g2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(g1.Id, u1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(g2.Id, u2.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(g1.Id, u3.Id)
require.NoError(t, err)
u3.DeleteAt = 1
_, err = ss.User().Update(u3, true)
require.NoError(t, err)
testCases := []struct {
Description string
GroupId string
Term string
Options *model.UserSearchOptions
Expected []*model.User
}{
{
"search jimb, group 1",
g1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1},
},
{
"search jimb, group 1, allow inactive",
g1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1, u3},
},
{
"search jimb, group 1, limit 1",
g1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: 1,
},
[]*model.User{u1},
},
{
"search jimb, group 2",
g2.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search jimb, allow inactive, group 2",
g2.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
users, err := ss.User().SearchInGroup(
testCase.GroupId,
testCase.Term,
testCase.Options,
)
require.NoError(t, err)
assertUsers(t, testCase.Expected, users)
})
}
}
func testUserStoreSearchNotInGroup(t *testing.T, ss store.Store) {
u1 := &model.User{
Username: "jimbo1" + model.NewId(),
FirstName: "Tim",
LastName: "Bill",
Nickname: "Rob",
Email: "harold" + model.NewId() + "@simulator.amazonses.com",
}
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2 := &model.User{
Username: "jim-bobby" + model.NewId(),
Email: MakeEmail(),
}
_, err = ss.User().Save(u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
u3 := &model.User{
Username: "jimbo3" + model.NewId(),
Email: MakeEmail(),
}
_, err = ss.User().Save(u3)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
// The users returned from the database will have AuthData as an empty string.
nilAuthData := model.NewString("")
u1.AuthData = nilAuthData
u2.AuthData = nilAuthData
u3.AuthData = nilAuthData
g1 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceCustom,
RemoteId: model.NewString(model.NewId()),
}
_, err = ss.Group().Create(g1)
require.NoError(t, err)
g2 := &model.Group{
Name: model.NewString(model.NewId()),
DisplayName: model.NewId(),
Description: model.NewId(),
Source: model.GroupSourceCustom,
RemoteId: model.NewString(model.NewId()),
}
_, err = ss.Group().Create(g2)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(g1.Id, u1.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(g2.Id, u2.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(g1.Id, u3.Id)
require.NoError(t, err)
u3.DeleteAt = 1
_, err = ss.User().Update(u3, true)
require.NoError(t, err)
testCases := []struct {
Description string
GroupId string
Term string
Options *model.UserSearchOptions
Expected []*model.User
}{
{
"search jimb, not in group 1",
g1.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{},
},
{
"search jim, not in group 1",
g1.Id,
"jim",
&model.UserSearchOptions{
AllowFullNames: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u2},
},
{
"search jimb, not in group 3, allow inactive",
g2.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1, u3},
},
{
"search jim, not in group 2",
g2.Id,
"jimb",
&model.UserSearchOptions{
AllowFullNames: true,
AllowInactive: true,
Limit: model.UserSearchDefaultLimit,
},
[]*model.User{u1, u3},
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
users, err := ss.User().SearchNotInGroup(
testCase.GroupId,
testCase.Term,
testCase.Options,
)
require.NoError(t, err)
assertUsers(t, testCase.Expected, users)
})
}
}
func testCount(t *testing.T, ss store.Store) {
// Regular
teamId := model.NewId()
channelId := model.NewId()
regularUser := &model.User{}
regularUser.Email = MakeEmail()
regularUser.Roles = model.SystemUserRoleId
_, err := ss.User().Save(regularUser)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(regularUser.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: regularUser.Id, SchemeAdmin: false, SchemeUser: true}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{UserId: regularUser.Id, ChannelId: channelId, SchemeAdmin: false, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
guestUser := &model.User{}
guestUser.Email = MakeEmail()
guestUser.Roles = model.SystemGuestRoleId
_, err = ss.User().Save(guestUser)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(guestUser.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: guestUser.Id, SchemeAdmin: false, SchemeUser: false, SchemeGuest: true}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{UserId: guestUser.Id, ChannelId: channelId, SchemeAdmin: false, SchemeUser: false, SchemeGuest: true, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
teamAdmin := &model.User{}
teamAdmin.Email = MakeEmail()
teamAdmin.Roles = model.SystemUserRoleId
_, err = ss.User().Save(teamAdmin)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(teamAdmin.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: teamAdmin.Id, SchemeAdmin: true, SchemeUser: true}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{UserId: teamAdmin.Id, ChannelId: channelId, SchemeAdmin: true, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
sysAdmin := &model.User{}
sysAdmin.Email = MakeEmail()
sysAdmin.Roles = model.SystemAdminRoleId + " " + model.SystemUserRoleId
_, err = ss.User().Save(sysAdmin)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(sysAdmin.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: sysAdmin.Id, SchemeAdmin: false, SchemeUser: true}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{UserId: sysAdmin.Id, ChannelId: channelId, SchemeAdmin: true, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
// Deleted
deletedUser := &model.User{}
deletedUser.Email = MakeEmail()
deletedUser.DeleteAt = model.GetMillis()
_, err = ss.User().Save(deletedUser)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(deletedUser.Id)) }()
// Bot
botUser, err := ss.User().Save(&model.User{
Email: MakeEmail(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(botUser.Id)) }()
_, nErr = ss.Bot().Save(&model.Bot{
UserId: botUser.Id,
Username: botUser.Username,
OwnerId: regularUser.Id,
})
require.NoError(t, nErr)
botUser.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(botUser.Id)) }()
testCases := []struct {
Description string
Options model.UserCountOptions
Expected int64
}{
{
"No bot accounts no deleted accounts and no team id",
model.UserCountOptions{
IncludeBotAccounts: false,
IncludeDeleted: false,
TeamId: "",
},
4,
},
{
"Include bot accounts no deleted accounts and no team id",
model.UserCountOptions{
IncludeBotAccounts: true,
IncludeDeleted: false,
TeamId: "",
},
5,
},
{
"Include delete accounts no bots and no team id",
model.UserCountOptions{
IncludeBotAccounts: false,
IncludeDeleted: true,
TeamId: "",
},
5,
},
{
"Include bot accounts and deleted accounts and no team id",
model.UserCountOptions{
IncludeBotAccounts: true,
IncludeDeleted: true,
TeamId: "",
},
6,
},
{
"Include bot accounts, deleted accounts, exclude regular users with no team id",
model.UserCountOptions{
IncludeBotAccounts: true,
IncludeDeleted: true,
ExcludeRegularUsers: true,
TeamId: "",
},
1,
},
{
"Include bot accounts and deleted accounts with existing team id",
model.UserCountOptions{
IncludeBotAccounts: true,
IncludeDeleted: true,
TeamId: teamId,
},
4,
},
{
"Include bot accounts and deleted accounts with fake team id",
model.UserCountOptions{
IncludeBotAccounts: true,
IncludeDeleted: true,
TeamId: model.NewId(),
},
0,
},
{
"Include bot accounts and deleted accounts with existing team id and view restrictions allowing team",
model.UserCountOptions{
IncludeBotAccounts: true,
IncludeDeleted: true,
TeamId: teamId,
ViewRestrictions: &model.ViewUsersRestrictions{Teams: []string{teamId}},
},
4,
},
{
"Include bot accounts and deleted accounts with existing team id and view restrictions not allowing current team",
model.UserCountOptions{
IncludeBotAccounts: true,
IncludeDeleted: true,
TeamId: teamId,
ViewRestrictions: &model.ViewUsersRestrictions{Teams: []string{model.NewId()}},
},
0,
},
{
"Filter by system admins only",
model.UserCountOptions{
TeamId: teamId,
Roles: []string{model.SystemAdminRoleId},
},
1,
},
{
"Filter by system users only",
model.UserCountOptions{
TeamId: teamId,
Roles: []string{model.SystemUserRoleId},
},
2,
},
{
"Filter by system guests only",
model.UserCountOptions{
TeamId: teamId,
Roles: []string{model.SystemGuestRoleId},
},
1,
},
{
"Filter by system admins and system users",
model.UserCountOptions{
TeamId: teamId,
Roles: []string{model.SystemAdminRoleId, model.SystemUserRoleId},
},
3,
},
{
"Filter by system admins, system user and system guests",
model.UserCountOptions{
TeamId: teamId,
Roles: []string{model.SystemAdminRoleId, model.SystemUserRoleId, model.SystemGuestRoleId},
},
4,
},
{
"Filter by team admins",
model.UserCountOptions{
TeamId: teamId,
TeamRoles: []string{model.TeamAdminRoleId},
},
1,
},
{
"Filter by team members",
model.UserCountOptions{
TeamId: teamId,
TeamRoles: []string{model.TeamUserRoleId},
},
1,
},
{
"Filter by team guests",
model.UserCountOptions{
TeamId: teamId,
TeamRoles: []string{model.TeamGuestRoleId},
},
1,
},
{
"Filter by team guests and any system role",
model.UserCountOptions{
TeamId: teamId,
TeamRoles: []string{model.TeamGuestRoleId},
Roles: []string{model.SystemAdminRoleId},
},
2,
},
{
"Filter by channel members",
model.UserCountOptions{
ChannelId: channelId,
ChannelRoles: []string{model.ChannelUserRoleId},
},
1,
},
{
"Filter by channel members and system admins",
model.UserCountOptions{
ChannelId: channelId,
Roles: []string{model.SystemAdminRoleId},
ChannelRoles: []string{model.ChannelUserRoleId},
},
2,
},
{
"Filter by channel members and system admins and channel admins",
model.UserCountOptions{
ChannelId: channelId,
Roles: []string{model.SystemAdminRoleId},
ChannelRoles: []string{model.ChannelUserRoleId, model.ChannelAdminRoleId},
},
3,
},
{
"Filter by channel guests",
model.UserCountOptions{
ChannelId: channelId,
ChannelRoles: []string{model.ChannelGuestRoleId},
},
1,
},
{
"Filter by channel guests and any system role",
model.UserCountOptions{
ChannelId: channelId,
ChannelRoles: []string{model.ChannelGuestRoleId},
Roles: []string{model.SystemAdminRoleId},
},
2,
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
count, err := ss.User().Count(testCase.Options)
require.NoError(t, err)
require.Equal(t, testCase.Expected, count)
})
}
}
func testUserStoreGetFirstSystemAdminID(t *testing.T, ss store.Store) {
sysAdmin := &model.User{}
sysAdmin.Email = MakeEmail()
sysAdmin.Roles = model.SystemAdminRoleId + " " + model.SystemUserRoleId
sysAdmin, err := ss.User().Save(sysAdmin)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(sysAdmin.Id)) }()
// We need the second system admin to be created after the first one
// our granulirity is ms
time.Sleep(1 * time.Millisecond)
sysAdmin2 := &model.User{}
sysAdmin2.Email = MakeEmail()
sysAdmin2.Roles = model.SystemAdminRoleId + " " + model.SystemUserRoleId
sysAdmin2, err = ss.User().Save(sysAdmin2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(sysAdmin2.Id)) }()
returnedId, err := ss.User().GetFirstSystemAdminID()
require.NoError(t, err)
require.Equal(t, sysAdmin.Id, returnedId)
}
func testUserStoreAnalyticsActiveCount(t *testing.T, ss store.Store, s SqlStore) {
cleanupStatusStore(t, s)
// Create 5 users statuses u0, u1, u2, u3, u4.
// u4 is also a bot
u0, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u0" + model.NewId(),
})
require.NoError(t, err)
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
})
require.NoError(t, err)
defer func() {
require.NoError(t, ss.User().PermanentDelete(u0.Id))
require.NoError(t, ss.User().PermanentDelete(u1.Id))
require.NoError(t, ss.User().PermanentDelete(u2.Id))
require.NoError(t, ss.User().PermanentDelete(u3.Id))
require.NoError(t, ss.User().PermanentDelete(u4.Id))
}()
_, nErr := ss.Bot().Save(&model.Bot{
UserId: u4.Id,
Username: u4.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
millis := model.GetMillis()
millisTwoDaysAgo := model.GetMillis() - (2 * DayMilliseconds)
millisTwoMonthsAgo := model.GetMillis() - (2 * MonthMilliseconds)
// u0 last activity status is two months ago.
// u1 last activity status is two days ago.
// u2, u3, u4 last activity is within last day
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u0.Id, Status: model.StatusOffline, LastActivityAt: millisTwoMonthsAgo}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u1.Id, Status: model.StatusOffline, LastActivityAt: millisTwoDaysAgo}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u2.Id, Status: model.StatusOffline, LastActivityAt: millis}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u3.Id, Status: model.StatusOffline, LastActivityAt: millis}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u4.Id, Status: model.StatusOffline, LastActivityAt: millis}))
// Daily counts (without bots)
count, err := ss.User().AnalyticsActiveCount(DayMilliseconds, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: true})
require.NoError(t, err)
assert.Equal(t, int64(2), count)
// Daily counts (with bots)
count, err = ss.User().AnalyticsActiveCount(DayMilliseconds, model.UserCountOptions{IncludeBotAccounts: true, IncludeDeleted: true})
require.NoError(t, err)
assert.Equal(t, int64(3), count)
// Monthly counts (without bots)
count, err = ss.User().AnalyticsActiveCount(MonthMilliseconds, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: true})
require.NoError(t, err)
assert.Equal(t, int64(3), count)
// Monthly counts - (with bots)
count, err = ss.User().AnalyticsActiveCount(MonthMilliseconds, model.UserCountOptions{IncludeBotAccounts: true, IncludeDeleted: true})
require.NoError(t, err)
assert.Equal(t, int64(4), count)
// Monthly counts - (with bots, excluding deleted)
count, err = ss.User().AnalyticsActiveCount(MonthMilliseconds, model.UserCountOptions{IncludeBotAccounts: true, IncludeDeleted: false})
require.NoError(t, err)
assert.Equal(t, int64(4), count)
}
func testUserStoreAnalyticsActiveCountForPeriod(t *testing.T, ss store.Store, s SqlStore) {
cleanupStatusStore(t, s)
// Create 5 users statuses u0, u1, u2, u3, u4.
// u4 is also a bot
u0, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u0" + model.NewId(),
})
require.NoError(t, err)
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
})
require.NoError(t, err)
defer func() {
require.NoError(t, ss.User().PermanentDelete(u0.Id))
require.NoError(t, ss.User().PermanentDelete(u1.Id))
require.NoError(t, ss.User().PermanentDelete(u2.Id))
require.NoError(t, ss.User().PermanentDelete(u3.Id))
require.NoError(t, ss.User().PermanentDelete(u4.Id))
}()
_, nErr := ss.Bot().Save(&model.Bot{
UserId: u4.Id,
Username: u4.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
millis := model.GetMillis()
millisTwoDaysAgo := model.GetMillis() - (2 * DayMilliseconds)
millisTwoMonthsAgo := model.GetMillis() - (2 * MonthMilliseconds)
// u0 last activity status is two months ago.
// u1 last activity status is one month ago
// u2 last activity is two days ago
// u2 last activity is one day ago
// u3 last activity is within last day
// u4 last activity is within last day
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u0.Id, Status: model.StatusOffline, LastActivityAt: millisTwoMonthsAgo}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u1.Id, Status: model.StatusOffline, LastActivityAt: millisTwoMonthsAgo + MonthMilliseconds}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u2.Id, Status: model.StatusOffline, LastActivityAt: millisTwoDaysAgo}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u3.Id, Status: model.StatusOffline, LastActivityAt: millisTwoDaysAgo + DayMilliseconds}))
require.NoError(t, ss.Status().SaveOrUpdate(&model.Status{UserId: u4.Id, Status: model.StatusOffline, LastActivityAt: millis}))
// Two months to two days (without bots)
count, nerr := ss.User().AnalyticsActiveCountForPeriod(millisTwoMonthsAgo, millisTwoDaysAgo, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: false})
require.NoError(t, nerr)
assert.Equal(t, int64(2), count)
// Two months to two days (without bots)
count, nerr = ss.User().AnalyticsActiveCountForPeriod(millisTwoMonthsAgo, millisTwoDaysAgo, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: true})
require.NoError(t, nerr)
assert.Equal(t, int64(2), count)
// Two days to present - (with bots)
count, nerr = ss.User().AnalyticsActiveCountForPeriod(millisTwoDaysAgo, millis, model.UserCountOptions{IncludeBotAccounts: true, IncludeDeleted: false})
require.NoError(t, nerr)
assert.Equal(t, int64(2), count)
// Two days to present - (with bots, excluding deleted)
count, nerr = ss.User().AnalyticsActiveCountForPeriod(millisTwoDaysAgo, millis, model.UserCountOptions{IncludeBotAccounts: true, IncludeDeleted: true})
require.NoError(t, nerr)
assert.Equal(t, int64(2), count)
}
func testUserStoreAnalyticsGetInactiveUsersCount(t *testing.T, ss store.Store) {
u1 := &model.User{}
u1.Email = MakeEmail()
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
count, err := ss.User().AnalyticsGetInactiveUsersCount()
require.NoError(t, err)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.DeleteAt = model.GetMillis()
_, err = ss.User().Save(u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
newCount, err := ss.User().AnalyticsGetInactiveUsersCount()
require.NoError(t, err)
require.Equal(t, count, newCount-1, "Expected 1 more inactive users but found otherwise.")
}
func testUserStoreAnalyticsGetSystemAdminCount(t *testing.T, ss store.Store) {
countBefore, err := ss.User().AnalyticsGetSystemAdminCount()
require.NoError(t, err)
u1 := model.User{}
u1.Email = MakeEmail()
u1.Username = model.NewId()
u1.Roles = "system_user system_admin"
u2 := model.User{}
u2.Email = MakeEmail()
u2.Username = model.NewId()
_, nErr := ss.User().Save(&u1)
require.NoError(t, nErr, "couldn't save user")
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr = ss.User().Save(&u2)
require.NoError(t, nErr, "couldn't save user")
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
result, err := ss.User().AnalyticsGetSystemAdminCount()
require.NoError(t, err)
require.Equal(t, countBefore+1, result, "Did not get the expected number of system admins.")
}
func testUserStoreAnalyticsGetGuestCount(t *testing.T, ss store.Store) {
countBefore, err := ss.User().AnalyticsGetGuestCount()
require.NoError(t, err)
u1 := model.User{}
u1.Email = MakeEmail()
u1.Username = model.NewId()
u1.Roles = "system_user system_admin"
u2 := model.User{}
u2.Email = MakeEmail()
u2.Username = model.NewId()
u2.Roles = "system_user"
u3 := model.User{}
u3.Email = MakeEmail()
u3.Username = model.NewId()
u3.Roles = "system_guest"
_, nErr := ss.User().Save(&u1)
require.NoError(t, nErr, "couldn't save user")
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr = ss.User().Save(&u2)
require.NoError(t, nErr, "couldn't save user")
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.User().Save(&u3)
require.NoError(t, nErr, "couldn't save user")
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
result, err := ss.User().AnalyticsGetGuestCount()
require.NoError(t, err)
require.Equal(t, countBefore+1, result, "Did not get the expected number of guests.")
}
func testUserStoreAnalyticsGetExternalUsers(t *testing.T, ss store.Store) {
localHostDomain := "mattermost.com"
result, err := ss.User().AnalyticsGetExternalUsers(localHostDomain)
require.NoError(t, err)
assert.False(t, result)
u1 := model.User{}
u1.Email = "a@mattermost.com"
u1.Username = model.NewId()
u1.Roles = "system_user system_admin"
u2 := model.User{}
u2.Email = "b@example.com"
u2.Username = model.NewId()
u2.Roles = "system_user"
u3 := model.User{}
u3.Email = "c@test.com"
u3.Username = model.NewId()
u3.Roles = "system_guest"
_, err = ss.User().Save(&u1)
require.NoError(t, err, "couldn't save user")
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, err = ss.User().Save(&u2)
require.NoError(t, err, "couldn't save user")
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, err = ss.User().Save(&u3)
require.NoError(t, err, "couldn't save user")
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
result, err = ss.User().AnalyticsGetExternalUsers(localHostDomain)
require.NoError(t, err)
assert.True(t, result)
}
func testUserStoreGetProfilesNotInTeam(t *testing.T, ss store.Store) {
team, err := ss.Team().Save(&model.Team{
DisplayName: "Team",
Name: NewTestId(),
Type: model.TeamOpen,
})
require.NoError(t, err)
teamId := team.Id
teamId2 := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
// Ensure update at timestamp changes
time.Sleep(time.Millisecond)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId2, UserId: u2.Id}, -1)
require.NoError(t, nErr)
// Ensure update at timestamp changes
time.Sleep(time.Millisecond)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
var etag1, etag2, etag3 string
t.Run("etag for profiles not in team 1", func(t *testing.T) {
etag1 = ss.User().GetEtagForProfilesNotInTeam(teamId)
})
t.Run("get not in team 1, offset 0, limit 100000", func(t *testing.T) {
users, userErr := ss.User().GetProfilesNotInTeam(teamId, false, 0, 100000, nil)
require.NoError(t, userErr)
assert.Equal(t, []*model.User{
sanitized(u2),
sanitized(u3),
}, users)
})
t.Run("get not in team 1, offset 1, limit 1", func(t *testing.T) {
users, userErr := ss.User().GetProfilesNotInTeam(teamId, false, 1, 1, nil)
require.NoError(t, userErr)
assert.Equal(t, []*model.User{
sanitized(u3),
}, users)
})
t.Run("get not in team 2, offset 0, limit 100", func(t *testing.T) {
users, userErr := ss.User().GetProfilesNotInTeam(teamId2, false, 0, 100, nil)
require.NoError(t, userErr)
assert.Equal(t, []*model.User{
sanitized(u1),
sanitized(u3),
}, users)
})
// Ensure update at timestamp changes
time.Sleep(time.Millisecond)
// Add u2 to team 1
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u2.UpdateAt, err = ss.User().UpdateUpdateAt(u2.Id)
require.NoError(t, err)
t.Run("etag for profiles not in team 1 after update", func(t *testing.T) {
etag2 = ss.User().GetEtagForProfilesNotInTeam(teamId)
require.NotEqual(t, etag2, etag1, "etag should have changed")
})
t.Run("get not in team 1, offset 0, limit 100000 after update", func(t *testing.T) {
users, userErr := ss.User().GetProfilesNotInTeam(teamId, false, 0, 100000, nil)
require.NoError(t, userErr)
assert.Equal(t, []*model.User{
sanitized(u3),
}, users)
})
// Ensure update at timestamp changes
time.Sleep(time.Millisecond)
e := ss.Team().RemoveMember(teamId, u1.Id)
require.NoError(t, e)
e = ss.Team().RemoveMember(teamId, u2.Id)
require.NoError(t, e)
u1.UpdateAt, err = ss.User().UpdateUpdateAt(u1.Id)
require.NoError(t, err)
u2.UpdateAt, err = ss.User().UpdateUpdateAt(u2.Id)
require.NoError(t, err)
t.Run("etag for profiles not in team 1 after second update", func(t *testing.T) {
etag3 = ss.User().GetEtagForProfilesNotInTeam(teamId)
require.NotEqual(t, etag1, etag3, "etag should have changed")
require.NotEqual(t, etag2, etag3, "etag should have changed")
})
t.Run("get not in team 1, offset 0, limit 100000 after second update", func(t *testing.T) {
users, userErr := ss.User().GetProfilesNotInTeam(teamId, false, 0, 100000, nil)
require.NoError(t, userErr)
assert.Equal(t, []*model.User{
sanitized(u1),
sanitized(u2),
sanitized(u3),
}, users)
})
// Ensure update at timestamp changes
time.Sleep(time.Millisecond)
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u4.Id}, -1)
require.NoError(t, nErr)
t.Run("etag for profiles not in team 1 after addition to team", func(t *testing.T) {
etag4 := ss.User().GetEtagForProfilesNotInTeam(teamId)
require.Equal(t, etag3, etag4, "etag should not have changed")
})
// Add u3 to team 2
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId2, UserId: u3.Id}, -1)
require.NoError(t, nErr)
u3.UpdateAt, err = ss.User().UpdateUpdateAt(u3.Id)
require.NoError(t, err)
// GetEtagForProfilesNotInTeam produces a new etag every time a member, not
// in the team, gets a new UpdateAt value. In the case that an older member
// in the set joins a different team, their UpdateAt value changes, thus
// creating a new etag (even though the user set doesn't change). A hashing
// solution, which only uses UserIds, would solve this issue.
t.Run("etag for profiles not in team 1 after u3 added to team 2", func(t *testing.T) {
t.Skip()
etag4 := ss.User().GetEtagForProfilesNotInTeam(teamId)
require.Equal(t, etag3, etag4, "etag should not have changed")
})
t.Run("get not in team 1, offset 0, limit 100000 after second update, setting group constrained when it's not", func(t *testing.T) {
users, userErr := ss.User().GetProfilesNotInTeam(teamId, true, 0, 100000, nil)
require.NoError(t, userErr)
assert.Empty(t, users)
})
// create a group
group, err := ss.Group().Create(&model.Group{
Name: model.NewString("n_" + model.NewId()),
DisplayName: "dn_" + model.NewId(),
Source: model.GroupSourceLdap,
RemoteId: model.NewString("ri_" + model.NewId()),
})
require.NoError(t, err)
// add two members to the group
for _, u := range []*model.User{u1, u2} {
_, err = ss.Group().UpsertMember(group.Id, u.Id)
require.NoError(t, err)
}
// associate the group with the team
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
GroupId: group.Id,
SyncableId: teamId,
Type: model.GroupSyncableTypeTeam,
})
require.NoError(t, err)
t.Run("get not in team 1, offset 0, limit 100000 after second update, setting group constrained", func(t *testing.T) {
users, userErr := ss.User().GetProfilesNotInTeam(teamId, true, 0, 100000, nil)
require.NoError(t, userErr)
assert.Equal(t, []*model.User{
sanitized(u1),
sanitized(u2),
}, users)
})
}
func testUserStoreClearAllCustomRoleAssignments(t *testing.T, ss store.Store) {
u1 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
Roles: "system_user system_admin system_post_all",
}
u2 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
Roles: "system_user custom_role system_admin another_custom_role",
}
u3 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
Roles: "system_user",
}
u4 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
Roles: "custom_only",
}
_, err := ss.User().Save(&u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, err = ss.User().Save(&u2)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, err = ss.User().Save(&u3)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, err = ss.User().Save(&u4)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
require.NoError(t, ss.User().ClearAllCustomRoleAssignments())
r1, err := ss.User().GetByUsername(u1.Username)
require.NoError(t, err)
assert.Equal(t, u1.Roles, r1.Roles)
r2, err1 := ss.User().GetByUsername(u2.Username)
require.NoError(t, err1)
assert.Equal(t, "system_user system_admin", r2.Roles)
r3, err2 := ss.User().GetByUsername(u3.Username)
require.NoError(t, err2)
assert.Equal(t, u3.Roles, r3.Roles)
r4, err3 := ss.User().GetByUsername(u4.Username)
require.NoError(t, err3)
assert.Equal(t, "", r4.Roles)
}
func testUserStoreGetAllAfter(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: model.NewId(),
Roles: "system_user system_admin system_post_all",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr := ss.Bot().Save(&model.Bot{
UserId: u2.Id,
Username: u2.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u2.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u2.Id)) }()
expected := []*model.User{u1, u2}
if strings.Compare(u2.Id, u1.Id) < 0 {
expected = []*model.User{u2, u1}
}
t.Run("get after lowest possible id", func(t *testing.T) {
actual, err := ss.User().GetAllAfter(10000, strings.Repeat("0", 26))
require.NoError(t, err)
assert.Equal(t, expected, actual)
})
t.Run("get after first user", func(t *testing.T) {
actual, err := ss.User().GetAllAfter(10000, expected[0].Id)
require.NoError(t, err)
assert.Equal(t, []*model.User{expected[1]}, actual)
})
t.Run("get after second user", func(t *testing.T) {
actual, err := ss.User().GetAllAfter(10000, expected[1].Id)
require.NoError(t, err)
assert.Equal(t, []*model.User{}, actual)
})
}
func testUserStoreGetUsersBatchForIndexing(t *testing.T, ss store.Store) {
// Set up all the objects needed
t1, err := ss.Team().Save(&model.Team{
DisplayName: "Team1",
Name: NewTestId(),
Type: model.TeamOpen,
})
require.NoError(t, err)
ch1 := &model.Channel{
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
cPub1, nErr := ss.Channel().Save(ch1, -1)
require.NoError(t, nErr)
ch2 := &model.Channel{
Name: model.NewId(),
Type: model.ChannelTypeOpen,
}
cPub2, nErr := ss.Channel().Save(ch2, -1)
require.NoError(t, nErr)
ch3 := &model.Channel{
Name: model.NewId(),
Type: model.ChannelTypePrivate,
}
cPriv, nErr := ss.Channel().Save(ch3, -1)
require.NoError(t, nErr)
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: model.NewId(),
CreateAt: model.GetMillis(),
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: model.NewId(),
CreateAt: model.GetMillis(),
})
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{
UserId: u2.Id,
TeamId: t1.Id,
}, 100)
require.NoError(t, nErr)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
UserId: u2.Id,
ChannelId: cPub1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
UserId: u2.Id,
ChannelId: cPub2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: model.NewId(),
CreateAt: model.GetMillis(),
})
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(&model.TeamMember{
UserId: u3.Id,
TeamId: t1.Id,
DeleteAt: model.GetMillis(),
}, 100)
require.NoError(t, nErr)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
UserId: u3.Id,
ChannelId: cPub2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
_, err = ss.Channel().SaveMember(&model.ChannelMember{
UserId: u3.Id,
ChannelId: cPriv.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
cDM := &model.Channel{
Name: model.NewId() + "__" + model.NewId(),
Type: model.ChannelTypeDirect,
}
cm1 := &model.ChannelMember{
UserId: u3.Id,
ChannelId: cDM.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
cm2 := &model.ChannelMember{
UserId: u2.Id,
ChannelId: cDM.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
cDM, nErr = ss.Channel().SaveDirectChannel(cDM, cm1, cm2)
require.NoError(t, nErr)
// Getting all users
res1List, err := ss.User().GetUsersBatchForIndexing(u1.CreateAt-1, "", 100)
require.NoError(t, err)
assert.Len(t, res1List, 3)
for _, user := range res1List {
switch user.Id {
case u2.Id:
assert.ElementsMatch(t, user.ChannelsIds, []string{cPub1.Id, cPub2.Id, cDM.Id})
case u3.Id:
assert.ElementsMatch(t, user.ChannelsIds, []string{cPub2.Id, cDM.Id})
}
}
// Testing pagination
res2List, err := ss.User().GetUsersBatchForIndexing(u1.CreateAt-1, "", 1)
require.NoError(t, err)
assert.Len(t, res2List, 1)
res2List, err = ss.User().GetUsersBatchForIndexing(res2List[0].CreateAt, res2List[0].Id, 2)
require.NoError(t, err)
assert.Len(t, res2List, 2)
res2List, err = ss.User().GetUsersBatchForIndexing(res2List[1].CreateAt, res2List[1].Id, 2)
require.NoError(t, err)
assert.Len(t, res2List, 0)
}
func testUserStoreGetTeamGroupUsers(t *testing.T, ss store.Store) {
// create team
id := model.NewId()
team, err := ss.Team().Save(&model.Team{
DisplayName: "dn_" + id,
Name: "n-" + id,
Email: id + "@test.com",
Type: model.TeamInvite,
})
require.NoError(t, err)
require.NotNil(t, team)
// create users
var testUsers []*model.User
for i := 0; i < 3; i++ {
id = model.NewId()
user, userErr := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
})
require.NoError(t, userErr)
require.NotNil(t, user)
testUsers = append(testUsers, user)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
}
require.Len(t, testUsers, 3, "testUsers length doesn't meet required length")
userGroupA, userGroupB, userNoGroup := testUsers[0], testUsers[1], testUsers[2]
// add non-group-member to the team (to prove that the query isn't just returning all members)
_, nErr := ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: userNoGroup.Id,
}, 999)
require.NoError(t, nErr)
// create groups
var testGroups []*model.Group
for i := 0; i < 2; i++ {
id = model.NewId()
var group *model.Group
group, err = ss.Group().Create(&model.Group{
Name: model.NewString("n_" + id),
DisplayName: "dn_" + id,
Source: model.GroupSourceLdap,
RemoteId: model.NewString("ri_" + id),
})
require.NoError(t, err)
require.NotNil(t, group)
testGroups = append(testGroups, group)
}
require.Len(t, testGroups, 2, "testGroups length doesn't meet required length")
groupA, groupB := testGroups[0], testGroups[1]
// add members to groups
_, err = ss.Group().UpsertMember(groupA.Id, userGroupA.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(groupB.Id, userGroupB.Id)
require.NoError(t, err)
// association one group to team
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
GroupId: groupA.Id,
SyncableId: team.Id,
Type: model.GroupSyncableTypeTeam,
})
require.NoError(t, err)
var users []*model.User
requireNUsers := func(n int) {
users, err = ss.User().GetTeamGroupUsers(team.Id)
require.NoError(t, err)
require.NotNil(t, users)
require.Len(t, users, n)
}
// team not group constrained returns users
requireNUsers(1)
// update team to be group-constrained
team.GroupConstrained = model.NewBool(true)
team, err = ss.Team().Update(team)
require.NoError(t, err)
// still returns user (being group-constrained has no effect)
requireNUsers(1)
// associate other group to team
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
GroupId: groupB.Id,
SyncableId: team.Id,
Type: model.GroupSyncableTypeTeam,
})
require.NoError(t, err)
// should return users from all groups
// 2 users now that both groups have been associated to the team
requireNUsers(2)
// add team membership of allowed user
_, nErr = ss.Team().SaveMember(&model.TeamMember{
TeamId: team.Id,
UserId: userGroupA.Id,
}, 999)
require.NoError(t, nErr)
// ensure allowed member still returned by query
requireNUsers(2)
// delete team membership of allowed user
err = ss.Team().RemoveMember(team.Id, userGroupA.Id)
require.NoError(t, err)
// ensure removed allowed member still returned by query
requireNUsers(2)
}
func testUserStoreGetChannelGroupUsers(t *testing.T, ss store.Store) {
// create channel
id := model.NewId()
channel, nErr := ss.Channel().Save(&model.Channel{
DisplayName: "dn_" + id,
Name: "n-" + id,
Type: model.ChannelTypePrivate,
}, 999)
require.NoError(t, nErr)
require.NotNil(t, channel)
// create users
var testUsers []*model.User
for i := 0; i < 3; i++ {
id = model.NewId()
user, userErr := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
})
require.NoError(t, userErr)
require.NotNil(t, user)
testUsers = append(testUsers, user)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
}
require.Len(t, testUsers, 3, "testUsers length doesn't meet required length")
userGroupA, userGroupB, userNoGroup := testUsers[0], testUsers[1], testUsers[2]
// add non-group-member to the channel (to prove that the query isn't just returning all members)
_, err := ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel.Id,
UserId: userNoGroup.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
// create groups
var testGroups []*model.Group
for i := 0; i < 2; i++ {
id = model.NewId()
var group *model.Group
group, err = ss.Group().Create(&model.Group{
Name: model.NewString("n_" + id),
DisplayName: "dn_" + id,
Source: model.GroupSourceLdap,
RemoteId: model.NewString("ri_" + id),
})
require.NoError(t, err)
require.NotNil(t, group)
testGroups = append(testGroups, group)
}
require.Len(t, testGroups, 2, "testGroups length doesn't meet required length")
groupA, groupB := testGroups[0], testGroups[1]
// add members to groups
_, err = ss.Group().UpsertMember(groupA.Id, userGroupA.Id)
require.NoError(t, err)
_, err = ss.Group().UpsertMember(groupB.Id, userGroupB.Id)
require.NoError(t, err)
// association one group to channel
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
GroupId: groupA.Id,
SyncableId: channel.Id,
Type: model.GroupSyncableTypeChannel,
})
require.NoError(t, err)
var users []*model.User
requireNUsers := func(n int) {
users, err = ss.User().GetChannelGroupUsers(channel.Id)
require.NoError(t, err)
require.NotNil(t, users)
require.Len(t, users, n)
}
// channel not group constrained returns users
requireNUsers(1)
// update team to be group-constrained
channel.GroupConstrained = model.NewBool(true)
_, nErr = ss.Channel().Update(channel)
require.NoError(t, nErr)
// still returns user (being group-constrained has no effect)
requireNUsers(1)
// associate other group to team
_, err = ss.Group().CreateGroupSyncable(&model.GroupSyncable{
GroupId: groupB.Id,
SyncableId: channel.Id,
Type: model.GroupSyncableTypeChannel,
})
require.NoError(t, err)
// should return users from all groups
// 2 users now that both groups have been associated to the team
requireNUsers(2)
// add team membership of allowed user
_, err = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel.Id,
UserId: userGroupA.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, err)
// ensure allowed member still returned by query
requireNUsers(2)
// delete team membership of allowed user
err = ss.Channel().RemoveMember(channel.Id, userGroupA.Id)
require.NoError(t, err)
// ensure removed allowed member still returned by query
requireNUsers(2)
}
func testUserStorePromoteGuestToUser(t *testing.T, ss store.Store) {
// create users
t.Run("Must do nothing with regular user", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_user",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
teamId := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: true, SchemeUser: false}, 999)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "Channel name",
Name: "channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: true, SchemeUser: false, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
err = ss.User().PromoteGuestToUser(user.Id)
require.NoError(t, err)
updatedUser, err := ss.User().Get(context.Background(), user.Id)
require.NoError(t, err)
require.Equal(t, "system_user", updatedUser.Roles)
require.True(t, user.UpdateAt < updatedUser.UpdateAt)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId, user.Id)
require.NoError(t, nErr)
require.False(t, updatedTeamMember.SchemeGuest)
require.True(t, updatedTeamMember.SchemeUser)
updatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user.Id)
require.NoError(t, nErr)
require.False(t, updatedChannelMember.SchemeGuest)
require.True(t, updatedChannelMember.SchemeUser)
})
t.Run("Must do nothing with admin user", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_user system_admin",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
teamId := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: true, SchemeUser: false}, 999)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "Channel name",
Name: "channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: true, SchemeUser: false, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
err = ss.User().PromoteGuestToUser(user.Id)
require.NoError(t, err)
updatedUser, err := ss.User().Get(context.Background(), user.Id)
require.NoError(t, err)
require.Equal(t, "system_user system_admin", updatedUser.Roles)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId, user.Id)
require.NoError(t, nErr)
require.False(t, updatedTeamMember.SchemeGuest)
require.True(t, updatedTeamMember.SchemeUser)
updatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user.Id)
require.NoError(t, nErr)
require.False(t, updatedChannelMember.SchemeGuest)
require.True(t, updatedChannelMember.SchemeUser)
})
t.Run("Must work with guest user without teams or channels", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_guest",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
err = ss.User().PromoteGuestToUser(user.Id)
require.NoError(t, err)
updatedUser, err := ss.User().Get(context.Background(), user.Id)
require.NoError(t, err)
require.Equal(t, "system_user", updatedUser.Roles)
})
t.Run("Must work with guest user with teams but no channels", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_guest",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
teamId := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: true, SchemeUser: false}, 999)
require.NoError(t, nErr)
err = ss.User().PromoteGuestToUser(user.Id)
require.NoError(t, err)
updatedUser, err := ss.User().Get(context.Background(), user.Id)
require.NoError(t, err)
require.Equal(t, "system_user", updatedUser.Roles)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId, user.Id)
require.NoError(t, nErr)
require.False(t, updatedTeamMember.SchemeGuest)
require.True(t, updatedTeamMember.SchemeUser)
})
t.Run("Must work with guest user with teams and channels", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_guest",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
teamId := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: true, SchemeUser: false}, 999)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "Channel name",
Name: "channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: true, SchemeUser: false, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
err = ss.User().PromoteGuestToUser(user.Id)
require.NoError(t, err)
updatedUser, err := ss.User().Get(context.Background(), user.Id)
require.NoError(t, err)
require.Equal(t, "system_user", updatedUser.Roles)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId, user.Id)
require.NoError(t, nErr)
require.False(t, updatedTeamMember.SchemeGuest)
require.True(t, updatedTeamMember.SchemeUser)
updatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user.Id)
require.NoError(t, nErr)
require.False(t, updatedChannelMember.SchemeGuest)
require.True(t, updatedChannelMember.SchemeUser)
})
t.Run("Must work with guest user with teams and channels and custom role", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_guest custom_role",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
teamId := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: true, SchemeUser: false}, 999)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "Channel name",
Name: "channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: true, SchemeUser: false, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
err = ss.User().PromoteGuestToUser(user.Id)
require.NoError(t, err)
updatedUser, err := ss.User().Get(context.Background(), user.Id)
require.NoError(t, err)
require.Equal(t, "system_user custom_role", updatedUser.Roles)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId, user.Id)
require.NoError(t, nErr)
require.False(t, updatedTeamMember.SchemeGuest)
require.True(t, updatedTeamMember.SchemeUser)
updatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user.Id)
require.NoError(t, nErr)
require.False(t, updatedChannelMember.SchemeGuest)
require.True(t, updatedChannelMember.SchemeUser)
})
t.Run("Must no change any other user guest role", func(t *testing.T) {
id := model.NewId()
user1, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_guest",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user1.Id)) }()
teamId1 := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId1, UserId: user1.Id, SchemeGuest: true, SchemeUser: false}, 999)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
TeamId: teamId1,
DisplayName: "Channel name",
Name: "channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user1.Id, SchemeGuest: true, SchemeUser: false, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
id = model.NewId()
user2, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_guest",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user2.Id)) }()
teamId2 := model.NewId()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId2, UserId: user2.Id, SchemeGuest: true, SchemeUser: false}, 999)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user2.Id, SchemeGuest: true, SchemeUser: false, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
err = ss.User().PromoteGuestToUser(user1.Id)
require.NoError(t, err)
updatedUser, err := ss.User().Get(context.Background(), user1.Id)
require.NoError(t, err)
require.Equal(t, "system_user", updatedUser.Roles)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId1, user1.Id)
require.NoError(t, nErr)
require.False(t, updatedTeamMember.SchemeGuest)
require.True(t, updatedTeamMember.SchemeUser)
updatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user1.Id)
require.NoError(t, nErr)
require.False(t, updatedChannelMember.SchemeGuest)
require.True(t, updatedChannelMember.SchemeUser)
notUpdatedUser, err := ss.User().Get(context.Background(), user2.Id)
require.NoError(t, err)
require.Equal(t, "system_guest", notUpdatedUser.Roles)
notUpdatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId2, user2.Id)
require.NoError(t, nErr)
require.True(t, notUpdatedTeamMember.SchemeGuest)
require.False(t, notUpdatedTeamMember.SchemeUser)
notUpdatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user2.Id)
require.NoError(t, nErr)
require.True(t, notUpdatedChannelMember.SchemeGuest)
require.False(t, notUpdatedChannelMember.SchemeUser)
})
}
func testUserStoreDemoteUserToGuest(t *testing.T, ss store.Store) {
// create users
t.Run("Must do nothing with guest", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_guest",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
teamId := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: false, SchemeUser: true}, 999)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "Channel name",
Name: "channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: false, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
updatedUser, err := ss.User().DemoteUserToGuest(user.Id)
require.NoError(t, err)
require.Equal(t, "system_guest", updatedUser.Roles)
require.True(t, user.UpdateAt < updatedUser.UpdateAt)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId, updatedUser.Id)
require.NoError(t, nErr)
require.True(t, updatedTeamMember.SchemeGuest)
require.False(t, updatedTeamMember.SchemeUser)
updatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, updatedUser.Id)
require.NoError(t, nErr)
require.True(t, updatedChannelMember.SchemeGuest)
require.False(t, updatedChannelMember.SchemeUser)
})
t.Run("Must demote properly an admin user", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_user system_admin",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
teamId := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: true, SchemeUser: false}, 999)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "Channel name",
Name: "channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: true, SchemeUser: false, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
updatedUser, err := ss.User().DemoteUserToGuest(user.Id)
require.NoError(t, err)
require.Equal(t, "system_guest", updatedUser.Roles)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId, user.Id)
require.NoError(t, nErr)
require.True(t, updatedTeamMember.SchemeGuest)
require.False(t, updatedTeamMember.SchemeUser)
updatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user.Id)
require.NoError(t, nErr)
require.True(t, updatedChannelMember.SchemeGuest)
require.False(t, updatedChannelMember.SchemeUser)
})
t.Run("Must work with user without teams or channels", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_user",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
updatedUser, err := ss.User().DemoteUserToGuest(user.Id)
require.NoError(t, err)
require.Equal(t, "system_guest", updatedUser.Roles)
})
t.Run("Must work with user with teams but no channels", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_user",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
teamId := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: false, SchemeUser: true}, 999)
require.NoError(t, nErr)
updatedUser, err := ss.User().DemoteUserToGuest(user.Id)
require.NoError(t, err)
require.Equal(t, "system_guest", updatedUser.Roles)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId, user.Id)
require.NoError(t, nErr)
require.True(t, updatedTeamMember.SchemeGuest)
require.False(t, updatedTeamMember.SchemeUser)
})
t.Run("Must work with user with teams and channels", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_user",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
teamId := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: false, SchemeUser: true}, 999)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "Channel name",
Name: "channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: false, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
updatedUser, err := ss.User().DemoteUserToGuest(user.Id)
require.NoError(t, err)
require.Equal(t, "system_guest", updatedUser.Roles)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId, user.Id)
require.NoError(t, nErr)
require.True(t, updatedTeamMember.SchemeGuest)
require.False(t, updatedTeamMember.SchemeUser)
updatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user.Id)
require.NoError(t, nErr)
require.True(t, updatedChannelMember.SchemeGuest)
require.False(t, updatedChannelMember.SchemeUser)
})
t.Run("Must work with user with teams and channels and custom role", func(t *testing.T) {
id := model.NewId()
user, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_user custom_role",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user.Id)) }()
teamId := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id, SchemeGuest: false, SchemeUser: true}, 999)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
TeamId: teamId,
DisplayName: "Channel name",
Name: "channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user.Id, SchemeGuest: false, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
updatedUser, err := ss.User().DemoteUserToGuest(user.Id)
require.NoError(t, err)
require.Equal(t, "system_guest custom_role", updatedUser.Roles)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId, user.Id)
require.NoError(t, nErr)
require.True(t, updatedTeamMember.SchemeGuest)
require.False(t, updatedTeamMember.SchemeUser)
updatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user.Id)
require.NoError(t, nErr)
require.True(t, updatedChannelMember.SchemeGuest)
require.False(t, updatedChannelMember.SchemeUser)
})
t.Run("Must no change any other user role", func(t *testing.T) {
id := model.NewId()
user1, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_user",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user1.Id)) }()
teamId1 := model.NewId()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId1, UserId: user1.Id, SchemeGuest: false, SchemeUser: true}, 999)
require.NoError(t, nErr)
channel, nErr := ss.Channel().Save(&model.Channel{
TeamId: teamId1,
DisplayName: "Channel name",
Name: "channel-" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user1.Id, SchemeGuest: false, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
id = model.NewId()
user2, err := ss.User().Save(&model.User{
Email: id + "@test.com",
Username: "un_" + id,
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
Roles: "system_user",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user2.Id)) }()
teamId2 := model.NewId()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId2, UserId: user2.Id, SchemeGuest: false, SchemeUser: true}, 999)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: user2.Id, SchemeGuest: false, SchemeUser: true, NotifyProps: model.GetDefaultChannelNotifyProps()})
require.NoError(t, nErr)
updatedUser, err := ss.User().DemoteUserToGuest(user1.Id)
require.NoError(t, err)
require.Equal(t, "system_guest", updatedUser.Roles)
updatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId1, user1.Id)
require.NoError(t, nErr)
require.True(t, updatedTeamMember.SchemeGuest)
require.False(t, updatedTeamMember.SchemeUser)
updatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user1.Id)
require.NoError(t, nErr)
require.True(t, updatedChannelMember.SchemeGuest)
require.False(t, updatedChannelMember.SchemeUser)
notUpdatedUser, err := ss.User().Get(context.Background(), user2.Id)
require.NoError(t, err)
require.Equal(t, "system_user", notUpdatedUser.Roles)
notUpdatedTeamMember, nErr := ss.Team().GetMember(context.Background(), teamId2, user2.Id)
require.NoError(t, nErr)
require.False(t, notUpdatedTeamMember.SchemeGuest)
require.True(t, notUpdatedTeamMember.SchemeUser)
notUpdatedChannelMember, nErr := ss.Channel().GetMember(context.Background(), channel.Id, user2.Id)
require.NoError(t, nErr)
require.False(t, notUpdatedChannelMember.SchemeGuest)
require.True(t, notUpdatedChannelMember.SchemeUser)
})
}
func testDeactivateGuests(t *testing.T, ss store.Store) {
// create users
t.Run("Must disable all guests and no regular user or already deactivated users", func(t *testing.T) {
guest1Random := model.NewId()
guest1, err := ss.User().Save(&model.User{
Email: guest1Random + "@test.com",
Username: "un_" + guest1Random,
Nickname: "nn_" + guest1Random,
FirstName: "f_" + guest1Random,
LastName: "l_" + guest1Random,
Password: "Password1",
Roles: "system_guest",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(guest1.Id)) }()
guest2Random := model.NewId()
guest2, err := ss.User().Save(&model.User{
Email: guest2Random + "@test.com",
Username: "un_" + guest2Random,
Nickname: "nn_" + guest2Random,
FirstName: "f_" + guest2Random,
LastName: "l_" + guest2Random,
Password: "Password1",
Roles: "system_guest",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(guest2.Id)) }()
guest3Random := model.NewId()
guest3, err := ss.User().Save(&model.User{
Email: guest3Random + "@test.com",
Username: "un_" + guest3Random,
Nickname: "nn_" + guest3Random,
FirstName: "f_" + guest3Random,
LastName: "l_" + guest3Random,
Password: "Password1",
Roles: "system_guest",
DeleteAt: 10,
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(guest3.Id)) }()
regularUserRandom := model.NewId()
regularUser, err := ss.User().Save(&model.User{
Email: regularUserRandom + "@test.com",
Username: "un_" + regularUserRandom,
Nickname: "nn_" + regularUserRandom,
FirstName: "f_" + regularUserRandom,
LastName: "l_" + regularUserRandom,
Password: "Password1",
Roles: "system_user",
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(regularUser.Id)) }()
ids, err := ss.User().DeactivateGuests()
require.NoError(t, err)
assert.ElementsMatch(t, []string{guest1.Id, guest2.Id}, ids)
u, err := ss.User().Get(context.Background(), guest1.Id)
require.NoError(t, err)
assert.NotEqual(t, u.DeleteAt, int64(0))
u, err = ss.User().Get(context.Background(), guest2.Id)
require.NoError(t, err)
assert.NotEqual(t, u.DeleteAt, int64(0))
u, err = ss.User().Get(context.Background(), guest3.Id)
require.NoError(t, err)
assert.Equal(t, u.DeleteAt, int64(10))
u, err = ss.User().Get(context.Background(), regularUser.Id)
require.NoError(t, err)
assert.Equal(t, u.DeleteAt, int64(0))
})
}
func testUserStoreResetLastPictureUpdate(t *testing.T, ss store.Store) {
u1 := &model.User{}
u1.Email = MakeEmail()
_, err := ss.User().Save(u1)
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
err = ss.User().UpdateLastPictureUpdate(u1.Id)
require.NoError(t, err)
user, err := ss.User().Get(context.Background(), u1.Id)
require.NoError(t, err)
assert.NotZero(t, user.LastPictureUpdate)
assert.NotZero(t, user.UpdateAt)
// Ensure update at timestamp changes
time.Sleep(time.Millisecond)
err = ss.User().ResetLastPictureUpdate(u1.Id)
require.NoError(t, err)
ss.User().InvalidateProfileCacheForUser(u1.Id)
user2, err := ss.User().Get(context.Background(), u1.Id)
require.NoError(t, err)
assert.True(t, user2.UpdateAt > user.UpdateAt)
assert.Zero(t, user2.LastPictureUpdate)
}
func testGetKnownUsers(t *testing.T, ss store.Store) {
teamId := model.NewId()
u1, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u2" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)
require.NoError(t, nErr)
u3, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u3" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Bot().Save(&model.Bot{
UserId: u3.Id,
Username: u3.Username,
OwnerId: u1.Id,
})
require.NoError(t, nErr)
u3.IsBot = true
defer func() { require.NoError(t, ss.Bot().PermanentDelete(u3.Id)) }()
u4, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "u4" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u4.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u4.Id}, -1)
require.NoError(t, nErr)
ch1 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in channel",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypeOpen,
}
c1, nErr := ss.Channel().Save(ch1, -1)
require.NoError(t, nErr)
ch2 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in private",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypePrivate,
}
c2, nErr := ss.Channel().Save(ch2, -1)
require.NoError(t, nErr)
ch3 := &model.Channel{
TeamId: teamId,
DisplayName: "Profiles in private",
Name: "profiles-" + model.NewId(),
Type: model.ChannelTypePrivate,
}
c3, nErr := ss.Channel().Save(ch3, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: u2.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c2.Id,
UserId: u3.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c2.Id,
UserId: u1.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c3.Id,
UserId: u4.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
require.NoError(t, nErr)
t.Run("get know users sharing no channels", func(t *testing.T) {
userIds, err := ss.User().GetKnownUsers(u4.Id)
require.NoError(t, err)
assert.Empty(t, userIds)
})
t.Run("get know users sharing one channel", func(t *testing.T) {
userIds, err := ss.User().GetKnownUsers(u3.Id)
require.NoError(t, err)
assert.Len(t, userIds, 1)
assert.Equal(t, userIds[0], u1.Id)
})
t.Run("get know users sharing multiple channels", func(t *testing.T) {
userIds, err := ss.User().GetKnownUsers(u1.Id)
require.NoError(t, err)
assert.Len(t, userIds, 2)
assert.ElementsMatch(t, userIds, []string{u2.Id, u3.Id})
})
}
func testIsEmpty(t *testing.T, ss store.Store) {
ok, err := ss.User().IsEmpty(false)
require.NoError(t, err)
require.True(t, ok)
ok, err = ss.User().IsEmpty(true)
require.NoError(t, err)
require.True(t, ok)
u := &model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
u, err = ss.User().Save(u)
require.NoError(t, err)
ok, err = ss.User().IsEmpty(false)
require.NoError(t, err)
require.False(t, ok)
ok, err = ss.User().IsEmpty(true)
require.NoError(t, err)
require.False(t, ok)
b := &model.Bot{
UserId: u.Id,
OwnerId: model.NewId(),
Username: model.NewId(),
}
_, err = ss.Bot().Save(b)
require.NoError(t, err)
ok, err = ss.User().IsEmpty(false)
require.NoError(t, err)
require.False(t, ok)
ok, err = ss.User().IsEmpty(true)
require.NoError(t, err)
require.True(t, ok)
err = ss.User().PermanentDelete(u.Id)
require.NoError(t, err)
ok, err = ss.User().IsEmpty(false)
require.NoError(t, err)
require.True(t, ok)
}
func testGetUsersWithInvalidEmails(t *testing.T, ss store.Store) {
u1, err := ss.User().Save(&model.User{
Email: "ben@invalid.mattermost.com",
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
users, err := ss.User().GetUsersWithInvalidEmails(0, 50, "localhost,simulator.amazonses.com")
require.NoError(t, err)
assert.Len(t, users, 1)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestUserTermsOfServiceStore(t *testing.T, ss store.Store) {
t.Run("TestSaveUserTermsOfService", func(t *testing.T) { testSaveUserTermsOfService(t, ss) })
t.Run("TestGetByUserTermsOfService", func(t *testing.T) { testGetByUserTermsOfService(t, ss) })
t.Run("TestDeleteUserTermsOfService", func(t *testing.T) { testDeleteUserTermsOfService(t, ss) })
}
func testSaveUserTermsOfService(t *testing.T, ss store.Store) {
userTermsOfService := &model.UserTermsOfService{
UserId: model.NewId(),
TermsOfServiceId: model.NewId(),
}
savedUserTermsOfService, err := ss.UserTermsOfService().Save(userTermsOfService)
require.NoError(t, err)
assert.Equal(t, userTermsOfService.UserId, savedUserTermsOfService.UserId)
assert.Equal(t, userTermsOfService.TermsOfServiceId, savedUserTermsOfService.TermsOfServiceId)
assert.NotEmpty(t, savedUserTermsOfService.CreateAt)
// Check we can save a new terms of service id (MM-41611)
newUserTermsOfService := &model.UserTermsOfService{
UserId: userTermsOfService.UserId,
TermsOfServiceId: model.NewId(),
}
savedUserTermsOfService, err = ss.UserTermsOfService().Save(newUserTermsOfService)
require.NoError(t, err)
assert.Equal(t, newUserTermsOfService.UserId, savedUserTermsOfService.UserId)
assert.Equal(t, newUserTermsOfService.TermsOfServiceId, savedUserTermsOfService.TermsOfServiceId)
assert.NotEmpty(t, savedUserTermsOfService.CreateAt)
}
func testGetByUserTermsOfService(t *testing.T, ss store.Store) {
userTermsOfService := &model.UserTermsOfService{
UserId: model.NewId(),
TermsOfServiceId: model.NewId(),
}
_, err := ss.UserTermsOfService().Save(userTermsOfService)
require.NoError(t, err)
fetchedUserTermsOfService, err := ss.UserTermsOfService().GetByUser(userTermsOfService.UserId)
require.NoError(t, err)
assert.Equal(t, userTermsOfService.UserId, fetchedUserTermsOfService.UserId)
assert.Equal(t, userTermsOfService.TermsOfServiceId, fetchedUserTermsOfService.TermsOfServiceId)
assert.NotEmpty(t, fetchedUserTermsOfService.CreateAt)
}
func testDeleteUserTermsOfService(t *testing.T, ss store.Store) {
userTermsOfService := &model.UserTermsOfService{
UserId: model.NewId(),
TermsOfServiceId: model.NewId(),
}
_, err := ss.UserTermsOfService().Save(userTermsOfService)
require.NoError(t, err)
_, err = ss.UserTermsOfService().GetByUser(userTermsOfService.UserId)
require.NoError(t, err)
err = ss.UserTermsOfService().Delete(userTermsOfService.UserId, userTermsOfService.TermsOfServiceId)
require.NoError(t, err)
_, err = ss.UserTermsOfService().GetByUser(userTermsOfService.UserId)
var nfErr *store.ErrNotFound
assert.Error(t, err)
assert.True(t, errors.As(err, &nfErr))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"github.com/mattermost/mattermost-server/v6/model"
)
// This function has a copy of it in app/helper_test
// NewTestId is used for testing as a replacement for model.NewId(). It is a [A-Z0-9] string 26
// characters long. It replaces every odd character with a digit.
func NewTestId() string {
newId := []byte(model.NewId())
for i := 1; i < len(newId); i = i + 2 {
newId[i] = 48 + newId[i-1]%10
}
return string(newId)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
func TestWebhookStore(t *testing.T, ss store.Store) {
t.Run("SaveIncoming", func(t *testing.T) { testWebhookStoreSaveIncoming(t, ss) })
t.Run("UpdateIncoming", func(t *testing.T) { testWebhookStoreUpdateIncoming(t, ss) })
t.Run("GetIncoming", func(t *testing.T) { testWebhookStoreGetIncoming(t, ss) })
t.Run("GetIncomingList", func(t *testing.T) { testWebhookStoreGetIncomingList(t, ss) })
t.Run("GetIncomingListByUser", func(t *testing.T) { testWebhookStoreGetIncomingListByUser(t, ss) })
t.Run("GetIncomingByTeam", func(t *testing.T) { testWebhookStoreGetIncomingByTeam(t, ss) })
t.Run("GetIncomingByTeamByUser", func(t *testing.T) { TestWebhookStoreGetIncomingByTeamByUser(t, ss) })
t.Run("GetIncomingByTeamByChannel", func(t *testing.T) { testWebhookStoreGetIncomingByChannel(t, ss) })
t.Run("DeleteIncoming", func(t *testing.T) { testWebhookStoreDeleteIncoming(t, ss) })
t.Run("DeleteIncomingByChannel", func(t *testing.T) { testWebhookStoreDeleteIncomingByChannel(t, ss) })
t.Run("DeleteIncomingByUser", func(t *testing.T) { testWebhookStoreDeleteIncomingByUser(t, ss) })
t.Run("SaveOutgoing", func(t *testing.T) { testWebhookStoreSaveOutgoing(t, ss) })
t.Run("GetOutgoing", func(t *testing.T) { testWebhookStoreGetOutgoing(t, ss) })
t.Run("GetOutgoingList", func(t *testing.T) { testWebhookStoreGetOutgoingList(t, ss) })
t.Run("GetOutgoingListByUser", func(t *testing.T) { testWebhookStoreGetOutgoingListByUser(t, ss) })
t.Run("GetOutgoingByChannel", func(t *testing.T) { testWebhookStoreGetOutgoingByChannel(t, ss) })
t.Run("GetOutgoingByChannelByUser", func(t *testing.T) { testWebhookStoreGetOutgoingByChannelByUser(t, ss) })
t.Run("GetOutgoingByTeam", func(t *testing.T) { testWebhookStoreGetOutgoingByTeam(t, ss) })
t.Run("GetOutgoingByTeamByUser", func(t *testing.T) { testWebhookStoreGetOutgoingByTeamByUser(t, ss) })
t.Run("DeleteOutgoing", func(t *testing.T) { testWebhookStoreDeleteOutgoing(t, ss) })
t.Run("DeleteOutgoingByChannel", func(t *testing.T) { testWebhookStoreDeleteOutgoingByChannel(t, ss) })
t.Run("DeleteOutgoingByUser", func(t *testing.T) { testWebhookStoreDeleteOutgoingByUser(t, ss) })
t.Run("UpdateOutgoing", func(t *testing.T) { testWebhookStoreUpdateOutgoing(t, ss) })
t.Run("CountIncoming", func(t *testing.T) { testWebhookStoreCountIncoming(t, ss) })
t.Run("CountOutgoing", func(t *testing.T) { testWebhookStoreCountOutgoing(t, ss) })
}
func testWebhookStoreSaveIncoming(t *testing.T, ss store.Store) {
o1 := buildIncomingWebhook()
_, err := ss.Webhook().SaveIncoming(o1)
require.NoError(t, err, "couldn't save item")
_, err = ss.Webhook().SaveIncoming(o1)
require.Error(t, err, "shouldn't be able to update from save")
}
func testWebhookStoreUpdateIncoming(t *testing.T, ss store.Store) {
var err error
o1 := buildIncomingWebhook()
o1, err = ss.Webhook().SaveIncoming(o1)
require.NoError(t, err, "unable to save webhook")
previousUpdatedAt := o1.UpdateAt
o1.DisplayName = "TestHook"
time.Sleep(10 * time.Millisecond)
webhook, err := ss.Webhook().UpdateIncoming(o1)
require.NoError(t, err)
require.NotEqual(t, webhook.UpdateAt, previousUpdatedAt, "should have updated the UpdatedAt of the hook")
require.Equal(t, "TestHook", webhook.DisplayName, "display name is not updated")
}
func testWebhookStoreGetIncoming(t *testing.T, ss store.Store) {
var err error
o1 := buildIncomingWebhook()
o1, err = ss.Webhook().SaveIncoming(o1)
require.NoError(t, err, "unable to save webhook")
webhook, err := ss.Webhook().GetIncoming(o1.Id, false)
require.NoError(t, err)
require.Equal(t, webhook.CreateAt, o1.CreateAt, "invalid returned webhook")
webhook, err = ss.Webhook().GetIncoming(o1.Id, true)
require.NoError(t, err)
require.Equal(t, webhook.CreateAt, o1.CreateAt, "invalid returned webhook")
_, err = ss.Webhook().GetIncoming("123", false)
require.Error(t, err, "Missing id should have failed")
_, err = ss.Webhook().GetIncoming("123", true)
require.Error(t, err, "Missing id should have failed")
_, err = ss.Webhook().GetIncoming("123", true)
require.Error(t, err)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr), "Should have set the status as not found for missing id")
}
func testWebhookStoreGetIncomingList(t *testing.T, ss store.Store) {
o1 := &model.IncomingWebhook{}
o1.ChannelId = model.NewId()
o1.UserId = model.NewId()
o1.TeamId = model.NewId()
var err error
o1, err = ss.Webhook().SaveIncoming(o1)
require.NoError(t, err, "unable to save webhook")
hooks, err := ss.Webhook().GetIncomingList(0, 1000)
require.NoError(t, err)
found := false
for _, hook := range hooks {
if hook.Id == o1.Id {
found = true
}
}
require.True(t, found, "missing webhook")
hooks, err = ss.Webhook().GetIncomingList(0, 1)
require.NoError(t, err)
require.Len(t, hooks, 1, "only 1 should be returned")
}
func testWebhookStoreGetIncomingListByUser(t *testing.T, ss store.Store) {
o1 := &model.IncomingWebhook{}
o1.ChannelId = model.NewId()
o1.UserId = model.NewId()
o1.TeamId = model.NewId()
o1, err := ss.Webhook().SaveIncoming(o1)
require.NoError(t, err)
t.Run("GetIncomingListByUser, known user filtered", func(t *testing.T) {
hooks, err := ss.Webhook().GetIncomingListByUser(o1.UserId, 0, 100)
require.NoError(t, err)
require.Equal(t, 1, len(hooks))
require.Equal(t, o1.CreateAt, hooks[0].CreateAt)
})
t.Run("GetIncomingListByUser, unknown user filtered", func(t *testing.T) {
hooks, err := ss.Webhook().GetIncomingListByUser("123465", 0, 100)
require.NoError(t, err)
require.Equal(t, 0, len(hooks))
})
}
func testWebhookStoreGetIncomingByTeam(t *testing.T, ss store.Store) {
var err error
o1 := buildIncomingWebhook()
o1, err = ss.Webhook().SaveIncoming(o1)
require.NoError(t, err)
hooks, err := ss.Webhook().GetIncomingByTeam(o1.TeamId, 0, 100)
require.NoError(t, err)
require.Equal(t, hooks[0].CreateAt, o1.CreateAt, "invalid returned webhook")
hooks, err = ss.Webhook().GetIncomingByTeam("123", 0, 100)
require.NoError(t, err)
require.Empty(t, hooks, "no webhooks should have returned")
}
func TestWebhookStoreGetIncomingByTeamByUser(t *testing.T, ss store.Store) {
var err error
o1 := buildIncomingWebhook()
o1, err = ss.Webhook().SaveIncoming(o1)
require.NoError(t, err)
o2 := buildIncomingWebhook()
o2.TeamId = o1.TeamId //Set both to the same team
o2, err = ss.Webhook().SaveIncoming(o2)
require.NoError(t, err)
t.Run("GetIncomingByTeamByUser, no user filter", func(t *testing.T) {
hooks, err := ss.Webhook().GetIncomingByTeam(o1.TeamId, 0, 100)
require.NoError(t, err)
require.Equal(t, len(hooks), 2)
})
t.Run("GetIncomingByTeamByUser, known user filtered", func(t *testing.T) {
hooks, err := ss.Webhook().GetIncomingByTeamByUser(o1.TeamId, o1.UserId, 0, 100)
require.NoError(t, err)
require.Equal(t, len(hooks), 1)
require.Equal(t, hooks[0].CreateAt, o1.CreateAt)
})
t.Run("GetIncomingByTeamByUser, unknown user filtered", func(t *testing.T) {
hooks, err := ss.Webhook().GetIncomingByTeamByUser(o2.TeamId, "123465", 0, 100)
require.NoError(t, err)
require.Equal(t, len(hooks), 0)
})
}
func testWebhookStoreGetIncomingByChannel(t *testing.T, ss store.Store) {
o1 := buildIncomingWebhook()
o1, err := ss.Webhook().SaveIncoming(o1)
require.NoError(t, err, "unable to save webhook")
webhooks, err := ss.Webhook().GetIncomingByChannel(o1.ChannelId)
require.NoError(t, err)
require.Equal(t, webhooks[0].CreateAt, o1.CreateAt, "invalid returned webhook")
webhooks, err = ss.Webhook().GetIncomingByChannel("123")
require.NoError(t, err)
require.Empty(t, webhooks, "no webhooks should have returned")
}
func testWebhookStoreDeleteIncoming(t *testing.T, ss store.Store) {
var err error
o1 := buildIncomingWebhook()
o1, err = ss.Webhook().SaveIncoming(o1)
require.NoError(t, err, "unable to save webhook")
webhook, err := ss.Webhook().GetIncoming(o1.Id, true)
require.NoError(t, err)
require.Equal(t, webhook.CreateAt, o1.CreateAt, "invalid returned webhook")
err = ss.Webhook().DeleteIncoming(o1.Id, model.GetMillis())
require.NoError(t, err)
_, err = ss.Webhook().GetIncoming(o1.Id, true)
require.Error(t, err)
}
func testWebhookStoreDeleteIncomingByChannel(t *testing.T, ss store.Store) {
var err error
o1 := buildIncomingWebhook()
o1, err = ss.Webhook().SaveIncoming(o1)
require.NoError(t, err, "unable to save webhook")
webhook, err := ss.Webhook().GetIncoming(o1.Id, true)
require.NoError(t, err)
require.Equal(t, webhook.CreateAt, o1.CreateAt, "invalid returned webhook")
err = ss.Webhook().PermanentDeleteIncomingByChannel(o1.ChannelId)
require.NoError(t, err)
_, err = ss.Webhook().GetIncoming(o1.Id, true)
require.Error(t, err, "Missing id should have failed")
}
func testWebhookStoreDeleteIncomingByUser(t *testing.T, ss store.Store) {
var err error
o1 := buildIncomingWebhook()
o1, err = ss.Webhook().SaveIncoming(o1)
require.NoError(t, err, "unable to save webhook")
webhook, err := ss.Webhook().GetIncoming(o1.Id, true)
require.NoError(t, err)
require.Equal(t, webhook.CreateAt, o1.CreateAt, "invalid returned webhook")
err = ss.Webhook().PermanentDeleteIncomingByUser(o1.UserId)
require.NoError(t, err)
_, err = ss.Webhook().GetIncoming(o1.Id, true)
require.Error(t, err, "Missing id should have failed")
}
func buildIncomingWebhook() *model.IncomingWebhook {
o1 := &model.IncomingWebhook{}
o1.ChannelId = model.NewId()
o1.UserId = model.NewId()
o1.TeamId = model.NewId()
return o1
}
func testWebhookStoreSaveOutgoing(t *testing.T, ss store.Store) {
o1 := model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1.Username = "test-user-name"
o1.IconURL = "http://nowhere.com/icon"
_, err := ss.Webhook().SaveOutgoing(&o1)
require.NoError(t, err, "couldn't save item")
_, err = ss.Webhook().SaveOutgoing(&o1)
require.Error(t, err, "shouldn't be able to update from save")
}
func testWebhookStoreGetOutgoing(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1.Username = "test-user-name"
o1.IconURL = "http://nowhere.com/icon"
o1, _ = ss.Webhook().SaveOutgoing(o1)
webhook, err := ss.Webhook().GetOutgoing(o1.Id)
require.NoError(t, err)
require.Equal(t, webhook.CreateAt, o1.CreateAt, "invalid returned webhook")
_, err = ss.Webhook().GetOutgoing("123")
require.Error(t, err, "Missing id should have failed")
}
func testWebhookStoreGetOutgoingListByUser(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1, err := ss.Webhook().SaveOutgoing(o1)
require.NoError(t, err)
t.Run("GetOutgoingListByUser, known user filtered", func(t *testing.T) {
hooks, err := ss.Webhook().GetOutgoingListByUser(o1.CreatorId, 0, 100)
require.NoError(t, err)
require.Equal(t, 1, len(hooks))
require.Equal(t, o1.CreateAt, hooks[0].CreateAt)
})
t.Run("GetOutgoingListByUser, unknown user filtered", func(t *testing.T) {
hooks, err := ss.Webhook().GetOutgoingListByUser("123465", 0, 100)
require.NoError(t, err)
require.Equal(t, 0, len(hooks))
})
}
func testWebhookStoreGetOutgoingList(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1, _ = ss.Webhook().SaveOutgoing(o1)
o2 := &model.OutgoingWebhook{}
o2.ChannelId = model.NewId()
o2.CreatorId = model.NewId()
o2.TeamId = model.NewId()
o2.CallbackURLs = []string{"http://nowhere.com/"}
o2, _ = ss.Webhook().SaveOutgoing(o2)
r1, err := ss.Webhook().GetOutgoingList(0, 1000)
require.NoError(t, err)
hooks := r1
found1 := false
found2 := false
for _, hook := range hooks {
if hook.CreateAt != o1.CreateAt {
found1 = true
}
if hook.CreateAt != o2.CreateAt {
found2 = true
}
}
require.True(t, found1, "missing hook1")
require.True(t, found2, "missing hook2")
result, err := ss.Webhook().GetOutgoingList(0, 2)
require.NoError(t, err)
require.Len(t, result, 2, "wrong number of hooks returned")
}
func testWebhookStoreGetOutgoingByChannel(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1, _ = ss.Webhook().SaveOutgoing(o1)
r1, err := ss.Webhook().GetOutgoingByChannel(o1.ChannelId, 0, 100)
require.NoError(t, err)
require.Equal(t, r1[0].CreateAt, o1.CreateAt, "invalid returned webhook")
result, err := ss.Webhook().GetOutgoingByChannel("123", -1, -1)
require.NoError(t, err)
require.Empty(t, result, "no webhooks should have returned")
}
func testWebhookStoreGetOutgoingByChannelByUser(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1, err := ss.Webhook().SaveOutgoing(o1)
require.NoError(t, err)
o2 := &model.OutgoingWebhook{}
o2.ChannelId = o1.ChannelId
o2.CreatorId = model.NewId()
o2.TeamId = model.NewId()
o2.CallbackURLs = []string{"http://nowhere.com/"}
_, err = ss.Webhook().SaveOutgoing(o2)
require.NoError(t, err)
t.Run("GetOutgoingByChannelByUser, no user filter", func(t *testing.T) {
hooks, err := ss.Webhook().GetOutgoingByChannel(o1.ChannelId, 0, 100)
require.NoError(t, err)
require.Equal(t, len(hooks), 2)
})
t.Run("GetOutgoingByChannelByUser, known user filtered", func(t *testing.T) {
hooks, err := ss.Webhook().GetOutgoingByChannelByUser(o1.ChannelId, o1.CreatorId, 0, 100)
require.NoError(t, err)
require.Equal(t, 1, len(hooks))
require.Equal(t, o1.CreateAt, hooks[0].CreateAt)
})
t.Run("GetOutgoingByChannelByUser, unknown user filtered", func(t *testing.T) {
hooks, err := ss.Webhook().GetOutgoingByChannelByUser(o1.ChannelId, "123465", 0, 100)
require.NoError(t, err)
require.Equal(t, 0, len(hooks))
})
}
func testWebhookStoreGetOutgoingByTeam(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1, _ = ss.Webhook().SaveOutgoing(o1)
r1, err := ss.Webhook().GetOutgoingByTeam(o1.TeamId, 0, 100)
require.NoError(t, err)
require.Equal(t, r1[0].CreateAt, o1.CreateAt, "invalid returned webhook")
result, err := ss.Webhook().GetOutgoingByTeam("123", -1, -1)
require.NoError(t, err)
require.Empty(t, result, "no webhooks should have returned")
}
func testWebhookStoreGetOutgoingByTeamByUser(t *testing.T, ss store.Store) {
var err error
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1, err = ss.Webhook().SaveOutgoing(o1)
require.NoError(t, err)
o2 := &model.OutgoingWebhook{}
o2.ChannelId = model.NewId()
o2.CreatorId = model.NewId()
o2.TeamId = o1.TeamId
o2.CallbackURLs = []string{"http://nowhere.com/"}
o2, err = ss.Webhook().SaveOutgoing(o2)
require.NoError(t, err)
t.Run("GetOutgoingByTeamByUser, no user filter", func(t *testing.T) {
hooks, err := ss.Webhook().GetOutgoingByTeam(o1.TeamId, 0, 100)
require.NoError(t, err)
require.Equal(t, len(hooks), 2)
})
t.Run("GetOutgoingByTeamByUser, known user filtered", func(t *testing.T) {
hooks, err := ss.Webhook().GetOutgoingByTeamByUser(o1.TeamId, o1.CreatorId, 0, 100)
require.NoError(t, err)
require.Equal(t, len(hooks), 1)
require.Equal(t, hooks[0].CreateAt, o1.CreateAt)
})
t.Run("GetOutgoingByTeamByUser, unknown user filtered", func(t *testing.T) {
hooks, err := ss.Webhook().GetOutgoingByTeamByUser(o2.TeamId, "123465", 0, 100)
require.NoError(t, err)
require.Equal(t, len(hooks), 0)
})
}
func testWebhookStoreDeleteOutgoing(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1, _ = ss.Webhook().SaveOutgoing(o1)
webhook, err := ss.Webhook().GetOutgoing(o1.Id)
require.NoError(t, err)
require.Equal(t, webhook.CreateAt, o1.CreateAt, "invalid returned webhook")
err = ss.Webhook().DeleteOutgoing(o1.Id, model.GetMillis())
require.NoError(t, err)
_, err = ss.Webhook().GetOutgoing(o1.Id)
require.Error(t, err, "Missing id should have failed")
}
func testWebhookStoreDeleteOutgoingByChannel(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1, _ = ss.Webhook().SaveOutgoing(o1)
webhook, err := ss.Webhook().GetOutgoing(o1.Id)
require.NoError(t, err)
require.Equal(t, webhook.CreateAt, o1.CreateAt, "invalid returned webhook")
err = ss.Webhook().PermanentDeleteOutgoingByChannel(o1.ChannelId)
require.NoError(t, err)
_, err = ss.Webhook().GetOutgoing(o1.Id)
require.Error(t, err, "Missing id should have failed")
}
func testWebhookStoreDeleteOutgoingByUser(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1, _ = ss.Webhook().SaveOutgoing(o1)
webhook, err := ss.Webhook().GetOutgoing(o1.Id)
require.NoError(t, err)
require.Equal(t, webhook.CreateAt, o1.CreateAt, "invalid returned webhook")
err = ss.Webhook().PermanentDeleteOutgoingByUser(o1.CreatorId)
require.NoError(t, err)
_, err = ss.Webhook().GetOutgoing(o1.Id)
require.Error(t, err, "Missing id should have failed")
}
func testWebhookStoreUpdateOutgoing(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
o1.Username = "test-user-name"
o1.IconURL = "http://nowhere.com/icon"
o1, _ = ss.Webhook().SaveOutgoing(o1)
o1.Token = model.NewId()
o1.Username = "another-test-user-name"
_, err := ss.Webhook().UpdateOutgoing(o1)
require.NoError(t, err)
}
func testWebhookStoreCountIncoming(t *testing.T, ss store.Store) {
o1 := &model.IncomingWebhook{}
o1.ChannelId = model.NewId()
o1.UserId = model.NewId()
o1.TeamId = model.NewId()
_, _ = ss.Webhook().SaveIncoming(o1)
c, err := ss.Webhook().AnalyticsIncomingCount("")
require.NoError(t, err)
require.NotEqual(t, 0, c, "should have at least 1 incoming hook")
}
func testWebhookStoreCountOutgoing(t *testing.T, ss store.Store) {
o1 := &model.OutgoingWebhook{}
o1.ChannelId = model.NewId()
o1.CreatorId = model.NewId()
o1.TeamId = model.NewId()
o1.CallbackURLs = []string{"http://nowhere.com/"}
_, err := ss.Webhook().SaveOutgoing(o1)
require.NoError(t, err)
r, err := ss.Webhook().AnalyticsOutgoingCount("")
require.NoError(t, err)
require.NotEqual(t, 0, r, "should have at least 1 outgoing hook")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Code generated by "make store-layers"
// DO NOT EDIT
package timerlayer
import (
"context"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
)
type TimerLayer struct {
store.Store
Metrics einterfaces.MetricsInterface
AuditStore store.AuditStore
BotStore store.BotStore
ChannelStore store.ChannelStore
ChannelMemberHistoryStore store.ChannelMemberHistoryStore
ClusterDiscoveryStore store.ClusterDiscoveryStore
CommandStore store.CommandStore
CommandWebhookStore store.CommandWebhookStore
ComplianceStore store.ComplianceStore
DraftStore store.DraftStore
EmojiStore store.EmojiStore
FileInfoStore store.FileInfoStore
GroupStore store.GroupStore
JobStore store.JobStore
LicenseStore store.LicenseStore
LinkMetadataStore store.LinkMetadataStore
NotifyAdminStore store.NotifyAdminStore
OAuthStore store.OAuthStore
PluginStore store.PluginStore
PostStore store.PostStore
PostAcknowledgementStore store.PostAcknowledgementStore
PostPriorityStore store.PostPriorityStore
PreferenceStore store.PreferenceStore
ProductNoticesStore store.ProductNoticesStore
ReactionStore store.ReactionStore
RemoteClusterStore store.RemoteClusterStore
RetentionPolicyStore store.RetentionPolicyStore
RoleStore store.RoleStore
SchemeStore store.SchemeStore
SessionStore store.SessionStore
SharedChannelStore store.SharedChannelStore
StatusStore store.StatusStore
SystemStore store.SystemStore
TeamStore store.TeamStore
TermsOfServiceStore store.TermsOfServiceStore
ThreadStore store.ThreadStore
TokenStore store.TokenStore
TrueUpReviewStore store.TrueUpReviewStore
UploadSessionStore store.UploadSessionStore
UserStore store.UserStore
UserAccessTokenStore store.UserAccessTokenStore
UserTermsOfServiceStore store.UserTermsOfServiceStore
WebhookStore store.WebhookStore
}
func (s *TimerLayer) Audit() store.AuditStore {
return s.AuditStore
}
func (s *TimerLayer) Bot() store.BotStore {
return s.BotStore
}
func (s *TimerLayer) Channel() store.ChannelStore {
return s.ChannelStore
}
func (s *TimerLayer) ChannelMemberHistory() store.ChannelMemberHistoryStore {
return s.ChannelMemberHistoryStore
}
func (s *TimerLayer) ClusterDiscovery() store.ClusterDiscoveryStore {
return s.ClusterDiscoveryStore
}
func (s *TimerLayer) Command() store.CommandStore {
return s.CommandStore
}
func (s *TimerLayer) CommandWebhook() store.CommandWebhookStore {
return s.CommandWebhookStore
}
func (s *TimerLayer) Compliance() store.ComplianceStore {
return s.ComplianceStore
}
func (s *TimerLayer) Draft() store.DraftStore {
return s.DraftStore
}
func (s *TimerLayer) Emoji() store.EmojiStore {
return s.EmojiStore
}
func (s *TimerLayer) FileInfo() store.FileInfoStore {
return s.FileInfoStore
}
func (s *TimerLayer) Group() store.GroupStore {
return s.GroupStore
}
func (s *TimerLayer) Job() store.JobStore {
return s.JobStore
}
func (s *TimerLayer) License() store.LicenseStore {
return s.LicenseStore
}
func (s *TimerLayer) LinkMetadata() store.LinkMetadataStore {
return s.LinkMetadataStore
}
func (s *TimerLayer) NotifyAdmin() store.NotifyAdminStore {
return s.NotifyAdminStore
}
func (s *TimerLayer) OAuth() store.OAuthStore {
return s.OAuthStore
}
func (s *TimerLayer) Plugin() store.PluginStore {
return s.PluginStore
}
func (s *TimerLayer) Post() store.PostStore {
return s.PostStore
}
func (s *TimerLayer) PostAcknowledgement() store.PostAcknowledgementStore {
return s.PostAcknowledgementStore
}
func (s *TimerLayer) PostPriority() store.PostPriorityStore {
return s.PostPriorityStore
}
func (s *TimerLayer) Preference() store.PreferenceStore {
return s.PreferenceStore
}
func (s *TimerLayer) ProductNotices() store.ProductNoticesStore {
return s.ProductNoticesStore
}
func (s *TimerLayer) Reaction() store.ReactionStore {
return s.ReactionStore
}
func (s *TimerLayer) RemoteCluster() store.RemoteClusterStore {
return s.RemoteClusterStore
}
func (s *TimerLayer) RetentionPolicy() store.RetentionPolicyStore {
return s.RetentionPolicyStore
}
func (s *TimerLayer) Role() store.RoleStore {
return s.RoleStore
}
func (s *TimerLayer) Scheme() store.SchemeStore {
return s.SchemeStore
}
func (s *TimerLayer) Session() store.SessionStore {
return s.SessionStore
}
func (s *TimerLayer) SharedChannel() store.SharedChannelStore {
return s.SharedChannelStore
}
func (s *TimerLayer) Status() store.StatusStore {
return s.StatusStore
}
func (s *TimerLayer) System() store.SystemStore {
return s.SystemStore
}
func (s *TimerLayer) Team() store.TeamStore {
return s.TeamStore
}
func (s *TimerLayer) TermsOfService() store.TermsOfServiceStore {
return s.TermsOfServiceStore
}
func (s *TimerLayer) Thread() store.ThreadStore {
return s.ThreadStore
}
func (s *TimerLayer) Token() store.TokenStore {
return s.TokenStore
}
func (s *TimerLayer) TrueUpReview() store.TrueUpReviewStore {
return s.TrueUpReviewStore
}
func (s *TimerLayer) UploadSession() store.UploadSessionStore {
return s.UploadSessionStore
}
func (s *TimerLayer) User() store.UserStore {
return s.UserStore
}
func (s *TimerLayer) UserAccessToken() store.UserAccessTokenStore {
return s.UserAccessTokenStore
}
func (s *TimerLayer) UserTermsOfService() store.UserTermsOfServiceStore {
return s.UserTermsOfServiceStore
}
func (s *TimerLayer) Webhook() store.WebhookStore {
return s.WebhookStore
}
type TimerLayerAuditStore struct {
store.AuditStore
Root *TimerLayer
}
type TimerLayerBotStore struct {
store.BotStore
Root *TimerLayer
}
type TimerLayerChannelStore struct {
store.ChannelStore
Root *TimerLayer
}
type TimerLayerChannelMemberHistoryStore struct {
store.ChannelMemberHistoryStore
Root *TimerLayer
}
type TimerLayerClusterDiscoveryStore struct {
store.ClusterDiscoveryStore
Root *TimerLayer
}
type TimerLayerCommandStore struct {
store.CommandStore
Root *TimerLayer
}
type TimerLayerCommandWebhookStore struct {
store.CommandWebhookStore
Root *TimerLayer
}
type TimerLayerComplianceStore struct {
store.ComplianceStore
Root *TimerLayer
}
type TimerLayerDraftStore struct {
store.DraftStore
Root *TimerLayer
}
type TimerLayerEmojiStore struct {
store.EmojiStore
Root *TimerLayer
}
type TimerLayerFileInfoStore struct {
store.FileInfoStore
Root *TimerLayer
}
type TimerLayerGroupStore struct {
store.GroupStore
Root *TimerLayer
}
type TimerLayerJobStore struct {
store.JobStore
Root *TimerLayer
}
type TimerLayerLicenseStore struct {
store.LicenseStore
Root *TimerLayer
}
type TimerLayerLinkMetadataStore struct {
store.LinkMetadataStore
Root *TimerLayer
}
type TimerLayerNotifyAdminStore struct {
store.NotifyAdminStore
Root *TimerLayer
}
type TimerLayerOAuthStore struct {
store.OAuthStore
Root *TimerLayer
}
type TimerLayerPluginStore struct {
store.PluginStore
Root *TimerLayer
}
type TimerLayerPostStore struct {
store.PostStore
Root *TimerLayer
}
type TimerLayerPostAcknowledgementStore struct {
store.PostAcknowledgementStore
Root *TimerLayer
}
type TimerLayerPostPriorityStore struct {
store.PostPriorityStore
Root *TimerLayer
}
type TimerLayerPreferenceStore struct {
store.PreferenceStore
Root *TimerLayer
}
type TimerLayerProductNoticesStore struct {
store.ProductNoticesStore
Root *TimerLayer
}
type TimerLayerReactionStore struct {
store.ReactionStore
Root *TimerLayer
}
type TimerLayerRemoteClusterStore struct {
store.RemoteClusterStore
Root *TimerLayer
}
type TimerLayerRetentionPolicyStore struct {
store.RetentionPolicyStore
Root *TimerLayer
}
type TimerLayerRoleStore struct {
store.RoleStore
Root *TimerLayer
}
type TimerLayerSchemeStore struct {
store.SchemeStore
Root *TimerLayer
}
type TimerLayerSessionStore struct {
store.SessionStore
Root *TimerLayer
}
type TimerLayerSharedChannelStore struct {
store.SharedChannelStore
Root *TimerLayer
}
type TimerLayerStatusStore struct {
store.StatusStore
Root *TimerLayer
}
type TimerLayerSystemStore struct {
store.SystemStore
Root *TimerLayer
}
type TimerLayerTeamStore struct {
store.TeamStore
Root *TimerLayer
}
type TimerLayerTermsOfServiceStore struct {
store.TermsOfServiceStore
Root *TimerLayer
}
type TimerLayerThreadStore struct {
store.ThreadStore
Root *TimerLayer
}
type TimerLayerTokenStore struct {
store.TokenStore
Root *TimerLayer
}
type TimerLayerTrueUpReviewStore struct {
store.TrueUpReviewStore
Root *TimerLayer
}
type TimerLayerUploadSessionStore struct {
store.UploadSessionStore
Root *TimerLayer
}
type TimerLayerUserStore struct {
store.UserStore
Root *TimerLayer
}
type TimerLayerUserAccessTokenStore struct {
store.UserAccessTokenStore
Root *TimerLayer
}
type TimerLayerUserTermsOfServiceStore struct {
store.UserTermsOfServiceStore
Root *TimerLayer
}
type TimerLayerWebhookStore struct {
store.WebhookStore
Root *TimerLayer
}
func (s *TimerLayerAuditStore) Get(user_id string, offset int, limit int) (model.Audits, error) {
start := time.Now()
result, err := s.AuditStore.Get(user_id, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("AuditStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerAuditStore) PermanentDeleteByUser(userID string) error {
start := time.Now()
err := s.AuditStore.PermanentDeleteByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("AuditStore.PermanentDeleteByUser", success, elapsed)
}
return err
}
func (s *TimerLayerAuditStore) Save(audit *model.Audit) error {
start := time.Now()
err := s.AuditStore.Save(audit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("AuditStore.Save", success, elapsed)
}
return err
}
func (s *TimerLayerBotStore) Get(userID string, includeDeleted bool) (*model.Bot, error) {
start := time.Now()
result, err := s.BotStore.Get(userID, includeDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("BotStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerBotStore) GetAll(options *model.BotGetOptions) ([]*model.Bot, error) {
start := time.Now()
result, err := s.BotStore.GetAll(options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("BotStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerBotStore) PermanentDelete(userID string) error {
start := time.Now()
err := s.BotStore.PermanentDelete(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("BotStore.PermanentDelete", success, elapsed)
}
return err
}
func (s *TimerLayerBotStore) Save(bot *model.Bot) (*model.Bot, error) {
start := time.Now()
result, err := s.BotStore.Save(bot)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("BotStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerBotStore) Update(bot *model.Bot) (*model.Bot, error) {
start := time.Now()
result, err := s.BotStore.Update(bot)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("BotStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) AnalyticsDeletedTypeCount(teamID string, channelType model.ChannelType) (int64, error) {
start := time.Now()
result, err := s.ChannelStore.AnalyticsDeletedTypeCount(teamID, channelType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.AnalyticsDeletedTypeCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) AnalyticsTypeCount(teamID string, channelType model.ChannelType) (int64, error) {
start := time.Now()
result, err := s.ChannelStore.AnalyticsTypeCount(teamID, channelType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.AnalyticsTypeCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) Autocomplete(userID string, term string, includeDeleted bool, isGuest bool) (model.ChannelListWithTeamData, error) {
start := time.Now()
result, err := s.ChannelStore.Autocomplete(userID, term, includeDeleted, isGuest)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.Autocomplete", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) AutocompleteInTeam(teamID string, userID string, term string, includeDeleted bool, isGuest bool) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.AutocompleteInTeam(teamID, userID, term, includeDeleted, isGuest)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.AutocompleteInTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) AutocompleteInTeamForSearch(teamID string, userID string, term string, includeDeleted bool) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.AutocompleteInTeamForSearch(teamID, userID, term, includeDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.AutocompleteInTeamForSearch", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) ClearAllCustomRoleAssignments() error {
start := time.Now()
err := s.ChannelStore.ClearAllCustomRoleAssignments()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.ClearAllCustomRoleAssignments", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) ClearCaches() {
start := time.Now()
s.ChannelStore.ClearCaches()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.ClearCaches", success, elapsed)
}
}
func (s *TimerLayerChannelStore) ClearMembersForUserCache() {
start := time.Now()
s.ChannelStore.ClearMembersForUserCache()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.ClearMembersForUserCache", success, elapsed)
}
}
func (s *TimerLayerChannelStore) ClearSidebarOnTeamLeave(userID string, teamID string) error {
start := time.Now()
err := s.ChannelStore.ClearSidebarOnTeamLeave(userID, teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.ClearSidebarOnTeamLeave", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) CountPostsAfter(channelID string, timestamp int64, userID string) (int, int, error) {
start := time.Now()
result, resultVar1, err := s.ChannelStore.CountPostsAfter(channelID, timestamp, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.CountPostsAfter", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerChannelStore) CountUrgentPostsAfter(channelID string, timestamp int64, userID string) (int, error) {
start := time.Now()
result, err := s.ChannelStore.CountUrgentPostsAfter(channelID, timestamp, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.CountUrgentPostsAfter", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) CreateDirectChannel(userID *model.User, otherUserID *model.User, channelOptions ...model.ChannelOption) (*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.CreateDirectChannel(userID, otherUserID, channelOptions...)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.CreateDirectChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) CreateInitialSidebarCategories(userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
start := time.Now()
result, err := s.ChannelStore.CreateInitialSidebarCategories(userID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.CreateInitialSidebarCategories", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) CreateSidebarCategory(userID string, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) {
start := time.Now()
result, err := s.ChannelStore.CreateSidebarCategory(userID, teamID, newCategory)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.CreateSidebarCategory", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) Delete(channelID string, timestamp int64) error {
start := time.Now()
err := s.ChannelStore.Delete(channelID, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) DeleteSidebarCategory(categoryID string) error {
start := time.Now()
err := s.ChannelStore.DeleteSidebarCategory(categoryID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.DeleteSidebarCategory", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) DeleteSidebarChannelsByPreferences(preferences model.Preferences) error {
start := time.Now()
err := s.ChannelStore.DeleteSidebarChannelsByPreferences(preferences)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.DeleteSidebarChannelsByPreferences", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) Get(id string, allowFromCache bool) (*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.Get(id, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetAll(teamID string) ([]*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.GetAll(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetAllChannelMembersById(id string) ([]string, error) {
start := time.Now()
result, err := s.ChannelStore.GetAllChannelMembersById(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetAllChannelMembersById", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetAllChannelMembersForUser(userID string, allowFromCache bool, includeDeleted bool) (map[string]string, error) {
start := time.Now()
result, err := s.ChannelStore.GetAllChannelMembersForUser(userID, allowFromCache, includeDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetAllChannelMembersForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetAllChannelMembersNotifyPropsForChannel(channelID string, allowFromCache bool) (map[string]model.StringMap, error) {
start := time.Now()
result, err := s.ChannelStore.GetAllChannelMembersNotifyPropsForChannel(channelID, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetAllChannelMembersNotifyPropsForChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetAllChannels(page int, perPage int, opts store.ChannelSearchOpts) (model.ChannelListWithTeamData, error) {
start := time.Now()
result, err := s.ChannelStore.GetAllChannels(page, perPage, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetAllChannels", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetAllChannelsCount(opts store.ChannelSearchOpts) (int64, error) {
start := time.Now()
result, err := s.ChannelStore.GetAllChannelsCount(opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetAllChannelsCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetAllChannelsForExportAfter(limit int, afterID string) ([]*model.ChannelForExport, error) {
start := time.Now()
result, err := s.ChannelStore.GetAllChannelsForExportAfter(limit, afterID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetAllChannelsForExportAfter", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetAllDirectChannelsForExportAfter(limit int, afterID string) ([]*model.DirectChannelForExport, error) {
start := time.Now()
result, err := s.ChannelStore.GetAllDirectChannelsForExportAfter(limit, afterID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetAllDirectChannelsForExportAfter", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetByName(team_id string, name string, allowFromCache bool) (*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.GetByName(team_id, name, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetByName", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetByNameIncludeDeleted(team_id string, name string, allowFromCache bool) (*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.GetByNameIncludeDeleted(team_id, name, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetByNameIncludeDeleted", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetByNames(team_id string, names []string, allowFromCache bool) ([]*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.GetByNames(team_id, names, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetByNames", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannelCounts(teamID string, userID string) (*model.ChannelCounts, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannelCounts(teamID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelCounts", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannelMembersForExport(userID string, teamID string) ([]*model.ChannelMemberForExport, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannelMembersForExport(userID, teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelMembersForExport", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannelMembersTimezones(channelID string) ([]model.StringMap, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannelMembersTimezones(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelMembersTimezones", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannelUnread(channelID string, userID string) (*model.ChannelUnread, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannelUnread(channelID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelUnread", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannels(teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannels(teamID, userID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannels", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannelsBatchForIndexing(startTime int64, startChannelID string, limit int) ([]*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannelsBatchForIndexing(startTime, startChannelID, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelsBatchForIndexing", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannelsByIds(channelIds []string, includeDeleted bool) ([]*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannelsByIds(channelIds, includeDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelsByIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannelsByScheme(schemeID string, offset int, limit int) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannelsByScheme(schemeID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelsByScheme", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannelsByUser(userID string, includeDeleted bool, lastDeleteAt int, pageSize int, fromChannelID string) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannelsByUser(userID, includeDeleted, lastDeleteAt, pageSize, fromChannelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelsByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannelsWithCursor(teamId, userId, opts, afterChannelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelsWithCursor", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetChannelsWithTeamDataByIds(channelIds []string, includeDeleted bool) ([]*model.ChannelWithTeamData, error) {
start := time.Now()
result, err := s.ChannelStore.GetChannelsWithTeamDataByIds(channelIds, includeDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelsWithTeamDataByIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetDeleted(team_id, offset, limit, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetDeleted", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetDeletedByName(team_id string, name string) (*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.GetDeletedByName(team_id, name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetDeletedByName", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetFileCount(channelID string) (int64, error) {
start := time.Now()
result, err := s.ChannelStore.GetFileCount(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetFileCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetForPost(postID string) (*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.GetForPost(postID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetForPost", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetGuestCount(channelID string, allowFromCache bool) (int64, error) {
start := time.Now()
result, err := s.ChannelStore.GetGuestCount(channelID, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetGuestCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMany(ids []string, allowFromCache bool) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetMany(ids, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMany", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMember(ctx context.Context, channelID string, userID string) (*model.ChannelMember, error) {
start := time.Now()
result, err := s.ChannelStore.GetMember(ctx, channelID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMember", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMemberCount(channelID string, allowFromCache bool) (int64, error) {
start := time.Now()
result, err := s.ChannelStore.GetMemberCount(channelID, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMemberCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMemberCountFromCache(channelID string) int64 {
start := time.Now()
result := s.ChannelStore.GetMemberCountFromCache(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMemberCountFromCache", success, elapsed)
}
return result
}
func (s *TimerLayerChannelStore) GetMemberCountsByGroup(ctx context.Context, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, error) {
start := time.Now()
result, err := s.ChannelStore.GetMemberCountsByGroup(ctx, channelID, includeTimezones)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMemberCountsByGroup", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMemberForPost(postID string, userID string) (*model.ChannelMember, error) {
start := time.Now()
result, err := s.ChannelStore.GetMemberForPost(postID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMemberForPost", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMembers(channelID string, offset int, limit int) (model.ChannelMembers, error) {
start := time.Now()
result, err := s.ChannelStore.GetMembers(channelID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMembersByChannelIds(channelIds []string, userID string) (model.ChannelMembers, error) {
start := time.Now()
result, err := s.ChannelStore.GetMembersByChannelIds(channelIds, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMembersByChannelIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMembersByIds(channelID string, userIds []string) (model.ChannelMembers, error) {
start := time.Now()
result, err := s.ChannelStore.GetMembersByIds(channelID, userIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMembersByIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMembersForUser(teamID string, userID string) (model.ChannelMembers, error) {
start := time.Now()
result, err := s.ChannelStore.GetMembersForUser(teamID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMembersForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMembersForUserWithCursor(userID string, teamID string, opts *store.ChannelMemberGraphQLSearchOpts) (model.ChannelMembers, error) {
start := time.Now()
result, err := s.ChannelStore.GetMembersForUserWithCursor(userID, teamID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMembersForUserWithCursor", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMembersForUserWithPagination(userID string, page int, perPage int) (model.ChannelMembersWithTeamData, error) {
start := time.Now()
result, err := s.ChannelStore.GetMembersForUserWithPagination(userID, page, perPage)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMembersForUserWithPagination", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error) {
start := time.Now()
result, err := s.ChannelStore.GetMembersInfoByChannelIds(channelIDs)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMembersInfoByChannelIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetMoreChannels(teamID string, userID string, offset int, limit int) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetMoreChannels(teamID, userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMoreChannels", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetPinnedPostCount(channelID string, allowFromCache bool) (int64, error) {
start := time.Now()
result, err := s.ChannelStore.GetPinnedPostCount(channelID, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetPinnedPostCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetPinnedPosts(channelID string) (*model.PostList, error) {
start := time.Now()
result, err := s.ChannelStore.GetPinnedPosts(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetPinnedPosts", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetPrivateChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetPrivateChannelsForTeam(teamID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetPrivateChannelsForTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetPublicChannelsByIdsForTeam(teamID string, channelIds []string) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetPublicChannelsByIdsForTeam(teamID, channelIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetPublicChannelsByIdsForTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetPublicChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetPublicChannelsForTeam(teamID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetPublicChannelsForTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetSidebarCategories(userID string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
start := time.Now()
result, err := s.ChannelStore.GetSidebarCategories(userID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetSidebarCategories", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetSidebarCategoriesForTeamForUser(userID string, teamID string) (*model.OrderedSidebarCategories, error) {
start := time.Now()
result, err := s.ChannelStore.GetSidebarCategoriesForTeamForUser(userID, teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetSidebarCategoriesForTeamForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetSidebarCategory(categoryID string) (*model.SidebarCategoryWithChannels, error) {
start := time.Now()
result, err := s.ChannelStore.GetSidebarCategory(categoryID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetSidebarCategory", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetSidebarCategoryOrder(userID string, teamID string) ([]string, error) {
start := time.Now()
result, err := s.ChannelStore.GetSidebarCategoryOrder(userID, teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetSidebarCategoryOrder", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetTeamChannels(teamID string) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetTeamChannels(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetTeamChannels", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetTeamForChannel(channelID string) (*model.Team, error) {
start := time.Now()
result, err := s.ChannelStore.GetTeamForChannel(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetTeamForChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetTeamMembersForChannel(channelID string) ([]string, error) {
start := time.Now()
result, err := s.ChannelStore.GetTeamMembersForChannel(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetTeamMembersForChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetTopChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetTopChannelsForTeamSince(teamID, userID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetTopChannelsForTeamSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetTopChannelsForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetTopChannelsForUserSince(userID, teamID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetTopChannelsForUserSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetTopInactiveChannelsForTeamSince(teamID, userID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetTopInactiveChannelsForTeamSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetTopInactiveChannelsForUserSince(teamID, userID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetTopInactiveChannelsForUserSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GroupSyncedChannelCount() (int64, error) {
start := time.Now()
result, err := s.ChannelStore.GroupSyncedChannelCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GroupSyncedChannelCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) IncrementMentionCount(channelID string, userIDs []string, isRoot bool, isUrgent bool) error {
start := time.Now()
err := s.ChannelStore.IncrementMentionCount(channelID, userIDs, isRoot, isUrgent)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.IncrementMentionCount", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) InvalidateAllChannelMembersForUser(userID string) {
start := time.Now()
s.ChannelStore.InvalidateAllChannelMembersForUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.InvalidateAllChannelMembersForUser", success, elapsed)
}
}
func (s *TimerLayerChannelStore) InvalidateCacheForChannelMembersNotifyProps(channelID string) {
start := time.Now()
s.ChannelStore.InvalidateCacheForChannelMembersNotifyProps(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.InvalidateCacheForChannelMembersNotifyProps", success, elapsed)
}
}
func (s *TimerLayerChannelStore) InvalidateChannel(id string) {
start := time.Now()
s.ChannelStore.InvalidateChannel(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.InvalidateChannel", success, elapsed)
}
}
func (s *TimerLayerChannelStore) InvalidateChannelByName(teamID string, name string) {
start := time.Now()
s.ChannelStore.InvalidateChannelByName(teamID, name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.InvalidateChannelByName", success, elapsed)
}
}
func (s *TimerLayerChannelStore) InvalidateGuestCount(channelID string) {
start := time.Now()
s.ChannelStore.InvalidateGuestCount(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.InvalidateGuestCount", success, elapsed)
}
}
func (s *TimerLayerChannelStore) InvalidateMemberCount(channelID string) {
start := time.Now()
s.ChannelStore.InvalidateMemberCount(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.InvalidateMemberCount", success, elapsed)
}
}
func (s *TimerLayerChannelStore) InvalidatePinnedPostCount(channelID string) {
start := time.Now()
s.ChannelStore.InvalidatePinnedPostCount(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.InvalidatePinnedPostCount", success, elapsed)
}
}
func (s *TimerLayerChannelStore) IsUserInChannelUseCache(userID string, channelID string) bool {
start := time.Now()
result := s.ChannelStore.IsUserInChannelUseCache(userID, channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.IsUserInChannelUseCache", success, elapsed)
}
return result
}
func (s *TimerLayerChannelStore) MigrateChannelMembers(fromChannelID string, fromUserID string) (map[string]string, error) {
start := time.Now()
result, err := s.ChannelStore.MigrateChannelMembers(fromChannelID, fromUserID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.MigrateChannelMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) PermanentDelete(channelID string) error {
start := time.Now()
err := s.ChannelStore.PermanentDelete(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.PermanentDelete", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) PermanentDeleteByTeam(teamID string) error {
start := time.Now()
err := s.ChannelStore.PermanentDeleteByTeam(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.PermanentDeleteByTeam", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) PermanentDeleteMembersByChannel(channelID string) error {
start := time.Now()
err := s.ChannelStore.PermanentDeleteMembersByChannel(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.PermanentDeleteMembersByChannel", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) PermanentDeleteMembersByUser(userID string) error {
start := time.Now()
err := s.ChannelStore.PermanentDeleteMembersByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.PermanentDeleteMembersByUser", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) PostCountsByDuration(channelIDs []string, sinceUnixMillis int64, userID *string, duration model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, error) {
start := time.Now()
result, err := s.ChannelStore.PostCountsByDuration(channelIDs, sinceUnixMillis, userID, duration, groupingLocation)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.PostCountsByDuration", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) RemoveAllDeactivatedMembers(channelID string) error {
start := time.Now()
err := s.ChannelStore.RemoveAllDeactivatedMembers(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.RemoveAllDeactivatedMembers", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) RemoveMember(channelID string, userID string) error {
start := time.Now()
err := s.ChannelStore.RemoveMember(channelID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.RemoveMember", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) RemoveMembers(channelID string, userIds []string) error {
start := time.Now()
err := s.ChannelStore.RemoveMembers(channelID, userIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.RemoveMembers", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) ResetAllChannelSchemes() error {
start := time.Now()
err := s.ChannelStore.ResetAllChannelSchemes()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.ResetAllChannelSchemes", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) Restore(channelID string, timestamp int64) error {
start := time.Now()
err := s.ChannelStore.Restore(channelID, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.Restore", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) Save(channel *model.Channel, maxChannelsPerTeam int64) (*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.Save(channel, maxChannelsPerTeam)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) SaveDirectChannel(channel *model.Channel, member1 *model.ChannelMember, member2 *model.ChannelMember) (*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.SaveDirectChannel(channel, member1, member2)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SaveDirectChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) SaveMember(member *model.ChannelMember) (*model.ChannelMember, error) {
start := time.Now()
result, err := s.ChannelStore.SaveMember(member)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SaveMember", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) SaveMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
start := time.Now()
result, err := s.ChannelStore.SaveMultipleMembers(members)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SaveMultipleMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) SearchAllChannels(term string, opts store.ChannelSearchOpts) (model.ChannelListWithTeamData, int64, error) {
start := time.Now()
result, resultVar1, err := s.ChannelStore.SearchAllChannels(term, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SearchAllChannels", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerChannelStore) SearchArchivedInTeam(teamID string, term string, userID string) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.SearchArchivedInTeam(teamID, term, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SearchArchivedInTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) SearchForUserInTeam(userID string, teamID string, term string, includeDeleted bool) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.SearchForUserInTeam(userID, teamID, term, includeDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SearchForUserInTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) SearchGroupChannels(userID string, term string) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.SearchGroupChannels(userID, term)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SearchGroupChannels", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) SearchInTeam(teamID string, term string, includeDeleted bool) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.SearchInTeam(teamID, term, includeDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SearchInTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) SearchMore(userID string, teamID string, term string) (model.ChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.SearchMore(userID, teamID, term)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SearchMore", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) SetDeleteAt(channelID string, deleteAt int64, updateAt int64) error {
start := time.Now()
err := s.ChannelStore.SetDeleteAt(channelID, deleteAt, updateAt)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SetDeleteAt", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) SetShared(channelId string, shared bool) error {
start := time.Now()
err := s.ChannelStore.SetShared(channelId, shared)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.SetShared", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) Update(channel *model.Channel) (*model.Channel, error) {
start := time.Now()
result, err := s.ChannelStore.Update(channel)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) UpdateLastViewedAt(channelIds []string, userID string) (map[string]int64, error) {
start := time.Now()
result, err := s.ChannelStore.UpdateLastViewedAt(channelIds, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UpdateLastViewedAt", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int, mentionCountRoot int, urgentMentionCount int, setUnreadCountRoot bool) (*model.ChannelUnreadAt, error) {
start := time.Now()
result, err := s.ChannelStore.UpdateLastViewedAtPost(unreadPost, userID, mentionCount, mentionCountRoot, urgentMentionCount, setUnreadCountRoot)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UpdateLastViewedAtPost", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) UpdateMember(member *model.ChannelMember) (*model.ChannelMember, error) {
start := time.Now()
result, err := s.ChannelStore.UpdateMember(member)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UpdateMember", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) UpdateMemberNotifyProps(channelID string, userID string, props map[string]string) (*model.ChannelMember, error) {
start := time.Now()
result, err := s.ChannelStore.UpdateMemberNotifyProps(channelID, userID, props)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UpdateMemberNotifyProps", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) UpdateMembersRole(channelID string, userIDs []string) error {
start := time.Now()
err := s.ChannelStore.UpdateMembersRole(channelID, userIDs)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UpdateMembersRole", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) UpdateMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
start := time.Now()
result, err := s.ChannelStore.UpdateMultipleMembers(members)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UpdateMultipleMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) UpdateSidebarCategories(userID string, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) {
start := time.Now()
result, resultVar1, err := s.ChannelStore.UpdateSidebarCategories(userID, teamID, categories)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UpdateSidebarCategories", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerChannelStore) UpdateSidebarCategoryOrder(userID string, teamID string, categoryOrder []string) error {
start := time.Now()
err := s.ChannelStore.UpdateSidebarCategoryOrder(userID, teamID, categoryOrder)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UpdateSidebarCategoryOrder", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) UpdateSidebarChannelCategoryOnMove(channel *model.Channel, newTeamID string) error {
start := time.Now()
err := s.ChannelStore.UpdateSidebarChannelCategoryOnMove(channel, newTeamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UpdateSidebarChannelCategoryOnMove", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) UpdateSidebarChannelsByPreferences(preferences model.Preferences) error {
start := time.Now()
err := s.ChannelStore.UpdateSidebarChannelsByPreferences(preferences)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UpdateSidebarChannelsByPreferences", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) UserBelongsToChannels(userID string, channelIds []string) (bool, error) {
start := time.Now()
result, err := s.ChannelStore.UserBelongsToChannels(userID, channelIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UserBelongsToChannels", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelMemberHistoryStore) DeleteOrphanedRows(limit int) (int64, error) {
start := time.Now()
result, err := s.ChannelMemberHistoryStore.DeleteOrphanedRows(limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelMemberHistoryStore.DeleteOrphanedRows", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelMemberHistoryStore) GetChannelsLeftSince(userID string, since int64) ([]string, error) {
start := time.Now()
result, err := s.ChannelMemberHistoryStore.GetChannelsLeftSince(userID, since)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelMemberHistoryStore.GetChannelsLeftSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelID string) ([]*model.ChannelMemberHistoryResult, error) {
start := time.Now()
result, err := s.ChannelMemberHistoryStore.GetUsersInChannelDuring(startTime, endTime, channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelMemberHistoryStore.GetUsersInChannelDuring", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelMemberHistoryStore) LogJoinEvent(userID string, channelID string, joinTime int64) error {
start := time.Now()
err := s.ChannelMemberHistoryStore.LogJoinEvent(userID, channelID, joinTime)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelMemberHistoryStore.LogJoinEvent", success, elapsed)
}
return err
}
func (s *TimerLayerChannelMemberHistoryStore) LogLeaveEvent(userID string, channelID string, leaveTime int64) error {
start := time.Now()
err := s.ChannelMemberHistoryStore.LogLeaveEvent(userID, channelID, leaveTime)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelMemberHistoryStore.LogLeaveEvent", success, elapsed)
}
return err
}
func (s *TimerLayerChannelMemberHistoryStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
start := time.Now()
result, err := s.ChannelMemberHistoryStore.PermanentDeleteBatch(endTime, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelMemberHistoryStore.PermanentDeleteBatch", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
start := time.Now()
result, resultVar1, err := s.ChannelMemberHistoryStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelMemberHistoryStore.PermanentDeleteBatchForRetentionPolicies", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerClusterDiscoveryStore) Cleanup() error {
start := time.Now()
err := s.ClusterDiscoveryStore.Cleanup()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ClusterDiscoveryStore.Cleanup", success, elapsed)
}
return err
}
func (s *TimerLayerClusterDiscoveryStore) Delete(discovery *model.ClusterDiscovery) (bool, error) {
start := time.Now()
result, err := s.ClusterDiscoveryStore.Delete(discovery)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ClusterDiscoveryStore.Delete", success, elapsed)
}
return result, err
}
func (s *TimerLayerClusterDiscoveryStore) Exists(discovery *model.ClusterDiscovery) (bool, error) {
start := time.Now()
result, err := s.ClusterDiscoveryStore.Exists(discovery)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ClusterDiscoveryStore.Exists", success, elapsed)
}
return result, err
}
func (s *TimerLayerClusterDiscoveryStore) GetAll(discoveryType string, clusterName string) ([]*model.ClusterDiscovery, error) {
start := time.Now()
result, err := s.ClusterDiscoveryStore.GetAll(discoveryType, clusterName)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ClusterDiscoveryStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerClusterDiscoveryStore) Save(discovery *model.ClusterDiscovery) error {
start := time.Now()
err := s.ClusterDiscoveryStore.Save(discovery)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ClusterDiscoveryStore.Save", success, elapsed)
}
return err
}
func (s *TimerLayerClusterDiscoveryStore) SetLastPingAt(discovery *model.ClusterDiscovery) error {
start := time.Now()
err := s.ClusterDiscoveryStore.SetLastPingAt(discovery)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ClusterDiscoveryStore.SetLastPingAt", success, elapsed)
}
return err
}
func (s *TimerLayerCommandStore) AnalyticsCommandCount(teamID string) (int64, error) {
start := time.Now()
result, err := s.CommandStore.AnalyticsCommandCount(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandStore.AnalyticsCommandCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerCommandStore) Delete(commandID string, timestamp int64) error {
start := time.Now()
err := s.CommandStore.Delete(commandID, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerCommandStore) Get(id string) (*model.Command, error) {
start := time.Now()
result, err := s.CommandStore.Get(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerCommandStore) GetByTeam(teamID string) ([]*model.Command, error) {
start := time.Now()
result, err := s.CommandStore.GetByTeam(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandStore.GetByTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerCommandStore) GetByTrigger(teamID string, trigger string) (*model.Command, error) {
start := time.Now()
result, err := s.CommandStore.GetByTrigger(teamID, trigger)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandStore.GetByTrigger", success, elapsed)
}
return result, err
}
func (s *TimerLayerCommandStore) PermanentDeleteByTeam(teamID string) error {
start := time.Now()
err := s.CommandStore.PermanentDeleteByTeam(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandStore.PermanentDeleteByTeam", success, elapsed)
}
return err
}
func (s *TimerLayerCommandStore) PermanentDeleteByUser(userID string) error {
start := time.Now()
err := s.CommandStore.PermanentDeleteByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandStore.PermanentDeleteByUser", success, elapsed)
}
return err
}
func (s *TimerLayerCommandStore) Save(webhook *model.Command) (*model.Command, error) {
start := time.Now()
result, err := s.CommandStore.Save(webhook)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerCommandStore) Update(hook *model.Command) (*model.Command, error) {
start := time.Now()
result, err := s.CommandStore.Update(hook)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerCommandWebhookStore) Cleanup() {
start := time.Now()
s.CommandWebhookStore.Cleanup()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandWebhookStore.Cleanup", success, elapsed)
}
}
func (s *TimerLayerCommandWebhookStore) Get(id string) (*model.CommandWebhook, error) {
start := time.Now()
result, err := s.CommandWebhookStore.Get(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandWebhookStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerCommandWebhookStore) Save(webhook *model.CommandWebhook) (*model.CommandWebhook, error) {
start := time.Now()
result, err := s.CommandWebhookStore.Save(webhook)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandWebhookStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerCommandWebhookStore) TryUse(id string, limit int) error {
start := time.Now()
err := s.CommandWebhookStore.TryUse(id, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("CommandWebhookStore.TryUse", success, elapsed)
}
return err
}
func (s *TimerLayerComplianceStore) ComplianceExport(compliance *model.Compliance, cursor model.ComplianceExportCursor, limit int) ([]*model.CompliancePost, model.ComplianceExportCursor, error) {
start := time.Now()
result, resultVar1, err := s.ComplianceStore.ComplianceExport(compliance, cursor, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ComplianceStore.ComplianceExport", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerComplianceStore) Get(id string) (*model.Compliance, error) {
start := time.Now()
result, err := s.ComplianceStore.Get(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ComplianceStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerComplianceStore) GetAll(offset int, limit int) (model.Compliances, error) {
start := time.Now()
result, err := s.ComplianceStore.GetAll(offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ComplianceStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerComplianceStore) MessageExport(ctx context.Context, cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) {
start := time.Now()
result, resultVar1, err := s.ComplianceStore.MessageExport(ctx, cursor, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ComplianceStore.MessageExport", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerComplianceStore) Save(compliance *model.Compliance) (*model.Compliance, error) {
start := time.Now()
result, err := s.ComplianceStore.Save(compliance)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ComplianceStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerComplianceStore) Update(compliance *model.Compliance) (*model.Compliance, error) {
start := time.Now()
result, err := s.ComplianceStore.Update(compliance)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ComplianceStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerDraftStore) Delete(userID string, channelID string, rootID string) error {
start := time.Now()
err := s.DraftStore.Delete(userID, channelID, rootID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("DraftStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerDraftStore) Get(userID string, channelID string, rootID string, includeDeleted bool) (*model.Draft, error) {
start := time.Now()
result, err := s.DraftStore.Get(userID, channelID, rootID, includeDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("DraftStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerDraftStore) GetDraftsForUser(userID string, teamID string) ([]*model.Draft, error) {
start := time.Now()
result, err := s.DraftStore.GetDraftsForUser(userID, teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("DraftStore.GetDraftsForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerDraftStore) Save(d *model.Draft) (*model.Draft, error) {
start := time.Now()
result, err := s.DraftStore.Save(d)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("DraftStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerDraftStore) Update(d *model.Draft) (*model.Draft, error) {
start := time.Now()
result, err := s.DraftStore.Update(d)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("DraftStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerEmojiStore) Delete(emoji *model.Emoji, timestamp int64) error {
start := time.Now()
err := s.EmojiStore.Delete(emoji, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("EmojiStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerEmojiStore) Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error) {
start := time.Now()
result, err := s.EmojiStore.Get(ctx, id, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("EmojiStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerEmojiStore) GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error) {
start := time.Now()
result, err := s.EmojiStore.GetByName(ctx, name, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("EmojiStore.GetByName", success, elapsed)
}
return result, err
}
func (s *TimerLayerEmojiStore) GetList(offset int, limit int, sort string) ([]*model.Emoji, error) {
start := time.Now()
result, err := s.EmojiStore.GetList(offset, limit, sort)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("EmojiStore.GetList", success, elapsed)
}
return result, err
}
func (s *TimerLayerEmojiStore) GetMultipleByName(names []string) ([]*model.Emoji, error) {
start := time.Now()
result, err := s.EmojiStore.GetMultipleByName(names)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("EmojiStore.GetMultipleByName", success, elapsed)
}
return result, err
}
func (s *TimerLayerEmojiStore) Save(emoji *model.Emoji) (*model.Emoji, error) {
start := time.Now()
result, err := s.EmojiStore.Save(emoji)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("EmojiStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerEmojiStore) Search(name string, prefixOnly bool, limit int) ([]*model.Emoji, error) {
start := time.Now()
result, err := s.EmojiStore.Search(name, prefixOnly, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("EmojiStore.Search", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) AttachToPost(fileID string, postID string, channelID string, creatorID string) error {
start := time.Now()
err := s.FileInfoStore.AttachToPost(fileID, postID, channelID, creatorID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.AttachToPost", success, elapsed)
}
return err
}
func (s *TimerLayerFileInfoStore) ClearCaches() {
start := time.Now()
s.FileInfoStore.ClearCaches()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.ClearCaches", success, elapsed)
}
}
func (s *TimerLayerFileInfoStore) CountAll() (int64, error) {
start := time.Now()
result, err := s.FileInfoStore.CountAll()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.CountAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) DeleteForPost(postID string) (string, error) {
start := time.Now()
result, err := s.FileInfoStore.DeleteForPost(postID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.DeleteForPost", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) Get(id string) (*model.FileInfo, error) {
start := time.Now()
result, err := s.FileInfoStore.Get(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) GetByIds(ids []string) ([]*model.FileInfo, error) {
start := time.Now()
result, err := s.FileInfoStore.GetByIds(ids)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.GetByIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) GetByPath(path string) (*model.FileInfo, error) {
start := time.Now()
result, err := s.FileInfoStore.GetByPath(path)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.GetByPath", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) GetFilesBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.FileForIndexing, error) {
start := time.Now()
result, err := s.FileInfoStore.GetFilesBatchForIndexing(startTime, startFileID, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.GetFilesBatchForIndexing", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) GetForPost(postID string, readFromMaster bool, includeDeleted bool, allowFromCache bool) ([]*model.FileInfo, error) {
start := time.Now()
result, err := s.FileInfoStore.GetForPost(postID, readFromMaster, includeDeleted, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.GetForPost", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) GetForUser(userID string) ([]*model.FileInfo, error) {
start := time.Now()
result, err := s.FileInfoStore.GetForUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.GetForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) GetFromMaster(id string) (*model.FileInfo, error) {
start := time.Now()
result, err := s.FileInfoStore.GetFromMaster(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.GetFromMaster", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) GetStorageUsage(allowFromCache bool, includeDeleted bool) (int64, error) {
start := time.Now()
result, err := s.FileInfoStore.GetStorageUsage(allowFromCache, includeDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.GetStorageUsage", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) GetUptoNSizeFileTime(n int64) (int64, error) {
start := time.Now()
result, err := s.FileInfoStore.GetUptoNSizeFileTime(n)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.GetUptoNSizeFileTime", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) GetWithOptions(page int, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, error) {
start := time.Now()
result, err := s.FileInfoStore.GetWithOptions(page, perPage, opt)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.GetWithOptions", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) InvalidateFileInfosForPostCache(postID string, deleted bool) {
start := time.Now()
s.FileInfoStore.InvalidateFileInfosForPostCache(postID, deleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.InvalidateFileInfosForPostCache", success, elapsed)
}
}
func (s *TimerLayerFileInfoStore) PermanentDelete(fileID string) error {
start := time.Now()
err := s.FileInfoStore.PermanentDelete(fileID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.PermanentDelete", success, elapsed)
}
return err
}
func (s *TimerLayerFileInfoStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
start := time.Now()
result, err := s.FileInfoStore.PermanentDeleteBatch(endTime, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.PermanentDeleteBatch", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) PermanentDeleteByUser(userID string) (int64, error) {
start := time.Now()
result, err := s.FileInfoStore.PermanentDeleteByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.PermanentDeleteByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) Save(info *model.FileInfo) (*model.FileInfo, error) {
start := time.Now()
result, err := s.FileInfoStore.Save(info)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) Search(paramsList []*model.SearchParams, userID string, teamID string, page int, perPage int) (*model.FileInfoList, error) {
start := time.Now()
result, err := s.FileInfoStore.Search(paramsList, userID, teamID, page, perPage)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.Search", success, elapsed)
}
return result, err
}
func (s *TimerLayerFileInfoStore) SetContent(fileID string, content string) error {
start := time.Now()
err := s.FileInfoStore.SetContent(fileID, content)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.SetContent", success, elapsed)
}
return err
}
func (s *TimerLayerFileInfoStore) Upsert(info *model.FileInfo) (*model.FileInfo, error) {
start := time.Now()
result, err := s.FileInfoStore.Upsert(info)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("FileInfoStore.Upsert", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) AdminRoleGroupsForSyncableMember(userID string, syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
start := time.Now()
result, err := s.GroupStore.AdminRoleGroupsForSyncableMember(userID, syncableID, syncableType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.AdminRoleGroupsForSyncableMember", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, error) {
start := time.Now()
result, err := s.GroupStore.ChannelMembersMinusGroupMembers(channelID, groupIDs, page, perPage)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.ChannelMembersMinusGroupMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) ChannelMembersToAdd(since int64, channelID *string, includeRemovedMembers bool) ([]*model.UserChannelIDPair, error) {
start := time.Now()
result, err := s.GroupStore.ChannelMembersToAdd(since, channelID, includeRemovedMembers)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.ChannelMembersToAdd", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) ChannelMembersToRemove(channelID *string) ([]*model.ChannelMember, error) {
start := time.Now()
result, err := s.GroupStore.ChannelMembersToRemove(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.ChannelMembersToRemove", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) CountChannelMembersMinusGroupMembers(channelID string, groupIDs []string) (int64, error) {
start := time.Now()
result, err := s.GroupStore.CountChannelMembersMinusGroupMembers(channelID, groupIDs)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.CountChannelMembersMinusGroupMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) CountGroupsByChannel(channelID string, opts model.GroupSearchOpts) (int64, error) {
start := time.Now()
result, err := s.GroupStore.CountGroupsByChannel(channelID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.CountGroupsByChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) CountGroupsByTeam(teamID string, opts model.GroupSearchOpts) (int64, error) {
start := time.Now()
result, err := s.GroupStore.CountGroupsByTeam(teamID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.CountGroupsByTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) CountTeamMembersMinusGroupMembers(teamID string, groupIDs []string) (int64, error) {
start := time.Now()
result, err := s.GroupStore.CountTeamMembersMinusGroupMembers(teamID, groupIDs)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.CountTeamMembersMinusGroupMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) Create(group *model.Group) (*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.Create(group)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.Create", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) CreateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
start := time.Now()
result, err := s.GroupStore.CreateGroupSyncable(groupSyncable)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.CreateGroupSyncable", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) CreateWithUserIds(group *model.GroupWithUserIds) (*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.CreateWithUserIds(group)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.CreateWithUserIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) Delete(groupID string) (*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.Delete(groupID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.Delete", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
start := time.Now()
result, err := s.GroupStore.DeleteGroupSyncable(groupID, syncableID, syncableType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.DeleteGroupSyncable", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) DeleteMember(groupID string, userID string) (*model.GroupMember, error) {
start := time.Now()
result, err := s.GroupStore.DeleteMember(groupID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.DeleteMember", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) DeleteMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
start := time.Now()
result, err := s.GroupStore.DeleteMembers(groupID, userIDs)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.DeleteMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) DistinctGroupMemberCount() (int64, error) {
start := time.Now()
result, err := s.GroupStore.DistinctGroupMemberCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.DistinctGroupMemberCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) DistinctGroupMemberCountForSource(source model.GroupSource) (int64, error) {
start := time.Now()
result, err := s.GroupStore.DistinctGroupMemberCountForSource(source)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.DistinctGroupMemberCountForSource", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) Get(groupID string) (*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.Get(groupID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetAllBySource(groupSource model.GroupSource) ([]*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.GetAllBySource(groupSource)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetAllBySource", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetAllGroupSyncablesByGroupId(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, error) {
start := time.Now()
result, err := s.GroupStore.GetAllGroupSyncablesByGroupId(groupID, syncableType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetAllGroupSyncablesByGroupId", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetByIDs(groupIDs []string) ([]*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.GetByIDs(groupIDs)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetByIDs", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetByName(name string, opts model.GroupSearchOpts) (*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.GetByName(name, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetByName", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.GetByRemoteID(remoteID, groupSource)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetByRemoteID", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetByUser(userID string) ([]*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.GetByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, error) {
start := time.Now()
result, err := s.GroupStore.GetGroupSyncable(groupID, syncableID, syncableType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetGroupSyncable", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetGroups(page int, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.GetGroups(page, perPage, opts, viewRestrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetGroups", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetGroupsAssociatedToChannelsByTeam(teamID string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, error) {
start := time.Now()
result, err := s.GroupStore.GetGroupsAssociatedToChannelsByTeam(teamID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetGroupsAssociatedToChannelsByTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetGroupsByChannel(channelID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
start := time.Now()
result, err := s.GroupStore.GetGroupsByChannel(channelID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetGroupsByChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetGroupsByTeam(teamID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) {
start := time.Now()
result, err := s.GroupStore.GetGroupsByTeam(teamID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetGroupsByTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetMember(groupID string, userID string) (*model.GroupMember, error) {
start := time.Now()
result, err := s.GroupStore.GetMember(groupID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetMember", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetMemberCount(groupID string) (int64, error) {
start := time.Now()
result, err := s.GroupStore.GetMemberCount(groupID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetMemberCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetMemberCountWithRestrictions(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, error) {
start := time.Now()
result, err := s.GroupStore.GetMemberCountWithRestrictions(groupID, viewRestrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetMemberCountWithRestrictions", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetMemberUsers(groupID string) ([]*model.User, error) {
start := time.Now()
result, err := s.GroupStore.GetMemberUsers(groupID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetMemberUsers", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetMemberUsersInTeam(groupID string, teamID string) ([]*model.User, error) {
start := time.Now()
result, err := s.GroupStore.GetMemberUsersInTeam(groupID, teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetMemberUsersInTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetMemberUsersNotInChannel(groupID string, channelID string) ([]*model.User, error) {
start := time.Now()
result, err := s.GroupStore.GetMemberUsersNotInChannel(groupID, channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetMemberUsersNotInChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
start := time.Now()
result, err := s.GroupStore.GetMemberUsersPage(groupID, page, perPage, viewRestrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetMemberUsersPage", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, error) {
start := time.Now()
result, err := s.GroupStore.GetMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, teammateNameDisplay)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetMemberUsersSortedPage", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
start := time.Now()
result, err := s.GroupStore.GetNonMemberUsersPage(groupID, page, perPage, viewRestrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GetNonMemberUsersPage", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GroupChannelCount() (int64, error) {
start := time.Now()
result, err := s.GroupStore.GroupChannelCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GroupChannelCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GroupCount() (int64, error) {
start := time.Now()
result, err := s.GroupStore.GroupCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GroupCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GroupCountBySource(source model.GroupSource) (int64, error) {
start := time.Now()
result, err := s.GroupStore.GroupCountBySource(source)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GroupCountBySource", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GroupCountWithAllowReference() (int64, error) {
start := time.Now()
result, err := s.GroupStore.GroupCountWithAllowReference()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GroupCountWithAllowReference", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GroupMemberCount() (int64, error) {
start := time.Now()
result, err := s.GroupStore.GroupMemberCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GroupMemberCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) GroupTeamCount() (int64, error) {
start := time.Now()
result, err := s.GroupStore.GroupTeamCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.GroupTeamCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) PermanentDeleteMembersByUser(userID string) error {
start := time.Now()
err := s.GroupStore.PermanentDeleteMembersByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.PermanentDeleteMembersByUser", success, elapsed)
}
return err
}
func (s *TimerLayerGroupStore) PermittedSyncableAdmins(syncableID string, syncableType model.GroupSyncableType) ([]string, error) {
start := time.Now()
result, err := s.GroupStore.PermittedSyncableAdmins(syncableID, syncableType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.PermittedSyncableAdmins", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) Restore(groupID string) (*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.Restore(groupID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.Restore", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, error) {
start := time.Now()
result, err := s.GroupStore.TeamMembersMinusGroupMembers(teamID, groupIDs, page, perPage)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.TeamMembersMinusGroupMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) TeamMembersToAdd(since int64, teamID *string, includeRemovedMembers bool) ([]*model.UserTeamIDPair, error) {
start := time.Now()
result, err := s.GroupStore.TeamMembersToAdd(since, teamID, includeRemovedMembers)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.TeamMembersToAdd", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) TeamMembersToRemove(teamID *string) ([]*model.TeamMember, error) {
start := time.Now()
result, err := s.GroupStore.TeamMembersToRemove(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.TeamMembersToRemove", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) Update(group *model.Group) (*model.Group, error) {
start := time.Now()
result, err := s.GroupStore.Update(group)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, error) {
start := time.Now()
result, err := s.GroupStore.UpdateGroupSyncable(groupSyncable)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.UpdateGroupSyncable", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) UpsertMember(groupID string, userID string) (*model.GroupMember, error) {
start := time.Now()
result, err := s.GroupStore.UpsertMember(groupID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.UpsertMember", success, elapsed)
}
return result, err
}
func (s *TimerLayerGroupStore) UpsertMembers(groupID string, userIDs []string) ([]*model.GroupMember, error) {
start := time.Now()
result, err := s.GroupStore.UpsertMembers(groupID, userIDs)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("GroupStore.UpsertMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) Cleanup(expiryTime int64, batchSize int) error {
start := time.Now()
err := s.JobStore.Cleanup(expiryTime, batchSize)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.Cleanup", success, elapsed)
}
return err
}
func (s *TimerLayerJobStore) Delete(id string) (string, error) {
start := time.Now()
result, err := s.JobStore.Delete(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.Delete", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) Get(id string) (*model.Job, error) {
start := time.Now()
result, err := s.JobStore.Get(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) GetAllByStatus(status string) ([]*model.Job, error) {
start := time.Now()
result, err := s.JobStore.GetAllByStatus(status)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.GetAllByStatus", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) GetAllByType(jobType string) ([]*model.Job, error) {
start := time.Now()
result, err := s.JobStore.GetAllByType(jobType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.GetAllByType", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) GetAllByTypeAndStatus(jobType string, status string) ([]*model.Job, error) {
start := time.Now()
result, err := s.JobStore.GetAllByTypeAndStatus(jobType, status)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.GetAllByTypeAndStatus", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) GetAllByTypePage(jobType string, offset int, limit int) ([]*model.Job, error) {
start := time.Now()
result, err := s.JobStore.GetAllByTypePage(jobType, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.GetAllByTypePage", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) GetAllByTypesPage(jobTypes []string, offset int, limit int) ([]*model.Job, error) {
start := time.Now()
result, err := s.JobStore.GetAllByTypesPage(jobTypes, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.GetAllByTypesPage", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) GetAllPage(offset int, limit int) ([]*model.Job, error) {
start := time.Now()
result, err := s.JobStore.GetAllPage(offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.GetAllPage", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) GetCountByStatusAndType(status string, jobType string) (int64, error) {
start := time.Now()
result, err := s.JobStore.GetCountByStatusAndType(status, jobType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.GetCountByStatusAndType", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) GetNewestJobByStatusAndType(status string, jobType string) (*model.Job, error) {
start := time.Now()
result, err := s.JobStore.GetNewestJobByStatusAndType(status, jobType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.GetNewestJobByStatusAndType", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) GetNewestJobByStatusesAndType(statuses []string, jobType string) (*model.Job, error) {
start := time.Now()
result, err := s.JobStore.GetNewestJobByStatusesAndType(statuses, jobType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.GetNewestJobByStatusesAndType", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) Save(job *model.Job) (*model.Job, error) {
start := time.Now()
result, err := s.JobStore.Save(job)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) UpdateOptimistically(job *model.Job, currentStatus string) (bool, error) {
start := time.Now()
result, err := s.JobStore.UpdateOptimistically(job, currentStatus)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.UpdateOptimistically", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) UpdateStatus(id string, status string) (*model.Job, error) {
start := time.Now()
result, err := s.JobStore.UpdateStatus(id, status)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.UpdateStatus", success, elapsed)
}
return result, err
}
func (s *TimerLayerJobStore) UpdateStatusOptimistically(id string, currentStatus string, newStatus string) (bool, error) {
start := time.Now()
result, err := s.JobStore.UpdateStatusOptimistically(id, currentStatus, newStatus)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("JobStore.UpdateStatusOptimistically", success, elapsed)
}
return result, err
}
func (s *TimerLayerLicenseStore) Get(id string) (*model.LicenseRecord, error) {
start := time.Now()
result, err := s.LicenseStore.Get(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("LicenseStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerLicenseStore) GetAll() ([]*model.LicenseRecord, error) {
start := time.Now()
result, err := s.LicenseStore.GetAll()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("LicenseStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerLicenseStore) Save(license *model.LicenseRecord) (*model.LicenseRecord, error) {
start := time.Now()
result, err := s.LicenseStore.Save(license)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("LicenseStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerLinkMetadataStore) Get(url string, timestamp int64) (*model.LinkMetadata, error) {
start := time.Now()
result, err := s.LinkMetadataStore.Get(url, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("LinkMetadataStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerLinkMetadataStore) Save(linkMetadata *model.LinkMetadata) (*model.LinkMetadata, error) {
start := time.Now()
result, err := s.LinkMetadataStore.Save(linkMetadata)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("LinkMetadataStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerNotifyAdminStore) DeleteBefore(trial bool, now int64) error {
start := time.Now()
err := s.NotifyAdminStore.DeleteBefore(trial, now)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("NotifyAdminStore.DeleteBefore", success, elapsed)
}
return err
}
func (s *TimerLayerNotifyAdminStore) Get(trial bool) ([]*model.NotifyAdminData, error) {
start := time.Now()
result, err := s.NotifyAdminStore.Get(trial)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("NotifyAdminStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostFeature) ([]*model.NotifyAdminData, error) {
start := time.Now()
result, err := s.NotifyAdminStore.GetDataByUserIdAndFeature(userId, feature)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("NotifyAdminStore.GetDataByUserIdAndFeature", success, elapsed)
}
return result, err
}
func (s *TimerLayerNotifyAdminStore) Save(data *model.NotifyAdminData) (*model.NotifyAdminData, error) {
start := time.Now()
result, err := s.NotifyAdminStore.Save(data)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("NotifyAdminStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerNotifyAdminStore) Update(userId string, requiredPlan string, requiredFeature model.MattermostFeature, now int64) error {
start := time.Now()
err := s.NotifyAdminStore.Update(userId, requiredPlan, requiredFeature, now)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("NotifyAdminStore.Update", success, elapsed)
}
return err
}
func (s *TimerLayerOAuthStore) DeleteApp(id string) error {
start := time.Now()
err := s.OAuthStore.DeleteApp(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.DeleteApp", success, elapsed)
}
return err
}
func (s *TimerLayerOAuthStore) GetAccessData(token string) (*model.AccessData, error) {
start := time.Now()
result, err := s.OAuthStore.GetAccessData(token)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.GetAccessData", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) GetAccessDataByRefreshToken(token string) (*model.AccessData, error) {
start := time.Now()
result, err := s.OAuthStore.GetAccessDataByRefreshToken(token)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.GetAccessDataByRefreshToken", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) GetAccessDataByUserForApp(userID string, clientId string) ([]*model.AccessData, error) {
start := time.Now()
result, err := s.OAuthStore.GetAccessDataByUserForApp(userID, clientId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.GetAccessDataByUserForApp", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) GetApp(id string) (*model.OAuthApp, error) {
start := time.Now()
result, err := s.OAuthStore.GetApp(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.GetApp", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) GetAppByUser(userID string, offset int, limit int) ([]*model.OAuthApp, error) {
start := time.Now()
result, err := s.OAuthStore.GetAppByUser(userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.GetAppByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) GetApps(offset int, limit int) ([]*model.OAuthApp, error) {
start := time.Now()
result, err := s.OAuthStore.GetApps(offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.GetApps", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) GetAuthData(code string) (*model.AuthData, error) {
start := time.Now()
result, err := s.OAuthStore.GetAuthData(code)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.GetAuthData", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) GetAuthorizedApps(userID string, offset int, limit int) ([]*model.OAuthApp, error) {
start := time.Now()
result, err := s.OAuthStore.GetAuthorizedApps(userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.GetAuthorizedApps", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) GetPreviousAccessData(userID string, clientId string) (*model.AccessData, error) {
start := time.Now()
result, err := s.OAuthStore.GetPreviousAccessData(userID, clientId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.GetPreviousAccessData", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) PermanentDeleteAuthDataByUser(userID string) error {
start := time.Now()
err := s.OAuthStore.PermanentDeleteAuthDataByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.PermanentDeleteAuthDataByUser", success, elapsed)
}
return err
}
func (s *TimerLayerOAuthStore) RemoveAccessData(token string) error {
start := time.Now()
err := s.OAuthStore.RemoveAccessData(token)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.RemoveAccessData", success, elapsed)
}
return err
}
func (s *TimerLayerOAuthStore) RemoveAllAccessData() error {
start := time.Now()
err := s.OAuthStore.RemoveAllAccessData()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.RemoveAllAccessData", success, elapsed)
}
return err
}
func (s *TimerLayerOAuthStore) RemoveAuthData(code string) error {
start := time.Now()
err := s.OAuthStore.RemoveAuthData(code)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.RemoveAuthData", success, elapsed)
}
return err
}
func (s *TimerLayerOAuthStore) RemoveAuthDataByClientId(clientId string, userId string) error {
start := time.Now()
err := s.OAuthStore.RemoveAuthDataByClientId(clientId, userId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.RemoveAuthDataByClientId", success, elapsed)
}
return err
}
func (s *TimerLayerOAuthStore) SaveAccessData(accessData *model.AccessData) (*model.AccessData, error) {
start := time.Now()
result, err := s.OAuthStore.SaveAccessData(accessData)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.SaveAccessData", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) SaveApp(app *model.OAuthApp) (*model.OAuthApp, error) {
start := time.Now()
result, err := s.OAuthStore.SaveApp(app)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.SaveApp", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) SaveAuthData(authData *model.AuthData) (*model.AuthData, error) {
start := time.Now()
result, err := s.OAuthStore.SaveAuthData(authData)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.SaveAuthData", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) UpdateAccessData(accessData *model.AccessData) (*model.AccessData, error) {
start := time.Now()
result, err := s.OAuthStore.UpdateAccessData(accessData)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.UpdateAccessData", success, elapsed)
}
return result, err
}
func (s *TimerLayerOAuthStore) UpdateApp(app *model.OAuthApp) (*model.OAuthApp, error) {
start := time.Now()
result, err := s.OAuthStore.UpdateApp(app)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.UpdateApp", success, elapsed)
}
return result, err
}
func (s *TimerLayerPluginStore) CompareAndDelete(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error) {
start := time.Now()
result, err := s.PluginStore.CompareAndDelete(keyVal, oldValue)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PluginStore.CompareAndDelete", success, elapsed)
}
return result, err
}
func (s *TimerLayerPluginStore) CompareAndSet(keyVal *model.PluginKeyValue, oldValue []byte) (bool, error) {
start := time.Now()
result, err := s.PluginStore.CompareAndSet(keyVal, oldValue)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PluginStore.CompareAndSet", success, elapsed)
}
return result, err
}
func (s *TimerLayerPluginStore) Delete(pluginID string, key string) error {
start := time.Now()
err := s.PluginStore.Delete(pluginID, key)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PluginStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerPluginStore) DeleteAllExpired() error {
start := time.Now()
err := s.PluginStore.DeleteAllExpired()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PluginStore.DeleteAllExpired", success, elapsed)
}
return err
}
func (s *TimerLayerPluginStore) DeleteAllForPlugin(PluginID string) error {
start := time.Now()
err := s.PluginStore.DeleteAllForPlugin(PluginID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PluginStore.DeleteAllForPlugin", success, elapsed)
}
return err
}
func (s *TimerLayerPluginStore) Get(pluginID string, key string) (*model.PluginKeyValue, error) {
start := time.Now()
result, err := s.PluginStore.Get(pluginID, key)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PluginStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerPluginStore) List(pluginID string, page int, perPage int) ([]string, error) {
start := time.Now()
result, err := s.PluginStore.List(pluginID, page, perPage)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PluginStore.List", success, elapsed)
}
return result, err
}
func (s *TimerLayerPluginStore) SaveOrUpdate(keyVal *model.PluginKeyValue) (*model.PluginKeyValue, error) {
start := time.Now()
result, err := s.PluginStore.SaveOrUpdate(keyVal)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PluginStore.SaveOrUpdate", success, elapsed)
}
return result, err
}
func (s *TimerLayerPluginStore) SetWithOptions(pluginID string, key string, value []byte, options model.PluginKVSetOptions) (bool, error) {
start := time.Now()
result, err := s.PluginStore.SetWithOptions(pluginID, key, value, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PluginStore.SetWithOptions", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) AnalyticsPostCount(options *model.PostCountOptions) (int64, error) {
start := time.Now()
result, err := s.PostStore.AnalyticsPostCount(options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.AnalyticsPostCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) AnalyticsPostCountsByDay(options *model.AnalyticsPostCountsOptions) (model.AnalyticsRows, error) {
start := time.Now()
result, err := s.PostStore.AnalyticsPostCountsByDay(options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.AnalyticsPostCountsByDay", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) AnalyticsUserCountsWithPostsByDay(teamID string) (model.AnalyticsRows, error) {
start := time.Now()
result, err := s.PostStore.AnalyticsUserCountsWithPostsByDay(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.AnalyticsUserCountsWithPostsByDay", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) ClearCaches() {
start := time.Now()
s.PostStore.ClearCaches()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.ClearCaches", success, elapsed)
}
}
func (s *TimerLayerPostStore) Delete(postID string, timestamp int64, deleteByID string) error {
start := time.Now()
err := s.PostStore.Delete(postID, timestamp, deleteByID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerPostStore) DeleteOrphanedRows(limit int) (int64, error) {
start := time.Now()
result, err := s.PostStore.DeleteOrphanedRows(limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.DeleteOrphanedRows", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) {
start := time.Now()
result, err := s.PostStore.Get(ctx, id, opts, userID, sanitizeOptions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetDirectPostParentsForExportAfter(limit int, afterID string) ([]*model.DirectPostForExport, error) {
start := time.Now()
result, err := s.PostStore.GetDirectPostParentsForExportAfter(limit, afterID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetDirectPostParentsForExportAfter", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetEditHistoryForPost(postId string) ([]*model.Post, error) {
start := time.Now()
result, err := s.PostStore.GetEditHistoryForPost(postId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetEditHistoryForPost", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetEtag(channelID string, allowFromCache bool, collapsedThreads bool) string {
start := time.Now()
result := s.PostStore.GetEtag(channelID, allowFromCache, collapsedThreads)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetEtag", success, elapsed)
}
return result
}
func (s *TimerLayerPostStore) GetFlaggedPosts(userID string, offset int, limit int) (*model.PostList, error) {
start := time.Now()
result, err := s.PostStore.GetFlaggedPosts(userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetFlaggedPosts", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetFlaggedPostsForChannel(userID string, channelID string, offset int, limit int) (*model.PostList, error) {
start := time.Now()
result, err := s.PostStore.GetFlaggedPostsForChannel(userID, channelID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetFlaggedPostsForChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetFlaggedPostsForTeam(userID string, teamID string, offset int, limit int) (*model.PostList, error) {
start := time.Now()
result, err := s.PostStore.GetFlaggedPostsForTeam(userID, teamID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetFlaggedPostsForTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetMaxPostSize() int {
start := time.Now()
result := s.PostStore.GetMaxPostSize()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetMaxPostSize", success, elapsed)
}
return result
}
func (s *TimerLayerPostStore) GetNthRecentPostTime(n int64) (int64, error) {
start := time.Now()
result, err := s.PostStore.GetNthRecentPostTime(n)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetNthRecentPostTime", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetOldest() (*model.Post, error) {
start := time.Now()
result, err := s.PostStore.GetOldest()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetOldest", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetOldestEntityCreationTime() (int64, error) {
start := time.Now()
result, err := s.PostStore.GetOldestEntityCreationTime()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetOldestEntityCreationTime", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetParentsForExportAfter(limit int, afterID string) ([]*model.PostForExport, error) {
start := time.Now()
result, err := s.PostStore.GetParentsForExportAfter(limit, afterID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetParentsForExportAfter", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostAfterTime(channelID string, timestamp int64, collapsedThreads bool) (*model.Post, error) {
start := time.Now()
result, err := s.PostStore.GetPostAfterTime(channelID, timestamp, collapsedThreads)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostAfterTime", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostIdAfterTime(channelID string, timestamp int64, collapsedThreads bool) (string, error) {
start := time.Now()
result, err := s.PostStore.GetPostIdAfterTime(channelID, timestamp, collapsedThreads)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostIdAfterTime", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostIdBeforeTime(channelID string, timestamp int64, collapsedThreads bool) (string, error) {
start := time.Now()
result, err := s.PostStore.GetPostIdBeforeTime(channelID, timestamp, collapsedThreads)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostIdBeforeTime", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostReminderMetadata(postID string) (*store.PostReminderMetadata, error) {
start := time.Now()
result, err := s.PostStore.GetPostReminderMetadata(postID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostReminderMetadata", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostReminders(now int64) ([]*model.PostReminder, error) {
start := time.Now()
result, err := s.PostStore.GetPostReminders(now)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostReminders", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPosts(options model.GetPostsOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
start := time.Now()
result, err := s.PostStore.GetPosts(options, allowFromCache, sanitizeOptions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPosts", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostsAfter(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
start := time.Now()
result, err := s.PostStore.GetPostsAfter(options, sanitizeOptions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostsAfter", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostsBatchForIndexing(startTime int64, startPostID string, limit int) ([]*model.PostForIndexing, error) {
start := time.Now()
result, err := s.PostStore.GetPostsBatchForIndexing(startTime, startPostID, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostsBatchForIndexing", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostsBefore(options model.GetPostsOptions, sanitizeOptions map[string]bool) (*model.PostList, error) {
start := time.Now()
result, err := s.PostStore.GetPostsBefore(options, sanitizeOptions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostsBefore", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostsByIds(postIds []string) ([]*model.Post, error) {
start := time.Now()
result, err := s.PostStore.GetPostsByIds(postIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostsByIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostsByThread(threadID string, since int64) ([]*model.Post, error) {
start := time.Now()
result, err := s.PostStore.GetPostsByThread(threadID, since)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostsByThread", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostsCreatedAt(channelID string, timestamp int64) ([]*model.Post, error) {
start := time.Now()
result, err := s.PostStore.GetPostsCreatedAt(channelID, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostsCreatedAt", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostsSince(options model.GetPostsSinceOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
start := time.Now()
result, err := s.PostStore.GetPostsSince(options, allowFromCache, sanitizeOptions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostsSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetPostsSinceForSync(options model.GetPostsSinceForSyncOptions, cursor model.GetPostsSinceForSyncCursor, limit int) ([]*model.Post, model.GetPostsSinceForSyncCursor, error) {
start := time.Now()
result, resultVar1, err := s.PostStore.GetPostsSinceForSync(options, cursor, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetPostsSinceForSync", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerPostStore) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) {
start := time.Now()
result, err := s.PostStore.GetRecentSearchesForUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetRecentSearchesForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetRepliesForExport(parentID string) ([]*model.ReplyForExport, error) {
start := time.Now()
result, err := s.PostStore.GetRepliesForExport(parentID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetRepliesForExport", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetSingle(id string, inclDeleted bool) (*model.Post, error) {
start := time.Now()
result, err := s.PostStore.GetSingle(id, inclDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetSingle", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error) {
start := time.Now()
result, err := s.PostStore.GetTopDMsForUserSince(userID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetTopDMsForUserSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
start := time.Now()
result, err := s.PostStore.HasAutoResponsePostByUserSince(options, userId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.HasAutoResponsePostByUserSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) InvalidateLastPostTimeCache(channelID string) {
start := time.Now()
s.PostStore.InvalidateLastPostTimeCache(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.InvalidateLastPostTimeCache", success, elapsed)
}
}
func (s *TimerLayerPostStore) LogRecentSearch(userID string, searchQuery []byte, createAt int64) error {
start := time.Now()
err := s.PostStore.LogRecentSearch(userID, searchQuery, createAt)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.LogRecentSearch", success, elapsed)
}
return err
}
func (s *TimerLayerPostStore) Overwrite(post *model.Post) (*model.Post, error) {
start := time.Now()
result, err := s.PostStore.Overwrite(post)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.Overwrite", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) OverwriteMultiple(posts []*model.Post) ([]*model.Post, int, error) {
start := time.Now()
result, resultVar1, err := s.PostStore.OverwriteMultiple(posts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.OverwriteMultiple", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerPostStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
start := time.Now()
result, err := s.PostStore.PermanentDeleteBatch(endTime, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.PermanentDeleteBatch", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
start := time.Now()
result, resultVar1, err := s.PostStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.PermanentDeleteBatchForRetentionPolicies", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerPostStore) PermanentDeleteByChannel(channelID string) error {
start := time.Now()
err := s.PostStore.PermanentDeleteByChannel(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.PermanentDeleteByChannel", success, elapsed)
}
return err
}
func (s *TimerLayerPostStore) PermanentDeleteByUser(userID string) error {
start := time.Now()
err := s.PostStore.PermanentDeleteByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.PermanentDeleteByUser", success, elapsed)
}
return err
}
func (s *TimerLayerPostStore) Save(post *model.Post) (*model.Post, error) {
start := time.Now()
result, err := s.PostStore.Save(post)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, int, error) {
start := time.Now()
result, resultVar1, err := s.PostStore.SaveMultiple(posts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.SaveMultiple", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerPostStore) Search(teamID string, userID string, params *model.SearchParams) (*model.PostList, error) {
start := time.Now()
result, err := s.PostStore.Search(teamID, userID, params)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.Search", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) SearchPostsForUser(paramsList []*model.SearchParams, userID string, teamID string, page int, perPage int) (*model.PostSearchResults, error) {
start := time.Now()
result, err := s.PostStore.SearchPostsForUser(paramsList, userID, teamID, page, perPage)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.SearchPostsForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) SetPostReminder(reminder *model.PostReminder) error {
start := time.Now()
err := s.PostStore.SetPostReminder(reminder)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.SetPostReminder", success, elapsed)
}
return err
}
func (s *TimerLayerPostStore) Update(newPost *model.Post, oldPost *model.Post) (*model.Post, error) {
start := time.Now()
result, err := s.PostStore.Update(newPost, oldPost)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostAcknowledgementStore) Delete(acknowledgement *model.PostAcknowledgement) error {
start := time.Now()
err := s.PostAcknowledgementStore.Delete(acknowledgement)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostAcknowledgementStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerPostAcknowledgementStore) Get(postID string, userID string) (*model.PostAcknowledgement, error) {
start := time.Now()
result, err := s.PostAcknowledgementStore.Get(postID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostAcknowledgementStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostAcknowledgementStore) GetForPost(postID string) ([]*model.PostAcknowledgement, error) {
start := time.Now()
result, err := s.PostAcknowledgementStore.GetForPost(postID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostAcknowledgementStore.GetForPost", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostAcknowledgementStore) GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) {
start := time.Now()
result, err := s.PostAcknowledgementStore.GetForPosts(postIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostAcknowledgementStore.GetForPosts", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostAcknowledgementStore) Save(postID string, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) {
start := time.Now()
result, err := s.PostAcknowledgementStore.Save(postID, userID, acknowledgedAt)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostAcknowledgementStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) {
start := time.Now()
result, err := s.PostPriorityStore.GetForPost(postId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostPriorityStore.GetForPost", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostPriorityStore) GetForPosts(ids []string) ([]*model.PostPriority, error) {
start := time.Now()
result, err := s.PostPriorityStore.GetForPosts(ids)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostPriorityStore.GetForPosts", success, elapsed)
}
return result, err
}
func (s *TimerLayerPreferenceStore) CleanupFlagsBatch(limit int64) (int64, error) {
start := time.Now()
result, err := s.PreferenceStore.CleanupFlagsBatch(limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.CleanupFlagsBatch", success, elapsed)
}
return result, err
}
func (s *TimerLayerPreferenceStore) Delete(userID string, category string, name string) error {
start := time.Now()
err := s.PreferenceStore.Delete(userID, category, name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerPreferenceStore) DeleteCategory(userID string, category string) error {
start := time.Now()
err := s.PreferenceStore.DeleteCategory(userID, category)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.DeleteCategory", success, elapsed)
}
return err
}
func (s *TimerLayerPreferenceStore) DeleteCategoryAndName(category string, name string) error {
start := time.Now()
err := s.PreferenceStore.DeleteCategoryAndName(category, name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.DeleteCategoryAndName", success, elapsed)
}
return err
}
func (s *TimerLayerPreferenceStore) DeleteOrphanedRows(limit int) (int64, error) {
start := time.Now()
result, err := s.PreferenceStore.DeleteOrphanedRows(limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.DeleteOrphanedRows", success, elapsed)
}
return result, err
}
func (s *TimerLayerPreferenceStore) Get(userID string, category string, name string) (*model.Preference, error) {
start := time.Now()
result, err := s.PreferenceStore.Get(userID, category, name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerPreferenceStore) GetAll(userID string) (model.Preferences, error) {
start := time.Now()
result, err := s.PreferenceStore.GetAll(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerPreferenceStore) GetCategory(userID string, category string) (model.Preferences, error) {
start := time.Now()
result, err := s.PreferenceStore.GetCategory(userID, category)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.GetCategory", success, elapsed)
}
return result, err
}
func (s *TimerLayerPreferenceStore) GetCategoryAndName(category string, nane string) (model.Preferences, error) {
start := time.Now()
result, err := s.PreferenceStore.GetCategoryAndName(category, nane)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.GetCategoryAndName", success, elapsed)
}
return result, err
}
func (s *TimerLayerPreferenceStore) PermanentDeleteByUser(userID string) error {
start := time.Now()
err := s.PreferenceStore.PermanentDeleteByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.PermanentDeleteByUser", success, elapsed)
}
return err
}
func (s *TimerLayerPreferenceStore) Save(preferences model.Preferences) error {
start := time.Now()
err := s.PreferenceStore.Save(preferences)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PreferenceStore.Save", success, elapsed)
}
return err
}
func (s *TimerLayerProductNoticesStore) Clear(notices []string) error {
start := time.Now()
err := s.ProductNoticesStore.Clear(notices)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ProductNoticesStore.Clear", success, elapsed)
}
return err
}
func (s *TimerLayerProductNoticesStore) ClearOldNotices(currentNotices model.ProductNotices) error {
start := time.Now()
err := s.ProductNoticesStore.ClearOldNotices(currentNotices)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ProductNoticesStore.ClearOldNotices", success, elapsed)
}
return err
}
func (s *TimerLayerProductNoticesStore) GetViews(userID string) ([]model.ProductNoticeViewState, error) {
start := time.Now()
result, err := s.ProductNoticesStore.GetViews(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ProductNoticesStore.GetViews", success, elapsed)
}
return result, err
}
func (s *TimerLayerProductNoticesStore) View(userID string, notices []string) error {
start := time.Now()
err := s.ProductNoticesStore.View(userID, notices)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ProductNoticesStore.View", success, elapsed)
}
return err
}
func (s *TimerLayerReactionStore) BulkGetForPosts(postIds []string) ([]*model.Reaction, error) {
start := time.Now()
result, err := s.ReactionStore.BulkGetForPosts(postIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.BulkGetForPosts", success, elapsed)
}
return result, err
}
func (s *TimerLayerReactionStore) Delete(reaction *model.Reaction) (*model.Reaction, error) {
start := time.Now()
result, err := s.ReactionStore.Delete(reaction)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.Delete", success, elapsed)
}
return result, err
}
func (s *TimerLayerReactionStore) DeleteAllWithEmojiName(emojiName string) error {
start := time.Now()
err := s.ReactionStore.DeleteAllWithEmojiName(emojiName)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.DeleteAllWithEmojiName", success, elapsed)
}
return err
}
func (s *TimerLayerReactionStore) DeleteOrphanedRows(limit int) (int64, error) {
start := time.Now()
result, err := s.ReactionStore.DeleteOrphanedRows(limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.DeleteOrphanedRows", success, elapsed)
}
return result, err
}
func (s *TimerLayerReactionStore) GetForPost(postID string, allowFromCache bool) ([]*model.Reaction, error) {
start := time.Now()
result, err := s.ReactionStore.GetForPost(postID, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.GetForPost", success, elapsed)
}
return result, err
}
func (s *TimerLayerReactionStore) GetForPostSince(postId string, since int64, excludeRemoteId string, inclDeleted bool) ([]*model.Reaction, error) {
start := time.Now()
result, err := s.ReactionStore.GetForPostSince(postId, since, excludeRemoteId, inclDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.GetForPostSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerReactionStore) GetTopForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
start := time.Now()
result, err := s.ReactionStore.GetTopForTeamSince(teamID, userID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.GetTopForTeamSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerReactionStore) GetTopForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
start := time.Now()
result, err := s.ReactionStore.GetTopForUserSince(userID, teamID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.GetTopForUserSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerReactionStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
start := time.Now()
result, err := s.ReactionStore.PermanentDeleteBatch(endTime, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.PermanentDeleteBatch", success, elapsed)
}
return result, err
}
func (s *TimerLayerReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) {
start := time.Now()
result, err := s.ReactionStore.Save(reaction)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerRemoteClusterStore) Delete(remoteClusterId string) (bool, error) {
start := time.Now()
result, err := s.RemoteClusterStore.Delete(remoteClusterId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RemoteClusterStore.Delete", success, elapsed)
}
return result, err
}
func (s *TimerLayerRemoteClusterStore) Get(remoteClusterId string) (*model.RemoteCluster, error) {
start := time.Now()
result, err := s.RemoteClusterStore.Get(remoteClusterId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RemoteClusterStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerRemoteClusterStore) GetAll(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
start := time.Now()
result, err := s.RemoteClusterStore.GetAll(filter)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RemoteClusterStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerRemoteClusterStore) Save(rc *model.RemoteCluster) (*model.RemoteCluster, error) {
start := time.Now()
result, err := s.RemoteClusterStore.Save(rc)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RemoteClusterStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerRemoteClusterStore) SetLastPingAt(remoteClusterId string) error {
start := time.Now()
err := s.RemoteClusterStore.SetLastPingAt(remoteClusterId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RemoteClusterStore.SetLastPingAt", success, elapsed)
}
return err
}
func (s *TimerLayerRemoteClusterStore) Update(rc *model.RemoteCluster) (*model.RemoteCluster, error) {
start := time.Now()
result, err := s.RemoteClusterStore.Update(rc)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RemoteClusterStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerRemoteClusterStore) UpdateTopics(remoteClusterId string, topics string) (*model.RemoteCluster, error) {
start := time.Now()
result, err := s.RemoteClusterStore.UpdateTopics(remoteClusterId, topics)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RemoteClusterStore.UpdateTopics", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) AddChannels(policyId string, channelIds []string) error {
start := time.Now()
err := s.RetentionPolicyStore.AddChannels(policyId, channelIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.AddChannels", success, elapsed)
}
return err
}
func (s *TimerLayerRetentionPolicyStore) AddTeams(policyId string, teamIds []string) error {
start := time.Now()
err := s.RetentionPolicyStore.AddTeams(policyId, teamIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.AddTeams", success, elapsed)
}
return err
}
func (s *TimerLayerRetentionPolicyStore) Delete(id string) error {
start := time.Now()
err := s.RetentionPolicyStore.Delete(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerRetentionPolicyStore) DeleteOrphanedRows(limit int) (int64, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.DeleteOrphanedRows(limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.DeleteOrphanedRows", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) Get(id string) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.Get(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) GetAll(offset int, limit int) ([]*model.RetentionPolicyWithTeamAndChannelCounts, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.GetAll(offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) GetChannelPoliciesCountForUser(userID string) (int64, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.GetChannelPoliciesCountForUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetChannelPoliciesCountForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) GetChannelPoliciesForUser(userID string, offset int, limit int) ([]*model.RetentionPolicyForChannel, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.GetChannelPoliciesForUser(userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetChannelPoliciesForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) GetChannels(policyId string, offset int, limit int) (model.ChannelListWithTeamData, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.GetChannels(policyId, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetChannels", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) GetChannelsCount(policyId string) (int64, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.GetChannelsCount(policyId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetChannelsCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) GetCount() (int64, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.GetCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.GetTeamPoliciesCountForUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetTeamPoliciesCountForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) GetTeamPoliciesForUser(userID string, offset int, limit int) ([]*model.RetentionPolicyForTeam, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.GetTeamPoliciesForUser(userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetTeamPoliciesForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) GetTeams(policyId string, offset int, limit int) ([]*model.Team, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.GetTeams(policyId, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetTeams", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) GetTeamsCount(policyId string) (int64, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.GetTeamsCount(policyId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetTeamsCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) Patch(patch *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.Patch(patch)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.Patch", success, elapsed)
}
return result, err
}
func (s *TimerLayerRetentionPolicyStore) RemoveChannels(policyId string, channelIds []string) error {
start := time.Now()
err := s.RetentionPolicyStore.RemoveChannels(policyId, channelIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.RemoveChannels", success, elapsed)
}
return err
}
func (s *TimerLayerRetentionPolicyStore) RemoveTeams(policyId string, teamIds []string) error {
start := time.Now()
err := s.RetentionPolicyStore.RemoveTeams(policyId, teamIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.RemoveTeams", success, elapsed)
}
return err
}
func (s *TimerLayerRetentionPolicyStore) Save(policy *model.RetentionPolicyWithTeamAndChannelIDs) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
start := time.Now()
result, err := s.RetentionPolicyStore.Save(policy)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerRoleStore) AllChannelSchemeRoles() ([]*model.Role, error) {
start := time.Now()
result, err := s.RoleStore.AllChannelSchemeRoles()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.AllChannelSchemeRoles", success, elapsed)
}
return result, err
}
func (s *TimerLayerRoleStore) ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, error) {
start := time.Now()
result, err := s.RoleStore.ChannelHigherScopedPermissions(roleNames)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.ChannelHigherScopedPermissions", success, elapsed)
}
return result, err
}
func (s *TimerLayerRoleStore) ChannelRolesUnderTeamRole(roleName string) ([]*model.Role, error) {
start := time.Now()
result, err := s.RoleStore.ChannelRolesUnderTeamRole(roleName)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.ChannelRolesUnderTeamRole", success, elapsed)
}
return result, err
}
func (s *TimerLayerRoleStore) Delete(roleID string) (*model.Role, error) {
start := time.Now()
result, err := s.RoleStore.Delete(roleID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.Delete", success, elapsed)
}
return result, err
}
func (s *TimerLayerRoleStore) Get(roleID string) (*model.Role, error) {
start := time.Now()
result, err := s.RoleStore.Get(roleID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerRoleStore) GetAll() ([]*model.Role, error) {
start := time.Now()
result, err := s.RoleStore.GetAll()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerRoleStore) GetByName(ctx context.Context, name string) (*model.Role, error) {
start := time.Now()
result, err := s.RoleStore.GetByName(ctx, name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.GetByName", success, elapsed)
}
return result, err
}
func (s *TimerLayerRoleStore) GetByNames(names []string) ([]*model.Role, error) {
start := time.Now()
result, err := s.RoleStore.GetByNames(names)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.GetByNames", success, elapsed)
}
return result, err
}
func (s *TimerLayerRoleStore) PermanentDeleteAll() error {
start := time.Now()
err := s.RoleStore.PermanentDeleteAll()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.PermanentDeleteAll", success, elapsed)
}
return err
}
func (s *TimerLayerRoleStore) Save(role *model.Role) (*model.Role, error) {
start := time.Now()
result, err := s.RoleStore.Save(role)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerSchemeStore) CountByScope(scope string) (int64, error) {
start := time.Now()
result, err := s.SchemeStore.CountByScope(scope)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SchemeStore.CountByScope", success, elapsed)
}
return result, err
}
func (s *TimerLayerSchemeStore) CountWithoutPermission(scope string, permissionID string, roleScope model.RoleScope, roleType model.RoleType) (int64, error) {
start := time.Now()
result, err := s.SchemeStore.CountWithoutPermission(scope, permissionID, roleScope, roleType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SchemeStore.CountWithoutPermission", success, elapsed)
}
return result, err
}
func (s *TimerLayerSchemeStore) Delete(schemeID string) (*model.Scheme, error) {
start := time.Now()
result, err := s.SchemeStore.Delete(schemeID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SchemeStore.Delete", success, elapsed)
}
return result, err
}
func (s *TimerLayerSchemeStore) Get(schemeID string) (*model.Scheme, error) {
start := time.Now()
result, err := s.SchemeStore.Get(schemeID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SchemeStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerSchemeStore) GetAllPage(scope string, offset int, limit int) ([]*model.Scheme, error) {
start := time.Now()
result, err := s.SchemeStore.GetAllPage(scope, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SchemeStore.GetAllPage", success, elapsed)
}
return result, err
}
func (s *TimerLayerSchemeStore) GetByName(schemeName string) (*model.Scheme, error) {
start := time.Now()
result, err := s.SchemeStore.GetByName(schemeName)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SchemeStore.GetByName", success, elapsed)
}
return result, err
}
func (s *TimerLayerSchemeStore) PermanentDeleteAll() error {
start := time.Now()
err := s.SchemeStore.PermanentDeleteAll()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SchemeStore.PermanentDeleteAll", success, elapsed)
}
return err
}
func (s *TimerLayerSchemeStore) Save(scheme *model.Scheme) (*model.Scheme, error) {
start := time.Now()
result, err := s.SchemeStore.Save(scheme)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SchemeStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerSessionStore) AnalyticsSessionCount() (int64, error) {
start := time.Now()
result, err := s.SessionStore.AnalyticsSessionCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.AnalyticsSessionCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerSessionStore) Cleanup(expiryTime int64, batchSize int64) error {
start := time.Now()
err := s.SessionStore.Cleanup(expiryTime, batchSize)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.Cleanup", success, elapsed)
}
return err
}
func (s *TimerLayerSessionStore) Get(ctx context.Context, sessionIDOrToken string) (*model.Session, error) {
start := time.Now()
result, err := s.SessionStore.Get(ctx, sessionIDOrToken)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerSessionStore) GetSessions(userID string) ([]*model.Session, error) {
start := time.Now()
result, err := s.SessionStore.GetSessions(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.GetSessions", success, elapsed)
}
return result, err
}
func (s *TimerLayerSessionStore) GetSessionsExpired(thresholdMillis int64, mobileOnly bool, unnotifiedOnly bool) ([]*model.Session, error) {
start := time.Now()
result, err := s.SessionStore.GetSessionsExpired(thresholdMillis, mobileOnly, unnotifiedOnly)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.GetSessionsExpired", success, elapsed)
}
return result, err
}
func (s *TimerLayerSessionStore) GetSessionsWithActiveDeviceIds(userID string) ([]*model.Session, error) {
start := time.Now()
result, err := s.SessionStore.GetSessionsWithActiveDeviceIds(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.GetSessionsWithActiveDeviceIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerSessionStore) PermanentDeleteSessionsByUser(teamID string) error {
start := time.Now()
err := s.SessionStore.PermanentDeleteSessionsByUser(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.PermanentDeleteSessionsByUser", success, elapsed)
}
return err
}
func (s *TimerLayerSessionStore) Remove(sessionIDOrToken string) error {
start := time.Now()
err := s.SessionStore.Remove(sessionIDOrToken)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.Remove", success, elapsed)
}
return err
}
func (s *TimerLayerSessionStore) RemoveAllSessions() error {
start := time.Now()
err := s.SessionStore.RemoveAllSessions()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.RemoveAllSessions", success, elapsed)
}
return err
}
func (s *TimerLayerSessionStore) Save(session *model.Session) (*model.Session, error) {
start := time.Now()
result, err := s.SessionStore.Save(session)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerSessionStore) UpdateDeviceId(id string, deviceID string, expiresAt int64) (string, error) {
start := time.Now()
result, err := s.SessionStore.UpdateDeviceId(id, deviceID, expiresAt)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.UpdateDeviceId", success, elapsed)
}
return result, err
}
func (s *TimerLayerSessionStore) UpdateExpiredNotify(sessionid string, notified bool) error {
start := time.Now()
err := s.SessionStore.UpdateExpiredNotify(sessionid, notified)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.UpdateExpiredNotify", success, elapsed)
}
return err
}
func (s *TimerLayerSessionStore) UpdateExpiresAt(sessionID string, timestamp int64) error {
start := time.Now()
err := s.SessionStore.UpdateExpiresAt(sessionID, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.UpdateExpiresAt", success, elapsed)
}
return err
}
func (s *TimerLayerSessionStore) UpdateLastActivityAt(sessionID string, timestamp int64) error {
start := time.Now()
err := s.SessionStore.UpdateLastActivityAt(sessionID, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.UpdateLastActivityAt", success, elapsed)
}
return err
}
func (s *TimerLayerSessionStore) UpdateProps(session *model.Session) error {
start := time.Now()
err := s.SessionStore.UpdateProps(session)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.UpdateProps", success, elapsed)
}
return err
}
func (s *TimerLayerSessionStore) UpdateRoles(userID string, roles string) (string, error) {
start := time.Now()
result, err := s.SessionStore.UpdateRoles(userID, roles)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.UpdateRoles", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) Delete(channelId string) (bool, error) {
start := time.Now()
result, err := s.SharedChannelStore.Delete(channelId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.Delete", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) DeleteRemote(remoteId string) (bool, error) {
start := time.Now()
result, err := s.SharedChannelStore.DeleteRemote(remoteId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.DeleteRemote", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) Get(channelId string) (*model.SharedChannel, error) {
start := time.Now()
result, err := s.SharedChannelStore.Get(channelId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetAll(offset int, limit int, opts model.SharedChannelFilterOpts) ([]*model.SharedChannel, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetAll(offset, limit, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetAllCount(opts model.SharedChannelFilterOpts) (int64, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetAllCount(opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetAllCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetAttachment(fileId string, remoteId string) (*model.SharedChannelAttachment, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetAttachment(fileId, remoteId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetAttachment", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetRemote(id string) (*model.SharedChannelRemote, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetRemote(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetRemote", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetRemoteByIds(channelId string, remoteId string) (*model.SharedChannelRemote, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetRemoteByIds(channelId, remoteId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetRemoteByIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetRemoteForUser(remoteId string, userId string) (*model.RemoteCluster, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetRemoteForUser(remoteId, userId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetRemoteForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetRemotes(opts model.SharedChannelRemoteFilterOpts) ([]*model.SharedChannelRemote, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetRemotes(opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetRemotes", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetRemotesStatus(channelId string) ([]*model.SharedChannelRemoteStatus, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetRemotesStatus(channelId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetRemotesStatus", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetSingleUser(userID string, channelID string, remoteID string) (*model.SharedChannelUser, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetSingleUser(userID, channelID, remoteID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetSingleUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetUsersForSync(filter model.GetUsersForSyncFilter) ([]*model.User, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetUsersForSync(filter)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetUsersForSync", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) GetUsersForUser(userID string) ([]*model.SharedChannelUser, error) {
start := time.Now()
result, err := s.SharedChannelStore.GetUsersForUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.GetUsersForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) HasChannel(channelID string) (bool, error) {
start := time.Now()
result, err := s.SharedChannelStore.HasChannel(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.HasChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) HasRemote(channelID string, remoteId string) (bool, error) {
start := time.Now()
result, err := s.SharedChannelStore.HasRemote(channelID, remoteId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.HasRemote", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) Save(sc *model.SharedChannel) (*model.SharedChannel, error) {
start := time.Now()
result, err := s.SharedChannelStore.Save(sc)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) SaveAttachment(remote *model.SharedChannelAttachment) (*model.SharedChannelAttachment, error) {
start := time.Now()
result, err := s.SharedChannelStore.SaveAttachment(remote)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.SaveAttachment", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) SaveRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
start := time.Now()
result, err := s.SharedChannelStore.SaveRemote(remote)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.SaveRemote", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) SaveUser(remote *model.SharedChannelUser) (*model.SharedChannelUser, error) {
start := time.Now()
result, err := s.SharedChannelStore.SaveUser(remote)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.SaveUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) Update(sc *model.SharedChannel) (*model.SharedChannel, error) {
start := time.Now()
result, err := s.SharedChannelStore.Update(sc)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) UpdateAttachmentLastSyncAt(id string, syncTime int64) error {
start := time.Now()
err := s.SharedChannelStore.UpdateAttachmentLastSyncAt(id, syncTime)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.UpdateAttachmentLastSyncAt", success, elapsed)
}
return err
}
func (s *TimerLayerSharedChannelStore) UpdateRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
start := time.Now()
result, err := s.SharedChannelStore.UpdateRemote(remote)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.UpdateRemote", success, elapsed)
}
return result, err
}
func (s *TimerLayerSharedChannelStore) UpdateRemoteCursor(id string, cursor model.GetPostsSinceForSyncCursor) error {
start := time.Now()
err := s.SharedChannelStore.UpdateRemoteCursor(id, cursor)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.UpdateRemoteCursor", success, elapsed)
}
return err
}
func (s *TimerLayerSharedChannelStore) UpdateUserLastSyncAt(userID string, channelID string, remoteID string) error {
start := time.Now()
err := s.SharedChannelStore.UpdateUserLastSyncAt(userID, channelID, remoteID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.UpdateUserLastSyncAt", success, elapsed)
}
return err
}
func (s *TimerLayerSharedChannelStore) UpsertAttachment(remote *model.SharedChannelAttachment) (string, error) {
start := time.Now()
result, err := s.SharedChannelStore.UpsertAttachment(remote)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SharedChannelStore.UpsertAttachment", success, elapsed)
}
return result, err
}
func (s *TimerLayerStatusStore) Get(userID string) (*model.Status, error) {
start := time.Now()
result, err := s.StatusStore.Get(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("StatusStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerStatusStore) GetByIds(userIds []string) ([]*model.Status, error) {
start := time.Now()
result, err := s.StatusStore.GetByIds(userIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("StatusStore.GetByIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerStatusStore) GetTotalActiveUsersCount() (int64, error) {
start := time.Now()
result, err := s.StatusStore.GetTotalActiveUsersCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("StatusStore.GetTotalActiveUsersCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerStatusStore) ResetAll() error {
start := time.Now()
err := s.StatusStore.ResetAll()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("StatusStore.ResetAll", success, elapsed)
}
return err
}
func (s *TimerLayerStatusStore) SaveOrUpdate(status *model.Status) error {
start := time.Now()
err := s.StatusStore.SaveOrUpdate(status)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("StatusStore.SaveOrUpdate", success, elapsed)
}
return err
}
func (s *TimerLayerStatusStore) UpdateExpiredDNDStatuses() ([]*model.Status, error) {
start := time.Now()
result, err := s.StatusStore.UpdateExpiredDNDStatuses()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("StatusStore.UpdateExpiredDNDStatuses", success, elapsed)
}
return result, err
}
func (s *TimerLayerStatusStore) UpdateLastActivityAt(userID string, lastActivityAt int64) error {
start := time.Now()
err := s.StatusStore.UpdateLastActivityAt(userID, lastActivityAt)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("StatusStore.UpdateLastActivityAt", success, elapsed)
}
return err
}
func (s *TimerLayerSystemStore) Get() (model.StringMap, error) {
start := time.Now()
result, err := s.SystemStore.Get()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SystemStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerSystemStore) GetByName(name string) (*model.System, error) {
start := time.Now()
result, err := s.SystemStore.GetByName(name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SystemStore.GetByName", success, elapsed)
}
return result, err
}
func (s *TimerLayerSystemStore) InsertIfExists(system *model.System) (*model.System, error) {
start := time.Now()
result, err := s.SystemStore.InsertIfExists(system)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SystemStore.InsertIfExists", success, elapsed)
}
return result, err
}
func (s *TimerLayerSystemStore) PermanentDeleteByName(name string) (*model.System, error) {
start := time.Now()
result, err := s.SystemStore.PermanentDeleteByName(name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SystemStore.PermanentDeleteByName", success, elapsed)
}
return result, err
}
func (s *TimerLayerSystemStore) Save(system *model.System) error {
start := time.Now()
err := s.SystemStore.Save(system)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SystemStore.Save", success, elapsed)
}
return err
}
func (s *TimerLayerSystemStore) SaveOrUpdate(system *model.System) error {
start := time.Now()
err := s.SystemStore.SaveOrUpdate(system)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SystemStore.SaveOrUpdate", success, elapsed)
}
return err
}
func (s *TimerLayerSystemStore) SaveOrUpdateWithWarnMetricHandling(system *model.System) error {
start := time.Now()
err := s.SystemStore.SaveOrUpdateWithWarnMetricHandling(system)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SystemStore.SaveOrUpdateWithWarnMetricHandling", success, elapsed)
}
return err
}
func (s *TimerLayerSystemStore) Update(system *model.System) error {
start := time.Now()
err := s.SystemStore.Update(system)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SystemStore.Update", success, elapsed)
}
return err
}
func (s *TimerLayerTeamStore) AnalyticsGetTeamCountForScheme(schemeID string) (int64, error) {
start := time.Now()
result, err := s.TeamStore.AnalyticsGetTeamCountForScheme(schemeID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.AnalyticsGetTeamCountForScheme", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) AnalyticsTeamCount(opts *model.TeamSearch) (int64, error) {
start := time.Now()
result, err := s.TeamStore.AnalyticsTeamCount(opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.AnalyticsTeamCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) ClearAllCustomRoleAssignments() error {
start := time.Now()
err := s.TeamStore.ClearAllCustomRoleAssignments()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.ClearAllCustomRoleAssignments", success, elapsed)
}
return err
}
func (s *TimerLayerTeamStore) ClearCaches() {
start := time.Now()
s.TeamStore.ClearCaches()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.ClearCaches", success, elapsed)
}
}
func (s *TimerLayerTeamStore) Get(id string) (*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.Get(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetActiveMemberCount(teamID string, restrictions *model.ViewUsersRestrictions) (int64, error) {
start := time.Now()
result, err := s.TeamStore.GetActiveMemberCount(teamID, restrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetActiveMemberCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetAll() ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetAll()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetAllForExportAfter(limit int, afterID string) ([]*model.TeamForExport, error) {
start := time.Now()
result, err := s.TeamStore.GetAllForExportAfter(limit, afterID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetAllForExportAfter", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetAllPage(offset int, limit int, opts *model.TeamSearch) ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetAllPage(offset, limit, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetAllPage", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetAllPrivateTeamListing() ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetAllPrivateTeamListing()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetAllPrivateTeamListing", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetAllTeamListing() ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetAllTeamListing()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetAllTeamListing", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetByEmptyInviteID() ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetByEmptyInviteID()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetByEmptyInviteID", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetByInviteId(inviteID string) (*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetByInviteId(inviteID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetByInviteId", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetByName(name string) (*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetByName(name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetByName", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetByNames(name []string) ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetByNames(name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetByNames", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetChannelUnreadsForAllTeams(excludeTeamID string, userID string) ([]*model.ChannelUnread, error) {
start := time.Now()
result, err := s.TeamStore.GetChannelUnreadsForAllTeams(excludeTeamID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetChannelUnreadsForAllTeams", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetChannelUnreadsForTeam(teamID string, userID string) ([]*model.ChannelUnread, error) {
start := time.Now()
result, err := s.TeamStore.GetChannelUnreadsForTeam(teamID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetChannelUnreadsForTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetCommonTeamIDsForTwoUsers(userID string, otherUserID string) ([]string, error) {
start := time.Now()
result, err := s.TeamStore.GetCommonTeamIDsForTwoUsers(userID, otherUserID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetCommonTeamIDsForTwoUsers", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetMany(ids []string) ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetMany(ids)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetMany", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetMember(ctx context.Context, teamID string, userID string) (*model.TeamMember, error) {
start := time.Now()
result, err := s.TeamStore.GetMember(ctx, teamID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetMember", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetMembers(teamID string, offset int, limit int, teamMembersGetOptions *model.TeamMembersGetOptions) ([]*model.TeamMember, error) {
start := time.Now()
result, err := s.TeamStore.GetMembers(teamID, offset, limit, teamMembersGetOptions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetMembersByIds(teamID string, userIds []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, error) {
start := time.Now()
result, err := s.TeamStore.GetMembersByIds(teamID, userIds, restrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetMembersByIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetNewTeamMembersSince(teamID string, since int64, offset int, limit int) (*model.NewTeamMembersList, int64, error) {
start := time.Now()
result, resultVar1, err := s.TeamStore.GetNewTeamMembersSince(teamID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetNewTeamMembersSince", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerTeamStore) GetTeamMembersForExport(userID string) ([]*model.TeamMemberForExport, error) {
start := time.Now()
result, err := s.TeamStore.GetTeamMembersForExport(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetTeamMembersForExport", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetTeamsByScheme(schemeID string, offset int, limit int) ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetTeamsByScheme(schemeID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetTeamsByScheme", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetTeamsByUserId(userID string) ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.GetTeamsByUserId(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetTeamsByUserId", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetTeamsForUser(ctx context.Context, userID string, excludeTeamID string, includeDeleted bool) ([]*model.TeamMember, error) {
start := time.Now()
result, err := s.TeamStore.GetTeamsForUser(ctx, userID, excludeTeamID, includeDeleted)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetTeamsForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetTeamsForUserWithPagination(userID string, page int, perPage int) ([]*model.TeamMember, error) {
start := time.Now()
result, err := s.TeamStore.GetTeamsForUserWithPagination(userID, page, perPage)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetTeamsForUserWithPagination", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetTotalMemberCount(teamID string, restrictions *model.ViewUsersRestrictions) (int64, error) {
start := time.Now()
result, err := s.TeamStore.GetTotalMemberCount(teamID, restrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetTotalMemberCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetUserTeamIds(userID string, allowFromCache bool) ([]string, error) {
start := time.Now()
result, err := s.TeamStore.GetUserTeamIds(userID, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetUserTeamIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GroupSyncedTeamCount() (int64, error) {
start := time.Now()
result, err := s.TeamStore.GroupSyncedTeamCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GroupSyncedTeamCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) InvalidateAllTeamIdsForUser(userID string) {
start := time.Now()
s.TeamStore.InvalidateAllTeamIdsForUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.InvalidateAllTeamIdsForUser", success, elapsed)
}
}
func (s *TimerLayerTeamStore) MigrateTeamMembers(fromTeamID string, fromUserID string) (map[string]string, error) {
start := time.Now()
result, err := s.TeamStore.MigrateTeamMembers(fromTeamID, fromUserID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.MigrateTeamMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) PermanentDelete(teamID string) error {
start := time.Now()
err := s.TeamStore.PermanentDelete(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.PermanentDelete", success, elapsed)
}
return err
}
func (s *TimerLayerTeamStore) RemoveAllMembersByTeam(teamID string) error {
start := time.Now()
err := s.TeamStore.RemoveAllMembersByTeam(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.RemoveAllMembersByTeam", success, elapsed)
}
return err
}
func (s *TimerLayerTeamStore) RemoveAllMembersByUser(userID string) error {
start := time.Now()
err := s.TeamStore.RemoveAllMembersByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.RemoveAllMembersByUser", success, elapsed)
}
return err
}
func (s *TimerLayerTeamStore) RemoveMember(teamID string, userID string) error {
start := time.Now()
err := s.TeamStore.RemoveMember(teamID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.RemoveMember", success, elapsed)
}
return err
}
func (s *TimerLayerTeamStore) RemoveMembers(teamID string, userIds []string) error {
start := time.Now()
err := s.TeamStore.RemoveMembers(teamID, userIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.RemoveMembers", success, elapsed)
}
return err
}
func (s *TimerLayerTeamStore) ResetAllTeamSchemes() error {
start := time.Now()
err := s.TeamStore.ResetAllTeamSchemes()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.ResetAllTeamSchemes", success, elapsed)
}
return err
}
func (s *TimerLayerTeamStore) Save(team *model.Team) (*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.Save(team)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) SaveMember(member *model.TeamMember, maxUsersPerTeam int) (*model.TeamMember, error) {
start := time.Now()
result, err := s.TeamStore.SaveMember(member, maxUsersPerTeam)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.SaveMember", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) SaveMultipleMembers(members []*model.TeamMember, maxUsersPerTeam int) ([]*model.TeamMember, error) {
start := time.Now()
result, err := s.TeamStore.SaveMultipleMembers(members, maxUsersPerTeam)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.SaveMultipleMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) SearchAll(opts *model.TeamSearch) ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.SearchAll(opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.SearchAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) SearchAllPaged(opts *model.TeamSearch) ([]*model.Team, int64, error) {
start := time.Now()
result, resultVar1, err := s.TeamStore.SearchAllPaged(opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.SearchAllPaged", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerTeamStore) SearchOpen(opts *model.TeamSearch) ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.SearchOpen(opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.SearchOpen", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) SearchPrivate(opts *model.TeamSearch) ([]*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.SearchPrivate(opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.SearchPrivate", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) Update(team *model.Team) (*model.Team, error) {
start := time.Now()
result, err := s.TeamStore.Update(team)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) UpdateLastTeamIconUpdate(teamID string, curTime int64) error {
start := time.Now()
err := s.TeamStore.UpdateLastTeamIconUpdate(teamID, curTime)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.UpdateLastTeamIconUpdate", success, elapsed)
}
return err
}
func (s *TimerLayerTeamStore) UpdateMember(member *model.TeamMember) (*model.TeamMember, error) {
start := time.Now()
result, err := s.TeamStore.UpdateMember(member)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.UpdateMember", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) UpdateMembersRole(teamID string, userIDs []string) error {
start := time.Now()
err := s.TeamStore.UpdateMembersRole(teamID, userIDs)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.UpdateMembersRole", success, elapsed)
}
return err
}
func (s *TimerLayerTeamStore) UpdateMultipleMembers(members []*model.TeamMember) ([]*model.TeamMember, error) {
start := time.Now()
result, err := s.TeamStore.UpdateMultipleMembers(members)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.UpdateMultipleMembers", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) UserBelongsToTeams(userID string, teamIds []string) (bool, error) {
start := time.Now()
result, err := s.TeamStore.UserBelongsToTeams(userID, teamIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.UserBelongsToTeams", success, elapsed)
}
return result, err
}
func (s *TimerLayerTermsOfServiceStore) Get(id string, allowFromCache bool) (*model.TermsOfService, error) {
start := time.Now()
result, err := s.TermsOfServiceStore.Get(id, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TermsOfServiceStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerTermsOfServiceStore) GetLatest(allowFromCache bool) (*model.TermsOfService, error) {
start := time.Now()
result, err := s.TermsOfServiceStore.GetLatest(allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TermsOfServiceStore.GetLatest", success, elapsed)
}
return result, err
}
func (s *TimerLayerTermsOfServiceStore) Save(termsOfService *model.TermsOfService) (*model.TermsOfService, error) {
start := time.Now()
result, err := s.TermsOfServiceStore.Save(termsOfService)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TermsOfServiceStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) DeleteMembershipForUser(userId string, postID string) error {
start := time.Now()
err := s.ThreadStore.DeleteMembershipForUser(userId, postID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.DeleteMembershipForUser", success, elapsed)
}
return err
}
func (s *TimerLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
start := time.Now()
result, err := s.ThreadStore.DeleteOrphanedRows(limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.DeleteOrphanedRows", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) Get(id string) (*model.Thread, error) {
start := time.Now()
result, err := s.ThreadStore.Get(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetMembershipForUser(userId string, postID string) (*model.ThreadMembership, error) {
start := time.Now()
result, err := s.ThreadStore.GetMembershipForUser(userId, postID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetMembershipForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetMembershipsForUser(userId string, teamID string) ([]*model.ThreadMembership, error) {
start := time.Now()
result, err := s.ThreadStore.GetMembershipsForUser(userId, teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetMembershipsForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string, includeUrgentMentionCount bool) (map[string]*model.TeamUnread, error) {
start := time.Now()
result, err := s.ThreadStore.GetTeamsUnreadForUser(userID, teamIDs, includeUrgentMentionCount)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetTeamsUnreadForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetThreadFollowers(threadID string, fetchOnlyActive bool) ([]string, error) {
start := time.Now()
result, err := s.ThreadStore.GetThreadFollowers(threadID, fetchOnlyActive)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetThreadFollowers", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool, postPriorityIsEnabled bool) (*model.ThreadResponse, error) {
start := time.Now()
result, err := s.ThreadStore.GetThreadForUser(threadMembership, extended, postPriorityIsEnabled)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetThreadForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error) {
start := time.Now()
result, err := s.ThreadStore.GetThreadUnreadReplyCount(threadMembership)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetThreadUnreadReplyCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetThreadsForUser(userId string, teamID string, opts model.GetUserThreadsOpts) ([]*model.ThreadResponse, error) {
start := time.Now()
result, err := s.ThreadStore.GetThreadsForUser(userId, teamID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetThreadsForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
start := time.Now()
result, err := s.ThreadStore.GetTopThreadsForTeamSince(teamID, userID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetTopThreadsForTeamSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
start := time.Now()
result, err := s.ThreadStore.GetTopThreadsForUserSince(teamID, userID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetTopThreadsForUserSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetTotalThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
start := time.Now()
result, err := s.ThreadStore.GetTotalThreads(userId, teamID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetTotalThreads", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetTotalUnreadMentions(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
start := time.Now()
result, err := s.ThreadStore.GetTotalUnreadMentions(userId, teamID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetTotalUnreadMentions", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetTotalUnreadThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
start := time.Now()
result, err := s.ThreadStore.GetTotalUnreadThreads(userId, teamID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetTotalUnreadThreads", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetTotalUnreadUrgentMentions(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
start := time.Now()
result, err := s.ThreadStore.GetTotalUnreadUrgentMentions(userId, teamID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetTotalUnreadUrgentMentions", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) MaintainMembership(userID string, postID string, opts store.ThreadMembershipOpts) (*model.ThreadMembership, error) {
start := time.Now()
result, err := s.ThreadStore.MaintainMembership(userID, postID, opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.MaintainMembership", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) MarkAllAsRead(userID string, threadIds []string) error {
start := time.Now()
err := s.ThreadStore.MarkAllAsRead(userID, threadIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.MarkAllAsRead", success, elapsed)
}
return err
}
func (s *TimerLayerThreadStore) MarkAllAsReadByChannels(userID string, channelIDs []string) error {
start := time.Now()
err := s.ThreadStore.MarkAllAsReadByChannels(userID, channelIDs)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.MarkAllAsReadByChannels", success, elapsed)
}
return err
}
func (s *TimerLayerThreadStore) MarkAllAsReadByTeam(userID string, teamID string) error {
start := time.Now()
err := s.ThreadStore.MarkAllAsReadByTeam(userID, teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.MarkAllAsReadByTeam", success, elapsed)
}
return err
}
func (s *TimerLayerThreadStore) MarkAsRead(userID string, threadID string, timestamp int64) error {
start := time.Now()
err := s.ThreadStore.MarkAsRead(userID, threadID, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.MarkAsRead", success, elapsed)
}
return err
}
func (s *TimerLayerThreadStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
start := time.Now()
result, resultVar1, err := s.ThreadStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.PermanentDeleteBatchForRetentionPolicies", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
start := time.Now()
result, resultVar1, err := s.ThreadStore.PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit, cursor)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.PermanentDeleteBatchThreadMembershipsForRetentionPolicies", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerThreadStore) UpdateMembership(membership *model.ThreadMembership) (*model.ThreadMembership, error) {
start := time.Now()
result, err := s.ThreadStore.UpdateMembership(membership)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.UpdateMembership", success, elapsed)
}
return result, err
}
func (s *TimerLayerTokenStore) Cleanup(expiryTime int64) {
start := time.Now()
s.TokenStore.Cleanup(expiryTime)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TokenStore.Cleanup", success, elapsed)
}
}
func (s *TimerLayerTokenStore) Delete(token string) error {
start := time.Now()
err := s.TokenStore.Delete(token)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TokenStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerTokenStore) GetAllTokensByType(tokenType string) ([]*model.Token, error) {
start := time.Now()
result, err := s.TokenStore.GetAllTokensByType(tokenType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TokenStore.GetAllTokensByType", success, elapsed)
}
return result, err
}
func (s *TimerLayerTokenStore) GetByToken(token string) (*model.Token, error) {
start := time.Now()
result, err := s.TokenStore.GetByToken(token)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TokenStore.GetByToken", success, elapsed)
}
return result, err
}
func (s *TimerLayerTokenStore) RemoveAllTokensByType(tokenType string) error {
start := time.Now()
err := s.TokenStore.RemoveAllTokensByType(tokenType)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TokenStore.RemoveAllTokensByType", success, elapsed)
}
return err
}
func (s *TimerLayerTokenStore) Save(recovery *model.Token) error {
start := time.Now()
err := s.TokenStore.Save(recovery)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TokenStore.Save", success, elapsed)
}
return err
}
func (s *TimerLayerTrueUpReviewStore) CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
start := time.Now()
result, err := s.TrueUpReviewStore.CreateTrueUpReviewStatusRecord(reviewStatus)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TrueUpReviewStore.CreateTrueUpReviewStatusRecord", success, elapsed)
}
return result, err
}
func (s *TimerLayerTrueUpReviewStore) GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error) {
start := time.Now()
result, err := s.TrueUpReviewStore.GetTrueUpReviewStatus(dueDate)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TrueUpReviewStore.GetTrueUpReviewStatus", success, elapsed)
}
return result, err
}
func (s *TimerLayerTrueUpReviewStore) Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
start := time.Now()
result, err := s.TrueUpReviewStore.Update(reviewStatus)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TrueUpReviewStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerUploadSessionStore) Delete(id string) error {
start := time.Now()
err := s.UploadSessionStore.Delete(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UploadSessionStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerUploadSessionStore) Get(ctx context.Context, id string) (*model.UploadSession, error) {
start := time.Now()
result, err := s.UploadSessionStore.Get(ctx, id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UploadSessionStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerUploadSessionStore) GetForUser(userID string) ([]*model.UploadSession, error) {
start := time.Now()
result, err := s.UploadSessionStore.GetForUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UploadSessionStore.GetForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerUploadSessionStore) Save(session *model.UploadSession) (*model.UploadSession, error) {
start := time.Now()
result, err := s.UploadSessionStore.Save(session)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UploadSessionStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerUploadSessionStore) Update(session *model.UploadSession) error {
start := time.Now()
err := s.UploadSessionStore.Update(session)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UploadSessionStore.Update", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) AnalyticsActiveCount(timestamp int64, options model.UserCountOptions) (int64, error) {
start := time.Now()
result, err := s.UserStore.AnalyticsActiveCount(timestamp, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.AnalyticsActiveCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) AnalyticsActiveCountForPeriod(startTime int64, endTime int64, options model.UserCountOptions) (int64, error) {
start := time.Now()
result, err := s.UserStore.AnalyticsActiveCountForPeriod(startTime, endTime, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.AnalyticsActiveCountForPeriod", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) AnalyticsGetExternalUsers(hostDomain string) (bool, error) {
start := time.Now()
result, err := s.UserStore.AnalyticsGetExternalUsers(hostDomain)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.AnalyticsGetExternalUsers", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) AnalyticsGetGuestCount() (int64, error) {
start := time.Now()
result, err := s.UserStore.AnalyticsGetGuestCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.AnalyticsGetGuestCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) AnalyticsGetInactiveUsersCount() (int64, error) {
start := time.Now()
result, err := s.UserStore.AnalyticsGetInactiveUsersCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.AnalyticsGetInactiveUsersCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) AnalyticsGetSystemAdminCount() (int64, error) {
start := time.Now()
result, err := s.UserStore.AnalyticsGetSystemAdminCount()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.AnalyticsGetSystemAdminCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) AutocompleteUsersInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error) {
start := time.Now()
result, err := s.UserStore.AutocompleteUsersInChannel(teamID, channelID, term, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.AutocompleteUsersInChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) ClearAllCustomRoleAssignments() error {
start := time.Now()
err := s.UserStore.ClearAllCustomRoleAssignments()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.ClearAllCustomRoleAssignments", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) ClearCaches() {
start := time.Now()
s.UserStore.ClearCaches()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.ClearCaches", success, elapsed)
}
}
func (s *TimerLayerUserStore) Count(options model.UserCountOptions) (int64, error) {
start := time.Now()
result, err := s.UserStore.Count(options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.Count", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) DeactivateGuests() ([]string, error) {
start := time.Now()
result, err := s.UserStore.DeactivateGuests()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.DeactivateGuests", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) DemoteUserToGuest(userID string) (*model.User, error) {
start := time.Now()
result, err := s.UserStore.DemoteUserToGuest(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.DemoteUserToGuest", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) Get(ctx context.Context, id string) (*model.User, error) {
start := time.Now()
result, err := s.UserStore.Get(ctx, id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetAll() ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetAll()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetAllAfter(limit int, afterID string) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetAllAfter(limit, afterID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetAllAfter", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetAllNotInAuthService(authServices []string) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetAllNotInAuthService(authServices)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetAllNotInAuthService", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetAllProfiles(options *model.UserGetOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetAllProfiles(options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetAllProfiles", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetAllProfilesInChannel(ctx context.Context, channelID string, allowFromCache bool) (map[string]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetAllProfilesInChannel(ctx, channelID, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetAllProfilesInChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetAllUsingAuthService(authService string) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetAllUsingAuthService(authService)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetAllUsingAuthService", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetAnyUnreadPostCountForChannel(userID string, channelID string) (int64, error) {
start := time.Now()
result, err := s.UserStore.GetAnyUnreadPostCountForChannel(userID, channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetAnyUnreadPostCountForChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetByAuth(authData *string, authService string) (*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetByAuth(authData, authService)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetByAuth", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetByEmail(email string) (*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetByEmail(email)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetByEmail", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetByUsername(username string) (*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetByUsername(username)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetByUsername", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetChannelGroupUsers(channelID string) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetChannelGroupUsers(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetChannelGroupUsers", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetEtagForAllProfiles() string {
start := time.Now()
result := s.UserStore.GetEtagForAllProfiles()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetEtagForAllProfiles", success, elapsed)
}
return result
}
func (s *TimerLayerUserStore) GetEtagForProfiles(teamID string) string {
start := time.Now()
result := s.UserStore.GetEtagForProfiles(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetEtagForProfiles", success, elapsed)
}
return result
}
func (s *TimerLayerUserStore) GetEtagForProfilesNotInTeam(teamID string) string {
start := time.Now()
result := s.UserStore.GetEtagForProfilesNotInTeam(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetEtagForProfilesNotInTeam", success, elapsed)
}
return result
}
func (s *TimerLayerUserStore) GetFirstSystemAdminID() (string, error) {
start := time.Now()
result, err := s.UserStore.GetFirstSystemAdminID()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetFirstSystemAdminID", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetForLogin(loginID string, allowSignInWithUsername bool, allowSignInWithEmail bool) (*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetForLogin(loginID, allowSignInWithUsername, allowSignInWithEmail)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetForLogin", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetKnownUsers(userID string) ([]string, error) {
start := time.Now()
result, err := s.UserStore.GetKnownUsers(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetKnownUsers", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetMany(ctx context.Context, ids []string) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetMany(ctx, ids)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetMany", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetNewUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetNewUsersForTeam(teamID, offset, limit, viewRestrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetNewUsersForTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetProfileByGroupChannelIdsForUser(userID string, channelIds []string) (map[string][]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetProfileByGroupChannelIdsForUser(userID, channelIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetProfileByGroupChannelIdsForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetProfileByIds(ctx context.Context, userIds []string, options *store.UserGetByIdsOpts, allowFromCache bool) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetProfileByIds(ctx, userIds, options, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetProfileByIds", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetProfiles(options *model.UserGetOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetProfiles(options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetProfiles", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetProfilesByUsernames(usernames []string, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetProfilesByUsernames(usernames, viewRestrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetProfilesByUsernames", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetProfilesInChannel(options *model.UserGetOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetProfilesInChannel(options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetProfilesInChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetProfilesInChannelByAdmin(options *model.UserGetOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetProfilesInChannelByAdmin(options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetProfilesInChannelByAdmin", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetProfilesInChannelByStatus(options *model.UserGetOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetProfilesInChannelByStatus(options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetProfilesInChannelByStatus", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetProfilesNotInChannel(teamID string, channelId string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetProfilesNotInChannel(teamID, channelId, groupConstrained, offset, limit, viewRestrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetProfilesNotInChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetProfilesNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetProfilesNotInTeam(teamID, groupConstrained, offset, limit, viewRestrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetProfilesNotInTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetProfilesWithoutTeam(options *model.UserGetOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetProfilesWithoutTeam(options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetProfilesWithoutTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetRecentlyActiveUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetRecentlyActiveUsersForTeam(teamID, offset, limit, viewRestrictions)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetRecentlyActiveUsersForTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetSystemAdminProfiles() (map[string]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetSystemAdminProfiles()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetSystemAdminProfiles", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetTeamGroupUsers(teamID string) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetTeamGroupUsers(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetTeamGroupUsers", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetUnreadCount(userID string, isCRTEnabled bool) (int64, error) {
start := time.Now()
result, err := s.UserStore.GetUnreadCount(userID, isCRTEnabled)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetUnreadCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetUnreadCountForChannel(userID string, channelID string) (int64, error) {
start := time.Now()
result, err := s.UserStore.GetUnreadCountForChannel(userID, channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetUnreadCountForChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
start := time.Now()
result, err := s.UserStore.GetUsersBatchForIndexing(startTime, startFileID, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetUsersBatchForIndexing", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetUsersWithInvalidEmails(page int, perPage int, restrictedDomains string) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.GetUsersWithInvalidEmails(page, perPage, restrictedDomains)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetUsersWithInvalidEmails", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) InferSystemInstallDate() (int64, error) {
start := time.Now()
result, err := s.UserStore.InferSystemInstallDate()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.InferSystemInstallDate", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) InsertUsers(users []*model.User) error {
start := time.Now()
err := s.UserStore.InsertUsers(users)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.InsertUsers", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) InvalidateProfileCacheForUser(userID string) {
start := time.Now()
s.UserStore.InvalidateProfileCacheForUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.InvalidateProfileCacheForUser", success, elapsed)
}
}
func (s *TimerLayerUserStore) InvalidateProfilesInChannelCache(channelID string) {
start := time.Now()
s.UserStore.InvalidateProfilesInChannelCache(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.InvalidateProfilesInChannelCache", success, elapsed)
}
}
func (s *TimerLayerUserStore) InvalidateProfilesInChannelCacheByUser(userID string) {
start := time.Now()
s.UserStore.InvalidateProfilesInChannelCacheByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.InvalidateProfilesInChannelCacheByUser", success, elapsed)
}
}
func (s *TimerLayerUserStore) IsEmpty(excludeBots bool) (bool, error) {
start := time.Now()
result, err := s.UserStore.IsEmpty(excludeBots)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.IsEmpty", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) PermanentDelete(userID string) error {
start := time.Now()
err := s.UserStore.PermanentDelete(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.PermanentDelete", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) PromoteGuestToUser(userID string) error {
start := time.Now()
err := s.UserStore.PromoteGuestToUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.PromoteGuestToUser", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) ResetAuthDataToEmailForUsers(service string, userIDs []string, includeDeleted bool, dryRun bool) (int, error) {
start := time.Now()
result, err := s.UserStore.ResetAuthDataToEmailForUsers(service, userIDs, includeDeleted, dryRun)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.ResetAuthDataToEmailForUsers", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) ResetLastPictureUpdate(userID string) error {
start := time.Now()
err := s.UserStore.ResetLastPictureUpdate(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.ResetLastPictureUpdate", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) Save(user *model.User) (*model.User, error) {
start := time.Now()
result, err := s.UserStore.Save(user)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) Search(teamID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.Search(teamID, term, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.Search", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) SearchInChannel(channelID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.SearchInChannel(channelID, term, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.SearchInChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) SearchInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.SearchInGroup(groupID, term, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.SearchInGroup", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) SearchNotInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.SearchNotInChannel(teamID, channelID, term, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.SearchNotInChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) SearchNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.SearchNotInGroup(groupID, term, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.SearchNotInGroup", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) SearchNotInTeam(notInTeamID string, term string, options *model.UserSearchOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.SearchNotInTeam(notInTeamID, term, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.SearchNotInTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) SearchWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, error) {
start := time.Now()
result, err := s.UserStore.SearchWithoutTeam(term, options)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.SearchWithoutTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) Update(user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error) {
start := time.Now()
result, err := s.UserStore.Update(user, allowRoleUpdate)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) UpdateAuthData(userID string, service string, authData *string, email string, resetMfa bool) (string, error) {
start := time.Now()
result, err := s.UserStore.UpdateAuthData(userID, service, authData, email, resetMfa)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.UpdateAuthData", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) UpdateFailedPasswordAttempts(userID string, attempts int) error {
start := time.Now()
err := s.UserStore.UpdateFailedPasswordAttempts(userID, attempts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.UpdateFailedPasswordAttempts", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) UpdateLastPictureUpdate(userID string) error {
start := time.Now()
err := s.UserStore.UpdateLastPictureUpdate(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.UpdateLastPictureUpdate", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) UpdateMfaActive(userID string, active bool) error {
start := time.Now()
err := s.UserStore.UpdateMfaActive(userID, active)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.UpdateMfaActive", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) UpdateMfaSecret(userID string, secret string) error {
start := time.Now()
err := s.UserStore.UpdateMfaSecret(userID, secret)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.UpdateMfaSecret", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) UpdateNotifyProps(userID string, props map[string]string) error {
start := time.Now()
err := s.UserStore.UpdateNotifyProps(userID, props)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.UpdateNotifyProps", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) UpdatePassword(userID string, newPassword string) error {
start := time.Now()
err := s.UserStore.UpdatePassword(userID, newPassword)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.UpdatePassword", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) UpdateUpdateAt(userID string) (int64, error) {
start := time.Now()
result, err := s.UserStore.UpdateUpdateAt(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.UpdateUpdateAt", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) VerifyEmail(userID string, email string) (string, error) {
start := time.Now()
result, err := s.UserStore.VerifyEmail(userID, email)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.VerifyEmail", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserAccessTokenStore) Delete(tokenID string) error {
start := time.Now()
err := s.UserAccessTokenStore.Delete(tokenID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserAccessTokenStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerUserAccessTokenStore) DeleteAllForUser(userID string) error {
start := time.Now()
err := s.UserAccessTokenStore.DeleteAllForUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserAccessTokenStore.DeleteAllForUser", success, elapsed)
}
return err
}
func (s *TimerLayerUserAccessTokenStore) Get(tokenID string) (*model.UserAccessToken, error) {
start := time.Now()
result, err := s.UserAccessTokenStore.Get(tokenID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserAccessTokenStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserAccessTokenStore) GetAll(offset int, limit int) ([]*model.UserAccessToken, error) {
start := time.Now()
result, err := s.UserAccessTokenStore.GetAll(offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserAccessTokenStore.GetAll", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserAccessTokenStore) GetByToken(tokenString string) (*model.UserAccessToken, error) {
start := time.Now()
result, err := s.UserAccessTokenStore.GetByToken(tokenString)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserAccessTokenStore.GetByToken", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserAccessTokenStore) GetByUser(userID string, page int, perPage int) ([]*model.UserAccessToken, error) {
start := time.Now()
result, err := s.UserAccessTokenStore.GetByUser(userID, page, perPage)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserAccessTokenStore.GetByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserAccessTokenStore) Save(token *model.UserAccessToken) (*model.UserAccessToken, error) {
start := time.Now()
result, err := s.UserAccessTokenStore.Save(token)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserAccessTokenStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserAccessTokenStore) Search(term string) ([]*model.UserAccessToken, error) {
start := time.Now()
result, err := s.UserAccessTokenStore.Search(term)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserAccessTokenStore.Search", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserAccessTokenStore) UpdateTokenDisable(tokenID string) error {
start := time.Now()
err := s.UserAccessTokenStore.UpdateTokenDisable(tokenID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserAccessTokenStore.UpdateTokenDisable", success, elapsed)
}
return err
}
func (s *TimerLayerUserAccessTokenStore) UpdateTokenEnable(tokenID string) error {
start := time.Now()
err := s.UserAccessTokenStore.UpdateTokenEnable(tokenID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserAccessTokenStore.UpdateTokenEnable", success, elapsed)
}
return err
}
func (s *TimerLayerUserTermsOfServiceStore) Delete(userID string, termsOfServiceId string) error {
start := time.Now()
err := s.UserTermsOfServiceStore.Delete(userID, termsOfServiceId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserTermsOfServiceStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerUserTermsOfServiceStore) GetByUser(userID string) (*model.UserTermsOfService, error) {
start := time.Now()
result, err := s.UserTermsOfServiceStore.GetByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserTermsOfServiceStore.GetByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserTermsOfServiceStore) Save(userTermsOfService *model.UserTermsOfService) (*model.UserTermsOfService, error) {
start := time.Now()
result, err := s.UserTermsOfServiceStore.Save(userTermsOfService)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserTermsOfServiceStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) AnalyticsIncomingCount(teamID string) (int64, error) {
start := time.Now()
result, err := s.WebhookStore.AnalyticsIncomingCount(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.AnalyticsIncomingCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) AnalyticsOutgoingCount(teamID string) (int64, error) {
start := time.Now()
result, err := s.WebhookStore.AnalyticsOutgoingCount(teamID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.AnalyticsOutgoingCount", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) ClearCaches() {
start := time.Now()
s.WebhookStore.ClearCaches()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.ClearCaches", success, elapsed)
}
}
func (s *TimerLayerWebhookStore) DeleteIncoming(webhookID string, timestamp int64) error {
start := time.Now()
err := s.WebhookStore.DeleteIncoming(webhookID, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.DeleteIncoming", success, elapsed)
}
return err
}
func (s *TimerLayerWebhookStore) DeleteOutgoing(webhookID string, timestamp int64) error {
start := time.Now()
err := s.WebhookStore.DeleteOutgoing(webhookID, timestamp)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.DeleteOutgoing", success, elapsed)
}
return err
}
func (s *TimerLayerWebhookStore) GetIncoming(id string, allowFromCache bool) (*model.IncomingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetIncoming(id, allowFromCache)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetIncoming", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetIncomingByChannel(channelID string) ([]*model.IncomingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetIncomingByChannel(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetIncomingByChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetIncomingByTeam(teamID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetIncomingByTeam(teamID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetIncomingByTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetIncomingByTeamByUser(teamID string, userID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetIncomingByTeamByUser(teamID, userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetIncomingByTeamByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetIncomingList(offset int, limit int) ([]*model.IncomingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetIncomingList(offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetIncomingList", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetIncomingListByUser(userID string, offset int, limit int) ([]*model.IncomingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetIncomingListByUser(userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetIncomingListByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetOutgoing(id string) (*model.OutgoingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetOutgoing(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetOutgoing", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetOutgoingByChannel(channelID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetOutgoingByChannel(channelID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetOutgoingByChannel", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetOutgoingByChannelByUser(channelID string, userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetOutgoingByChannelByUser(channelID, userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetOutgoingByChannelByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetOutgoingByTeam(teamID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetOutgoingByTeam(teamID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetOutgoingByTeam", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetOutgoingByTeamByUser(teamID string, userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetOutgoingByTeamByUser(teamID, userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetOutgoingByTeamByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetOutgoingList(offset int, limit int) ([]*model.OutgoingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetOutgoingList(offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetOutgoingList", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) GetOutgoingListByUser(userID string, offset int, limit int) ([]*model.OutgoingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.GetOutgoingListByUser(userID, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.GetOutgoingListByUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) InvalidateWebhookCache(webhook string) {
start := time.Now()
s.WebhookStore.InvalidateWebhookCache(webhook)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.InvalidateWebhookCache", success, elapsed)
}
}
func (s *TimerLayerWebhookStore) PermanentDeleteIncomingByChannel(channelID string) error {
start := time.Now()
err := s.WebhookStore.PermanentDeleteIncomingByChannel(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.PermanentDeleteIncomingByChannel", success, elapsed)
}
return err
}
func (s *TimerLayerWebhookStore) PermanentDeleteIncomingByUser(userID string) error {
start := time.Now()
err := s.WebhookStore.PermanentDeleteIncomingByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.PermanentDeleteIncomingByUser", success, elapsed)
}
return err
}
func (s *TimerLayerWebhookStore) PermanentDeleteOutgoingByChannel(channelID string) error {
start := time.Now()
err := s.WebhookStore.PermanentDeleteOutgoingByChannel(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.PermanentDeleteOutgoingByChannel", success, elapsed)
}
return err
}
func (s *TimerLayerWebhookStore) PermanentDeleteOutgoingByUser(userID string) error {
start := time.Now()
err := s.WebhookStore.PermanentDeleteOutgoingByUser(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.PermanentDeleteOutgoingByUser", success, elapsed)
}
return err
}
func (s *TimerLayerWebhookStore) SaveIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.SaveIncoming(webhook)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.SaveIncoming", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) SaveOutgoing(webhook *model.OutgoingWebhook) (*model.OutgoingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.SaveOutgoing(webhook)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.SaveOutgoing", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) UpdateIncoming(webhook *model.IncomingWebhook) (*model.IncomingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.UpdateIncoming(webhook)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.UpdateIncoming", success, elapsed)
}
return result, err
}
func (s *TimerLayerWebhookStore) UpdateOutgoing(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, error) {
start := time.Now()
result, err := s.WebhookStore.UpdateOutgoing(hook)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("WebhookStore.UpdateOutgoing", success, elapsed)
}
return result, err
}
func (s *TimerLayer) Close() {
s.Store.Close()
}
func (s *TimerLayer) DropAllTables() {
s.Store.DropAllTables()
}
func (s *TimerLayer) LockToMaster() {
s.Store.LockToMaster()
}
func (s *TimerLayer) MarkSystemRanUnitTests() {
s.Store.MarkSystemRanUnitTests()
}
func (s *TimerLayer) SetContext(context context.Context) {
s.Store.SetContext(context)
}
func (s *TimerLayer) TotalMasterDbConnections() int {
return s.Store.TotalMasterDbConnections()
}
func (s *TimerLayer) TotalReadDbConnections() int {
return s.Store.TotalReadDbConnections()
}
func (s *TimerLayer) TotalSearchDbConnections() int {
return s.Store.TotalSearchDbConnections()
}
func (s *TimerLayer) UnlockFromMaster() {
s.Store.UnlockFromMaster()
}
func New(childStore store.Store, metrics einterfaces.MetricsInterface) *TimerLayer {
newStore := TimerLayer{
Store: childStore,
Metrics: metrics,
}
newStore.AuditStore = &TimerLayerAuditStore{AuditStore: childStore.Audit(), Root: &newStore}
newStore.BotStore = &TimerLayerBotStore{BotStore: childStore.Bot(), Root: &newStore}
newStore.ChannelStore = &TimerLayerChannelStore{ChannelStore: childStore.Channel(), Root: &newStore}
newStore.ChannelMemberHistoryStore = &TimerLayerChannelMemberHistoryStore{ChannelMemberHistoryStore: childStore.ChannelMemberHistory(), Root: &newStore}
newStore.ClusterDiscoveryStore = &TimerLayerClusterDiscoveryStore{ClusterDiscoveryStore: childStore.ClusterDiscovery(), Root: &newStore}
newStore.CommandStore = &TimerLayerCommandStore{CommandStore: childStore.Command(), Root: &newStore}
newStore.CommandWebhookStore = &TimerLayerCommandWebhookStore{CommandWebhookStore: childStore.CommandWebhook(), Root: &newStore}
newStore.ComplianceStore = &TimerLayerComplianceStore{ComplianceStore: childStore.Compliance(), Root: &newStore}
newStore.DraftStore = &TimerLayerDraftStore{DraftStore: childStore.Draft(), Root: &newStore}
newStore.EmojiStore = &TimerLayerEmojiStore{EmojiStore: childStore.Emoji(), Root: &newStore}
newStore.FileInfoStore = &TimerLayerFileInfoStore{FileInfoStore: childStore.FileInfo(), Root: &newStore}
newStore.GroupStore = &TimerLayerGroupStore{GroupStore: childStore.Group(), Root: &newStore}
newStore.JobStore = &TimerLayerJobStore{JobStore: childStore.Job(), Root: &newStore}
newStore.LicenseStore = &TimerLayerLicenseStore{LicenseStore: childStore.License(), Root: &newStore}
newStore.LinkMetadataStore = &TimerLayerLinkMetadataStore{LinkMetadataStore: childStore.LinkMetadata(), Root: &newStore}
newStore.NotifyAdminStore = &TimerLayerNotifyAdminStore{NotifyAdminStore: childStore.NotifyAdmin(), Root: &newStore}
newStore.OAuthStore = &TimerLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore}
newStore.PluginStore = &TimerLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
newStore.PostStore = &TimerLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
newStore.PostAcknowledgementStore = &TimerLayerPostAcknowledgementStore{PostAcknowledgementStore: childStore.PostAcknowledgement(), Root: &newStore}
newStore.PostPriorityStore = &TimerLayerPostPriorityStore{PostPriorityStore: childStore.PostPriority(), Root: &newStore}
newStore.PreferenceStore = &TimerLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore}
newStore.ProductNoticesStore = &TimerLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore}
newStore.ReactionStore = &TimerLayerReactionStore{ReactionStore: childStore.Reaction(), Root: &newStore}
newStore.RemoteClusterStore = &TimerLayerRemoteClusterStore{RemoteClusterStore: childStore.RemoteCluster(), Root: &newStore}
newStore.RetentionPolicyStore = &TimerLayerRetentionPolicyStore{RetentionPolicyStore: childStore.RetentionPolicy(), Root: &newStore}
newStore.RoleStore = &TimerLayerRoleStore{RoleStore: childStore.Role(), Root: &newStore}
newStore.SchemeStore = &TimerLayerSchemeStore{SchemeStore: childStore.Scheme(), Root: &newStore}
newStore.SessionStore = &TimerLayerSessionStore{SessionStore: childStore.Session(), Root: &newStore}
newStore.SharedChannelStore = &TimerLayerSharedChannelStore{SharedChannelStore: childStore.SharedChannel(), Root: &newStore}
newStore.StatusStore = &TimerLayerStatusStore{StatusStore: childStore.Status(), Root: &newStore}
newStore.SystemStore = &TimerLayerSystemStore{SystemStore: childStore.System(), Root: &newStore}
newStore.TeamStore = &TimerLayerTeamStore{TeamStore: childStore.Team(), Root: &newStore}
newStore.TermsOfServiceStore = &TimerLayerTermsOfServiceStore{TermsOfServiceStore: childStore.TermsOfService(), Root: &newStore}
newStore.ThreadStore = &TimerLayerThreadStore{ThreadStore: childStore.Thread(), Root: &newStore}
newStore.TokenStore = &TimerLayerTokenStore{TokenStore: childStore.Token(), Root: &newStore}
newStore.TrueUpReviewStore = &TimerLayerTrueUpReviewStore{TrueUpReviewStore: childStore.TrueUpReview(), Root: &newStore}
newStore.UploadSessionStore = &TimerLayerUploadSessionStore{UploadSessionStore: childStore.UploadSession(), Root: &newStore}
newStore.UserStore = &TimerLayerUserStore{UserStore: childStore.User(), Root: &newStore}
newStore.UserAccessTokenStore = &TimerLayerUserAccessTokenStore{UserAccessTokenStore: childStore.UserAccessToken(), Root: &newStore}
newStore.UserTermsOfServiceStore = &TimerLayerUserTermsOfServiceStore{UserTermsOfServiceStore: childStore.UserTermsOfService(), Root: &newStore}
newStore.WebhookStore = &TimerLayerWebhookStore{WebhookStore: childStore.Webhook(), Root: &newStore}
return &newStore
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package testlib
import (
"encoding/json"
"io"
"testing"
)
// AssertLog asserts that a JSON-encoded buffer of logs contains one with the given level and message.
func AssertLog(t *testing.T, logs io.Reader, level, message string) {
dec := json.NewDecoder(logs)
for {
var log struct {
Level string
Msg string
}
if err := dec.Decode(&log); err == io.EOF {
break
} else if err != nil {
t.Logf("Error decoding log entry: %s", err)
continue
}
if log.Level == level && log.Msg == message {
return
}
}
t.Fatalf("failed to find %s log message: %s", level, message)
}
// AssertNoLog asserts that a JSON-encoded buffer of logs does not contains one with the given level and message.
func AssertNoLog(t *testing.T, logs io.Reader, level, message string) {
dec := json.NewDecoder(logs)
for {
var log struct {
Level string
Msg string
}
if err := dec.Decode(&log); err == io.EOF {
break
} else if err != nil {
t.Logf("Error decoding log entry: %s", err)
continue
}
if log.Level == level && log.Msg == message {
t.Fatalf("found %s log message: %s", level, message)
return
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package testlib
import (
"sync"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
)
type FakeClusterInterface struct {
clusterMessageHandler einterfaces.ClusterMessageHandler
mut sync.RWMutex
messages []*model.ClusterMessage
}
func (c *FakeClusterInterface) StartInterNodeCommunication() {}
func (c *FakeClusterInterface) StopInterNodeCommunication() {}
func (c *FakeClusterInterface) RegisterClusterMessageHandler(event model.ClusterEvent, crm einterfaces.ClusterMessageHandler) {
c.clusterMessageHandler = crm
}
func (c *FakeClusterInterface) HealthScore() int {
return 0
}
func (c *FakeClusterInterface) GetClusterId() string { return "" }
func (c *FakeClusterInterface) IsLeader() bool { return false }
func (c *FakeClusterInterface) GetMyClusterInfo() *model.ClusterInfo { return nil }
func (c *FakeClusterInterface) GetClusterInfos() []*model.ClusterInfo { return nil }
func (c *FakeClusterInterface) SendClusterMessage(message *model.ClusterMessage) {
c.mut.Lock()
defer c.mut.Unlock()
c.messages = append(c.messages, message)
}
func (c *FakeClusterInterface) SendClusterMessageToNode(nodeID string, message *model.ClusterMessage) error {
c.mut.Lock()
defer c.mut.Unlock()
c.messages = append(c.messages, message)
return nil
}
func (c *FakeClusterInterface) NotifyMsg(buf []byte) {}
func (c *FakeClusterInterface) GetClusterStats() ([]*model.ClusterStats, *model.AppError) {
return nil, nil
}
func (c *FakeClusterInterface) GetLogs(page, perPage int) ([]string, *model.AppError) {
return []string{}, nil
}
func (c *FakeClusterInterface) QueryLogs(page, perPage int) (map[string][]string, *model.AppError) {
return make(map[string][]string), nil
}
func (c *FakeClusterInterface) ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError {
return nil
}
func (c *FakeClusterInterface) SendClearRoleCacheMessage() {
if c.clusterMessageHandler != nil {
c.clusterMessageHandler(&model.ClusterMessage{
Event: model.ClusterEventInvalidateCacheForRoles,
})
}
}
func (c *FakeClusterInterface) GetPluginStatuses() (model.PluginStatuses, *model.AppError) {
return nil, nil
}
func (c *FakeClusterInterface) GetMessages() []*model.ClusterMessage {
c.mut.RLock()
defer c.mut.RUnlock()
return c.messages
}
func (c *FakeClusterInterface) SelectMessages(filterCond func(message *model.ClusterMessage) bool) []*model.ClusterMessage {
c.mut.RLock()
defer c.mut.RUnlock()
filteredMessages := []*model.ClusterMessage{}
for _, msg := range c.messages {
if filterCond(msg) {
filteredMessages = append(filteredMessages, msg)
}
}
return filteredMessages
}
func (c *FakeClusterInterface) ClearMessages() {
c.mut.Lock()
defer c.mut.Unlock()
c.messages = nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package testlib
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"testing"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/searchlayer"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
)
type MainHelper struct {
Settings *model.SqlSettings
Store store.Store
SearchEngine *searchengine.Broker
SQLStore *sqlstore.SqlStore
ClusterInterface *FakeClusterInterface
status int
testResourcePath string
replicas []string
}
type HelperOptions struct {
EnableStore bool
EnableResources bool
WithReadReplica bool
}
func NewMainHelper() *MainHelper {
return NewMainHelperWithOptions(&HelperOptions{
EnableStore: true,
EnableResources: true,
})
}
func NewMainHelperWithOptions(options *HelperOptions) *MainHelper {
var mainHelper MainHelper
flag.Parse()
utils.TranslationsPreInit()
if options != nil {
if options.EnableStore && !testing.Short() {
mainHelper.setupStore(options.WithReadReplica)
}
if options.EnableResources {
mainHelper.setupResources()
}
}
return &mainHelper
}
func (h *MainHelper) Main(m *testing.M) {
if h.testResourcePath != "" {
prevDir, err := os.Getwd()
if err != nil {
panic("Failed to get current working directory: " + err.Error())
}
err = os.Chdir(h.testResourcePath)
if err != nil {
panic(fmt.Sprintf("Failed to set current working directory to %s: %s", h.testResourcePath, err.Error()))
}
defer func() {
err := os.Chdir(prevDir)
if err != nil {
panic(fmt.Sprintf("Failed to restore current working directory to %s: %s", prevDir, err.Error()))
}
}()
}
h.status = m.Run()
}
func (h *MainHelper) setupStore(withReadReplica bool) {
driverName := os.Getenv("MM_SQLSETTINGS_DRIVERNAME")
if driverName == "" {
driverName = model.DatabaseDriverPostgres
}
h.Settings = storetest.MakeSqlSettings(driverName, withReadReplica)
h.replicas = h.Settings.DataSourceReplicas
config := &model.Config{}
config.SetDefaults()
h.SearchEngine = searchengine.NewBroker(config)
h.ClusterInterface = &FakeClusterInterface{}
h.SQLStore = sqlstore.New(*h.Settings, nil)
h.Store = searchlayer.NewSearchLayer(&TestStore{
h.SQLStore,
}, h.SearchEngine, config)
}
func (h *MainHelper) ToggleReplicasOff() {
if h.SQLStore.GetLicense() == nil {
panic("expecting a license to use this")
}
h.Settings.DataSourceReplicas = []string{}
lic := h.SQLStore.GetLicense()
h.SQLStore = sqlstore.New(*h.Settings, nil)
h.SQLStore.UpdateLicense(lic)
}
func (h *MainHelper) ToggleReplicasOn() {
if h.SQLStore.GetLicense() == nil {
panic("expecting a license to use this")
}
h.Settings.DataSourceReplicas = h.replicas
lic := h.SQLStore.GetLicense()
h.SQLStore = sqlstore.New(*h.Settings, nil)
h.SQLStore.UpdateLicense(lic)
}
func (h *MainHelper) setupResources() {
var err error
h.testResourcePath, err = SetupTestResources()
if err != nil {
panic("failed to setup test resources: " + err.Error())
}
}
// PreloadMigrations preloads the migrations and roles into the database
// so that they are not run again when the migrations happen every time
// the server is started.
// This change is forward-compatible with new migrations and only new migrations
// will get executed.
// Only if the schema of either roles or systems table changes, this will break.
// In that case, just update the migrations or comment this out for the time being.
// In the worst case, only an optimization is lost.
//
// Re-generate the files with:
// pg_dump -a -h localhost -U mmuser -d <> --no-comments --inserts -t roles -t systems
// mysqldump -u root -p <> --no-create-info --extended-insert=FALSE Systems Roles
// And keep only the permission related rows in the systems table output.
func (h *MainHelper) PreloadMigrations() {
var buf []byte
var err error
basePath := os.Getenv("MM_SERVER_PATH")
if basePath == "" {
basePath = "mattermost-server/server"
}
relPath := "channels/testlib/testdata"
switch *h.Settings.DriverName {
case model.DatabaseDriverPostgres:
finalPath := filepath.Join(basePath, relPath, "postgres_migration_warmup.sql")
buf, err = os.ReadFile(finalPath)
if err != nil {
panic(fmt.Errorf("cannot read file: %v", err))
}
case model.DatabaseDriverMysql:
finalPath := filepath.Join(basePath, relPath, "mysql_migration_warmup.sql")
buf, err = os.ReadFile(finalPath)
if err != nil {
panic(fmt.Errorf("cannot read file: %v", err))
}
}
handle := h.SQLStore.GetMasterX()
_, err = handle.Exec(string(buf))
if err != nil {
panic(errors.Wrap(err, "Error preloading migrations. Check if you have &multiStatements=true in your DSN if you are using MySQL. Or perhaps the schema changed? If yes, then update the warmup files accordingly"))
}
}
func (h *MainHelper) Close() error {
if h.SQLStore != nil {
h.SQLStore.Close()
}
if h.Settings != nil {
storetest.CleanupSqlSettings(h.Settings)
}
if h.testResourcePath != "" {
os.RemoveAll(h.testResourcePath)
}
if r := recover(); r != nil {
log.Fatalln(r)
}
os.Exit(h.status)
return nil
}
func (h *MainHelper) GetSQLSettings() *model.SqlSettings {
if h.Settings == nil {
panic("MainHelper not initialized with database access.")
}
return h.Settings
}
func (h *MainHelper) GetStore() store.Store {
if h.Store == nil {
panic("MainHelper not initialized with store.")
}
return h.Store
}
func (h *MainHelper) GetSQLStore() *sqlstore.SqlStore {
if h.SQLStore == nil {
panic("MainHelper not initialized with sql store.")
}
return h.SQLStore
}
func (h *MainHelper) GetClusterInterface() *FakeClusterInterface {
if h.ClusterInterface == nil {
panic("MainHelper not initialized with cluster interface.")
}
return h.ClusterInterface
}
func (h *MainHelper) GetSearchEngine() *searchengine.Broker {
if h.SearchEngine == nil {
panic("MainHelper not initialized with search engine")
}
return h.SearchEngine
}
func (h *MainHelper) SetReplicationLagForTesting(seconds int) error {
if dn := h.SQLStore.DriverName(); dn != model.DatabaseDriverMysql {
return fmt.Errorf("method not implemented for %q database driver, only %q is supported", dn, model.DatabaseDriverMysql)
}
err := h.execOnEachReplica("STOP SLAVE SQL_THREAD FOR CHANNEL ''")
if err != nil {
return err
}
err = h.execOnEachReplica(fmt.Sprintf("CHANGE MASTER TO MASTER_DELAY = %d", seconds))
if err != nil {
return err
}
err = h.execOnEachReplica("START SLAVE SQL_THREAD FOR CHANNEL ''")
if err != nil {
return err
}
return nil
}
func (h *MainHelper) execOnEachReplica(query string, args ...any) error {
for _, replica := range h.SQLStore.ReplicaXs {
_, err := replica.Exec(query, args...)
if err != nil {
return err
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package testlib
import (
"encoding/json"
"fmt"
"os"
"path"
"path/filepath"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
)
const (
resourceTypeFile = iota
resourceTypeFolder
)
const (
actionCopy = iota
actionSymlink
)
const root = "___mattermost-server"
type testResourceDetails struct {
src string
dest string
resType int8
action int8
}
func findFile(path string) string {
return fileutils.FindPath(path, fileutils.CommonBaseSearchPaths(), func(fileInfo os.FileInfo) bool {
return !fileInfo.IsDir()
})
}
func findDir(dir string) (string, bool) {
if dir == root {
srcPath := findFile("go.mod")
if srcPath == "" {
return "./", false
}
return path.Dir(srcPath), true
}
found := fileutils.FindPath(dir, fileutils.CommonBaseSearchPaths(), func(fileInfo os.FileInfo) bool {
return fileInfo.IsDir()
})
if found == "" {
return "./", false
}
return found, true
}
func getTestResourcesToSetup() []testResourceDetails {
var srcPath string
var found bool
var testResourcesToSetup = []testResourceDetails{
{root, "mattermost-server", resourceTypeFolder, actionSymlink},
{"go.mod", "go.mod", resourceTypeFile, actionSymlink},
{"i18n", "i18n", resourceTypeFolder, actionSymlink},
{"templates", "templates", resourceTypeFolder, actionSymlink},
{"tests", "tests", resourceTypeFolder, actionSymlink},
{"fonts", "fonts", resourceTypeFolder, actionSymlink},
{"channels/utils/policies-roles-mapping.json", "channels/utils/policies-roles-mapping.json", resourceTypeFile, actionSymlink},
}
// Finding resources and setting full path to source to be used for further processing
for i, testResource := range testResourcesToSetup {
if testResource.resType == resourceTypeFile {
srcPath = findFile(testResource.src)
if srcPath == "" {
panic(fmt.Sprintf("Failed to find file %s", testResource.src))
}
testResourcesToSetup[i].src = srcPath
} else if testResource.resType == resourceTypeFolder {
srcPath, found = findDir(testResource.src)
if !found {
panic(fmt.Sprintf("Failed to find folder %s", testResource.src))
}
testResourcesToSetup[i].src = srcPath
} else {
panic(fmt.Sprintf("Invalid resource type: %d", testResource.resType))
}
}
return testResourcesToSetup
}
func CopyFile(src, dst string) error {
fileBackend, err := filestore.NewFileBackend(filestore.FileBackendSettings{DriverName: "local", Directory: ""})
if err != nil {
return errors.Wrapf(err, "failed to copy file %s to %s", src, dst)
}
if err = fileBackend.CopyFile(src, dst); err != nil {
return errors.Wrapf(err, "failed to copy file %s to %s", src, dst)
}
return nil
}
func SetupTestResources() (string, error) {
testResourcesToSetup := getTestResourcesToSetup()
tempDir, err := os.MkdirTemp("", "testlib")
if err != nil {
return "", errors.Wrap(err, "failed to create temporary directory")
}
pluginsDir := path.Join(tempDir, "plugins")
err = os.Mkdir(pluginsDir, 0700)
if err != nil {
return "", errors.Wrapf(err, "failed to create plugins directory %s", pluginsDir)
}
clientDir := path.Join(tempDir, "client")
err = os.Mkdir(clientDir, 0700)
if err != nil {
return "", errors.Wrapf(err, "failed to create client directory %s", clientDir)
}
err = setupConfig(path.Join(tempDir, "config"))
if err != nil {
return "", errors.Wrap(err, "failed to setup config")
}
var resourceDestInTemp string
// Setting up test resources in temp.
// Action in each resource tells whether it needs to be copied or just symlinked
for _, testResource := range testResourcesToSetup {
resourceDestInTemp = filepath.Join(tempDir, testResource.dest)
if testResource.action == actionCopy {
if testResource.resType == resourceTypeFile {
if err = CopyFile(testResource.src, resourceDestInTemp); err != nil {
return "", err
}
} else if testResource.resType == resourceTypeFolder {
err = utils.CopyDir(testResource.src, resourceDestInTemp)
if err != nil {
return "", errors.Wrapf(err, "failed to copy folder %s to %s", testResource.src, resourceDestInTemp)
}
}
} else if testResource.action == actionSymlink {
destDir := path.Dir(resourceDestInTemp)
if destDir != "." {
err = os.MkdirAll(destDir, os.ModePerm)
if err != nil {
return "", errors.Wrapf(err, "failed to make dir %s", destDir)
}
}
err = os.Symlink(testResource.src, resourceDestInTemp)
if err != nil {
return "", errors.Wrapf(err, "failed to symlink %s to %s", testResource.src, resourceDestInTemp)
}
} else {
return "", errors.Wrapf(err, "Invalid action: %d", testResource.action)
}
}
return tempDir, nil
}
func setupConfig(configDir string) error {
var err error
var config model.Config
config.SetDefaults()
err = os.Mkdir(configDir, 0700)
if err != nil {
return errors.Wrapf(err, "failed to create config directory %s", configDir)
}
buf, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("failed to marshal config: %v", err)
}
configJSON := path.Join(configDir, "config.json")
err = os.WriteFile(configJSON, buf, 0644)
if err != nil {
return errors.Wrapf(err, "failed to write config to %s", configJSON)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package testlib
import (
"net/http"
"strconv"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
)
type TestStore struct {
store.Store
}
func (s *TestStore) Close() {
// Don't propagate to the underlying store, since this instance is persistent.
}
func GetMockStoreForSetupFunctions() *mocks.Store {
mockStore := mocks.Store{}
systemStore := mocks.SystemStore{}
systemStore.On("GetByName", "FirstAdminSetupComplete").Return(&model.System{Name: "FirstAdminSetupComplete", Value: "true"}, nil)
systemStore.On("GetByName", "RemainingSchemaMigrations").Return(&model.System{Name: "RemainingSchemaMigrations", Value: "true"}, nil)
systemStore.On("GetByName", "ContentExtractionConfigDefaultTrueMigrationComplete").Return(&model.System{Name: "ContentExtractionConfigDefaultTrueMigrationComplete", Value: "true"}, nil)
systemStore.On("GetByName", "UpgradedFromTE").Return(nil, model.NewAppError("FakeError", "app.system.get_by_name.app_error", nil, "", http.StatusInternalServerError))
systemStore.On("GetByName", "ContentExtractionConfigMigrationComplete").Return(&model.System{Name: "ContentExtractionConfigMigrationComplete", Value: "true"}, nil)
systemStore.On("GetByName", "AsymmetricSigningKey").Return(nil, model.NewAppError("FakeError", "app.system.get_by_name.app_error", nil, "", http.StatusInternalServerError))
systemStore.On("GetByName", "PostActionCookieSecret").Return(nil, model.NewAppError("FakeError", "app.system.get_by_name.app_error", nil, "", http.StatusInternalServerError))
systemStore.On("GetByName", "InstallationDate").Return(&model.System{Name: "InstallationDate", Value: strconv.FormatInt(model.GetMillis(), 10)}, nil)
systemStore.On("GetByName", "FirstServerRunTimestamp").Return(&model.System{Name: "FirstServerRunTimestamp", Value: "10"}, nil)
systemStore.On("GetByName", "AdvancedPermissionsMigrationComplete").Return(&model.System{Name: "AdvancedPermissionsMigrationComplete", Value: "true"}, nil)
systemStore.On("GetByName", "EmojisPermissionsMigrationComplete").Return(&model.System{Name: "EmojisPermissionsMigrationComplete", Value: "true"}, nil)
systemStore.On("GetByName", "GuestRolesCreationMigrationComplete").Return(&model.System{Name: "GuestRolesCreationMigrationComplete", Value: "true"}, nil)
systemStore.On("GetByName", "SystemConsoleRolesCreationMigrationComplete").Return(&model.System{Name: "SystemConsoleRolesCreationMigrationComplete", Value: "true"}, nil)
systemStore.On("GetByName", "PlaybookRolesCreationMigrationComplete").Return(&model.System{Name: "PlaybookRolesCreationMigrationComplete", Value: "true"}, nil)
systemStore.On("GetByName", "PostPriorityConfigDefaultTrueMigrationComplete").Return(&model.System{Name: "PostPriorityConfigDefaultTrueMigrationComplete", Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyEmojiPermissionsSplit).Return(&model.System{Name: model.MigrationKeyEmojiPermissionsSplit, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyWebhookPermissionsSplit).Return(&model.System{Name: model.MigrationKeyWebhookPermissionsSplit, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyListJoinPublicPrivateTeams).Return(&model.System{Name: model.MigrationKeyListJoinPublicPrivateTeams, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyRemovePermanentDeleteUser).Return(&model.System{Name: model.MigrationKeyRemovePermanentDeleteUser, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddBotPermissions).Return(&model.System{Name: model.MigrationKeyAddBotPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyApplyChannelManageDeleteToChannelUser).Return(&model.System{Name: model.MigrationKeyApplyChannelManageDeleteToChannelUser, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyRemoveChannelManageDeleteFromTeamUser).Return(&model.System{Name: model.MigrationKeyRemoveChannelManageDeleteFromTeamUser, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyViewMembersNewPermission).Return(&model.System{Name: model.MigrationKeyViewMembersNewPermission, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddManageGuestsPermissions).Return(&model.System{Name: model.MigrationKeyAddManageGuestsPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyChannelModerationsPermissions).Return(&model.System{Name: model.MigrationKeyChannelModerationsPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddUseGroupMentionsPermission).Return(&model.System{Name: model.MigrationKeyAddUseGroupMentionsPermission, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddSystemConsolePermissions).Return(&model.System{Name: model.MigrationKeyAddSystemConsolePermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddConvertChannelPermissions).Return(&model.System{Name: model.MigrationKeyAddConvertChannelPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddSystemRolesPermissions).Return(&model.System{Name: model.MigrationKeyAddSystemRolesPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddBillingPermissions).Return(&model.System{Name: model.MigrationKeyAddBillingPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddDownloadComplianceExportResults).Return(&model.System{Name: model.MigrationKeyAddDownloadComplianceExportResults, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddSiteSubsectionPermissions).Return(&model.System{Name: model.MigrationKeyAddSiteSubsectionPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddExperimentalSubsectionPermissions).Return(&model.System{Name: model.MigrationKeyAddExperimentalSubsectionPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddAuthenticationSubsectionPermissions).Return(&model.System{Name: model.MigrationKeyAddAuthenticationSubsectionPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddComplianceSubsectionPermissions).Return(&model.System{Name: model.MigrationKeyAddExperimentalSubsectionPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddEnvironmentSubsectionPermissions).Return(&model.System{Name: model.MigrationKeyAddEnvironmentSubsectionPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddReportingSubsectionPermissions).Return(&model.System{Name: model.MigrationKeyAddReportingSubsectionPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddTestEmailAncillaryPermission).Return(&model.System{Name: model.MigrationKeyAddTestEmailAncillaryPermission, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddAboutSubsectionPermissions).Return(&model.System{Name: model.MigrationKeyAddAboutSubsectionPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddIntegrationsSubsectionPermissions).Return(&model.System{Name: model.MigrationKeyAddIntegrationsSubsectionPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddManageSharedChannelPermissions).Return(&model.System{Name: model.MigrationKeyAddManageSharedChannelPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddManageSecureConnectionsPermissions).Return(&model.System{Name: model.MigrationKeyAddManageSecureConnectionsPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddPlaybooksPermissions).Return(&model.System{Name: model.MigrationKeyAddPlaybooksPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddCustomUserGroupsPermissions).Return(&model.System{Name: model.MigrationKeyAddCustomUserGroupsPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddPlayboosksManageRolesPermissions).Return(&model.System{Name: model.MigrationKeyAddPlayboosksManageRolesPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddCustomUserGroupsPermissionRestore).Return(&model.System{Name: model.MigrationKeyAddCustomUserGroupsPermissionRestore, Value: "true"}, nil)
systemStore.On("GetByName", "CustomGroupAdminRoleCreationMigrationComplete").Return(&model.System{Name: model.MigrationKeyAddPlayboosksManageRolesPermissions, Value: "true"}, nil)
systemStore.On("GetByName", "products_boards").Return(&model.System{Name: "products_boards", Value: "true"}, nil)
systemStore.On("InsertIfExists", mock.AnythingOfType("*model.System")).Return(&model.System{}, nil).Once()
systemStore.On("Save", mock.AnythingOfType("*model.System")).Return(nil)
userStore := mocks.UserStore{}
userStore.On("Count", mock.AnythingOfType("model.UserCountOptions")).Return(int64(1), nil)
userStore.On("DeactivateGuests").Return(nil, nil)
userStore.On("ClearCaches").Return(nil)
postStore := mocks.PostStore{}
postStore.On("GetMaxPostSize").Return(4000)
statusStore := mocks.StatusStore{}
statusStore.On("ResetAll").Return(nil)
channelStore := mocks.ChannelStore{}
channelStore.On("ClearCaches").Return(nil)
schemeStore := mocks.SchemeStore{}
schemeStore.On("GetAllPage", model.SchemeScopeTeam, mock.Anything, 100).Return([]*model.Scheme{}, nil)
teamStore := mocks.TeamStore{}
roleStore := mocks.RoleStore{}
roleStore.On("GetAll").Return([]*model.Role{}, nil)
sessionStore := mocks.SessionStore{}
oAuthStore := mocks.OAuthStore{}
groupStore := mocks.GroupStore{}
mockStore.On("System").Return(&systemStore)
mockStore.On("User").Return(&userStore)
mockStore.On("Post").Return(&postStore)
mockStore.On("Status").Return(&statusStore)
mockStore.On("Channel").Return(&channelStore)
mockStore.On("Team").Return(&teamStore)
mockStore.On("Role").Return(&roleStore)
mockStore.On("Scheme").Return(&schemeStore)
mockStore.On("Close").Return(nil)
mockStore.On("DropAllTables").Return(nil)
mockStore.On("MarkSystemRanUnitTests").Return(nil)
mockStore.On("Session").Return(&sessionStore)
mockStore.On("OAuth").Return(&oAuthStore)
mockStore.On("Group").Return(&groupStore)
mockStore.On("GetDBSchemaVersion").Return(1, nil)
return &mockStore
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"crypto"
"crypto/rand"
"encoding/base64"
"fmt"
"html/template"
"net/http"
"net/url"
"path"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
func CheckOrigin(r *http.Request, allowedOrigins string) bool {
origin := r.Header.Get("Origin")
if origin == "" {
return true
}
if allowedOrigins == "*" {
return true
}
for _, allowed := range strings.Split(allowedOrigins, " ") {
if allowed == origin {
return true
}
}
return false
}
func OriginChecker(allowedOrigins string) func(*http.Request) bool {
return func(r *http.Request) bool {
return CheckOrigin(r, allowedOrigins)
}
}
func RenderWebAppError(config *model.Config, w http.ResponseWriter, r *http.Request, err *model.AppError, s crypto.Signer) {
RenderWebError(config, w, r, err.StatusCode, url.Values{
"message": []string{err.Message},
}, s)
}
func RenderWebError(config *model.Config, w http.ResponseWriter, r *http.Request, status int, params url.Values, s crypto.Signer) {
queryString := params.Encode()
subpath, _ := GetSubpathFromConfig(config)
h := crypto.SHA256
sum := h.New()
sum.Write([]byte(path.Join(subpath, "error") + "?" + queryString))
signature, err := s.Sign(rand.Reader, sum.Sum(nil), h)
if err != nil {
http.Error(w, "", http.StatusInternalServerError)
return
}
destination := path.Join(subpath, "error") + "?" + queryString + "&s=" + base64.URLEncoding.EncodeToString(signature)
if status >= 300 && status < 400 {
http.Redirect(w, r, destination, status)
return
}
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(status)
fmt.Fprintln(w, `<!DOCTYPE html><html><head></head>`)
fmt.Fprintln(w, `<body onload="window.location = '`+template.HTMLEscapeString(template.JSEscapeString(destination))+`'">`)
fmt.Fprintln(w, `<noscript><meta http-equiv="refresh" content="0; url=`+template.HTMLEscapeString(destination)+`"></noscript>`)
fmt.Fprintln(w, `<!-- web error message -->`)
fmt.Fprintln(w, `<a href="`+template.HTMLEscapeString(destination)+`" style="color: #c0c0c0;">...</a>`)
fmt.Fprintln(w, `</body></html>`)
}
func RenderMobileAuthComplete(w http.ResponseWriter, redirectURL string) {
var link = template.HTMLEscapeString(redirectURL)
RenderMobileMessage(w, `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="width: 64px; height: 64px; fill: #3c763d">
<!-- Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) -->
<path stroke="green" d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"/>
</svg>
<h2> `+i18n.T("api.oauth.auth_complete")+` </h2>
<p id="redirecting-message"> `+i18n.T("api.oauth.redirecting_back")+` </p>
<p id="close-tab-message" style="display: none"> `+i18n.T("api.oauth.close_browser")+` </p>
<p> `+i18n.T("api.oauth.click_redirect", model.StringInterface{"Link": link})+` </p>
<meta http-equiv="refresh" content="2; url=`+link+`">
<script>
window.onload = function() {
setTimeout(function() {
document.getElementById('redirecting-message').style.display = 'none';
document.getElementById('close-tab-message').style.display = 'block';
}, 2000);
}
</script>
`)
}
func RenderMobileError(config *model.Config, w http.ResponseWriter, err *model.AppError, redirectURL string) {
var link = template.HTMLEscapeString(redirectURL)
var invalidSchemes = map[string]bool{
"data": true,
"javascript": true,
"vbscript": true,
}
u, redirectErr := url.Parse(redirectURL)
if redirectErr != nil || invalidSchemes[u.Scheme] {
link = *config.ServiceSettings.SiteURL
}
RenderMobileMessage(w, `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" style="width: 64px; height: 64px; fill: #ccc">
<!-- Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) -->
<path d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"/>
</svg>
<h2> `+i18n.T("error")+` </h2>
<p> `+err.Message+` </p>
<a href="`+link+`">
`+i18n.T("api.back_to_app", map[string]any{"SiteName": config.TeamSettings.SiteName})+`
</a>
`)
}
func RenderMobileMessage(w http.ResponseWriter, message string) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprintln(w, `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=yes, viewport-fit=cover">
<style>
body {
color: #333;
background-color: #fff;
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 14px;
line-height: 1.42857143;
}
a {
color: #337ab7;
text-decoration: none;
}
a:focus, a:hover {
color: #23527c;
text-decoration: underline;
}
h2 {
font-size: 30px;
margin: 20px 0 10px 0;
font-weight: 500;
line-height: 1.1
}
p {
margin: 0 0 10px;
}
.message-container {
color: #555;
display: table-cell;
padding: 5em 0;
text-align: left;
vertical-align: top;
}
</style>
</head>
<body>
<!-- mobile app message -->
<div class="message-container">
`+message+`
</div>
</body>
</html>
`)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"archive/zip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
func sanitizePath(p string) string {
dir := strings.ReplaceAll(filepath.Dir(filepath.Clean(p)), "..", "")
base := filepath.Base(p)
if strings.Count(base, ".") == len(base) {
return ""
}
return filepath.Join(dir, base)
}
// UnzipToPath extracts a given zip archive into a given path.
// It returns a list of extracted paths.
func UnzipToPath(zipFile io.ReaderAt, size int64, outPath string) ([]string, error) {
rd, err := zip.NewReader(zipFile, size)
if err != nil {
return nil, fmt.Errorf("failed to create reader: %w", err)
}
paths := make([]string, len(rd.File))
for i, f := range rd.File {
filePath := sanitizePath(f.Name)
if filePath == "" {
return nil, fmt.Errorf("invalid filepath `%s`", f.Name)
}
path := filepath.Join(outPath, filePath)
paths[i] = path
if f.FileInfo().IsDir() {
if err := os.Mkdir(path, 0700); err != nil {
return nil, fmt.Errorf("failed to create directory: %w", err)
}
continue
}
if _, err := os.Stat(filepath.Dir(path)); os.IsNotExist(err) {
if err = os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return nil, fmt.Errorf("failed to create directory: %w", err)
}
}
outFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0600)
if err != nil {
return nil, fmt.Errorf("failed to create file: %w", err)
}
defer outFile.Close()
file, err := f.Open()
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
if _, err := io.Copy(outFile, file); err != nil {
return nil, fmt.Errorf("failed to write to file: %w", err)
}
}
return paths, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"time"
)
var backoffTimeouts = []time.Duration{50 * time.Millisecond, 100 * time.Millisecond, 200 * time.Millisecond, 200 * time.Millisecond, 400 * time.Millisecond, 400 * time.Millisecond}
// ProgressiveRetry executes a BackoffOperation and waits an increasing time before retrying the operation.
func ProgressiveRetry(operation func() error) error {
var err error
for attempts := 0; attempts < len(backoffTimeouts); attempts++ {
err = operation()
if err == nil {
return nil
}
time.Sleep(backoffTimeouts[attempts])
}
return err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"bytes"
"image"
"image/color"
"image/gif"
"image/jpeg"
"image/png"
"testing"
"github.com/stretchr/testify/require"
)
func CreateTestGif(t *testing.T, width int, height int) []byte {
var buffer bytes.Buffer
err := gif.Encode(&buffer, image.NewRGBA(image.Rect(0, 0, width, height)), nil)
require.NoErrorf(t, err, "failed to create gif: %v", err)
return buffer.Bytes()
}
func CreateTestAnimatedGif(t *testing.T, width int, height int, frames int) []byte {
var buffer bytes.Buffer
img := gif.GIF{
Image: make([]*image.Paletted, frames),
Delay: make([]int, frames),
}
for i := 0; i < frames; i++ {
img.Image[i] = image.NewPaletted(image.Rect(0, 0, width, height), color.Palette{color.Black})
img.Delay[i] = 0
}
err := gif.EncodeAll(&buffer, &img)
require.NoErrorf(t, err, "failed to create animated gif: %v", err)
return buffer.Bytes()
}
func CreateTestJpeg(t *testing.T, width int, height int) []byte {
var buffer bytes.Buffer
err := jpeg.Encode(&buffer, image.NewRGBA(image.Rect(0, 0, width, height)), nil)
require.NoErrorf(t, err, "failed to create jpeg: %v", err)
return buffer.Bytes()
}
func CreateTestPng(t *testing.T, width int, height int) []byte {
var buffer bytes.Buffer
err := png.Encode(&buffer, image.NewRGBA(image.Rect(0, 0, width, height)))
require.NoErrorf(t, err, "failed to create png: %v", err)
return buffer.Bytes()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
)
// CopyFile will copy a file from src path to dst path.
// Overwrites any existing files at dst.
// Permissions are copied from file at src to the new file at dst.
func CopyFile(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return
}
defer in.Close()
if err = os.MkdirAll(filepath.Dir(dst), os.ModePerm); err != nil {
return
}
out, err := os.Create(dst)
if err != nil {
return
}
defer func() {
if e := out.Close(); e != nil {
err = e
}
}()
_, err = io.Copy(out, in)
if err != nil {
return
}
err = out.Sync()
if err != nil {
return
}
stat, err := os.Stat(src)
if err != nil {
return
}
err = os.Chmod(dst, stat.Mode())
if err != nil {
return
}
return
}
// CopyDir will copy a directory and all contained files and directories.
// src must exist and dst must not exist.
// Permissions are preserved when possible. Symlinks are skipped.
func CopyDir(src string, dst string) (err error) {
src = filepath.Clean(src)
dst = filepath.Clean(dst)
stat, err := os.Stat(src)
if err != nil {
return
}
if !stat.IsDir() {
return fmt.Errorf("source must be a directory")
}
_, err = os.Stat(dst)
if err != nil && !os.IsNotExist(err) {
return
}
if err == nil {
return fmt.Errorf("destination already exists")
}
err = os.MkdirAll(dst, stat.Mode())
if err != nil {
return
}
items, err := os.ReadDir(src)
if err != nil {
return
}
for _, item := range items {
srcPath := filepath.Join(src, item.Name())
dstPath := filepath.Join(dst, item.Name())
if item.IsDir() {
err = CopyDir(srcPath, dstPath)
if err != nil {
return
}
} else {
info, ierr := item.Info()
if ierr != nil {
continue
}
if info.Mode()&os.ModeSymlink != 0 {
continue
}
err = CopyFile(srcPath, dstPath)
if err != nil {
return
}
}
}
return
}
var SizeLimitExceeded = errors.New("Size limit exceeded")
type LimitedReaderWithError struct {
limitedReader *io.LimitedReader
}
func NewLimitedReaderWithError(reader io.Reader, maxBytes int64) *LimitedReaderWithError {
return &LimitedReaderWithError{
limitedReader: &io.LimitedReader{R: reader, N: maxBytes + 1},
}
}
func (l *LimitedReaderWithError) Read(p []byte) (int, error) {
n, err := l.limitedReader.Read(p)
if l.limitedReader.N <= 0 && err == io.EOF {
return n, SizeLimitExceeded
}
return n, err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package fileutils
import (
"os"
"path/filepath"
)
func CommonBaseSearchPaths() []string {
paths := []string{
".",
"..",
"../..",
"../../..",
"../../../..",
}
// this enables the server to be used in tests from a different repository
if mmPath := os.Getenv("MM_SERVER_PATH"); mmPath != "" {
paths = append(paths, mmPath)
}
return paths
}
func findPath(path string, baseSearchPaths []string, workingDirFirst bool, filter func(os.FileInfo) bool) string {
if filepath.IsAbs(path) {
if _, err := os.Stat(path); err == nil {
return path
}
return ""
}
searchPaths := []string{}
if workingDirFirst {
searchPaths = append(searchPaths, baseSearchPaths...)
}
// Attempt to search relative to the location of the running binary either before
// or after searching relative to the working directory, depending on `workingDirFirst`.
var binaryDir string
if exe, err := os.Executable(); err == nil {
if exe, err = filepath.EvalSymlinks(exe); err == nil {
if exe, err = filepath.Abs(exe); err == nil {
binaryDir = filepath.Dir(exe)
}
}
}
if binaryDir != "" {
for _, baseSearchPath := range baseSearchPaths {
searchPaths = append(
searchPaths,
filepath.Join(binaryDir, baseSearchPath),
)
}
}
if !workingDirFirst {
searchPaths = append(searchPaths, baseSearchPaths...)
}
for _, parent := range searchPaths {
found, err := filepath.Abs(filepath.Join(parent, path))
if err != nil {
continue
} else if fileInfo, err := os.Stat(found); err == nil {
if filter != nil {
if filter(fileInfo) {
return found
}
} else {
return found
}
}
}
return ""
}
func FindPath(path string, baseSearchPaths []string, filter func(os.FileInfo) bool) string {
return findPath(path, baseSearchPaths, true, filter)
}
// FindFile looks for the given file in nearby ancestors relative to the current working
// directory as well as the directory of the executable.
func FindFile(path string) string {
return FindPath(path, CommonBaseSearchPaths(), func(fileInfo os.FileInfo) bool {
return !fileInfo.IsDir()
})
}
// fileutils.FindDir looks for the given directory in nearby ancestors relative to the current working
// directory as well as the directory of the executable, falling back to `./` if not found.
func FindDir(dir string) (string, bool) {
found := FindPath(dir, CommonBaseSearchPaths(), func(fileInfo os.FileInfo) bool {
return fileInfo.IsDir()
})
if found == "" {
return "./", false
}
return found, true
}
// FindDirRelBinary looks for the given directory in nearby ancestors relative to the
// directory of the executable, then relative to the working directory, falling back to `./` if not found.
func FindDirRelBinary(dir string) (string, bool) {
found := findPath(dir, CommonBaseSearchPaths(), false, func(fileInfo os.FileInfo) bool {
return fileInfo.IsDir()
})
if found == "" {
return "./", false
}
return found, true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"crypto/sha256"
"fmt"
)
func HashSha256(text string) string {
hash := sha256.New()
hash.Write([]byte(text))
return fmt.Sprintf("%x", hash.Sum(nil))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"fmt"
"os"
"path/filepath"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
// this functions loads translations from filesystem if they are not
// loaded already and assigns english while loading server config
func TranslationsPreInit() error {
translationsDir := "i18n"
if mattermostPath := os.Getenv("MM_SERVER_PATH"); mattermostPath != "" {
translationsDir = filepath.Join(mattermostPath, "i18n")
}
i18nDirectory, found := fileutils.FindDirRelBinary(translationsDir)
if !found {
return fmt.Errorf("unable to find i18n directory at %q", translationsDir)
}
return i18n.TranslationsPreInit(i18nDirectory)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// This is a modified version, the original copyright was: Copyright (c) 2011
// The Go Authors.
package imgutils
// This contains a portion of Go's image/go library, modified to count the number of frames in a gif without loading
// the entire image into memory.
import (
"bufio"
"compress/lzw"
"encoding/binary"
"errors"
"fmt"
"io"
)
var (
errNotEnough = errors.New("gif: not enough image data")
errTooMuch = errors.New("gif: too much image data")
)
// If the io.Reader does not also have ReadByte, then decode will introduce its own buffering.
type reader interface {
io.Reader
io.ByteReader
}
// Masks etc.
const (
// Fields.
fColorTable = 1 << 7
fColorTableBitsMask = 7
// Graphic control flags.
gcTransparentColorSet = 1 << 0
gcDisposalMethodMask = 7 << 2
)
// Disposal Methods.
const (
DisposalNone = 0x01
DisposalBackground = 0x02
DisposalPrevious = 0x03
)
// Section indicators.
const (
sExtension = 0x21
sImageDescriptor = 0x2C
sTrailer = 0x3B
)
// Extensions.
const (
eText = 0x01 // Plain Text
eGraphicControl = 0xF9 // Graphic Control
eComment = 0xFE // Comment
eApplication = 0xFF // Application
)
func readFull(r io.Reader, b []byte) error {
_, err := io.ReadFull(r, b)
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
return err
}
func readByte(r io.ByteReader) (byte, error) {
b, err := r.ReadByte()
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
return b, err
}
// decoder is the type used to decode a GIF file.
type decoder struct {
r reader
// From header.
vers string
width int
height int
loopCount int
delayTime int
backgroundIndex byte
disposalMethod byte
// From image descriptor.
imageFields byte
// From graphics control.
transparentIndex byte
hasTransparentIndex bool
// Computed.
hasGlobalColorTable bool
// Used when decoding.
imageCount int
tmp [1024]byte // must be at least 768 so we can read color table
}
// blockReader parses the block structure of GIF image data, which comprises
// (n, (n bytes)) blocks, with 1 <= n <= 255. It is the reader given to the
// LZW decoder, which is thus immune to the blocking. After the LZW decoder
// completes, there will be a 0-byte block remaining (0, ()), which is
// consumed when checking that the blockReader is exhausted.
//
// To avoid the allocation of a bufio.Reader for the lzw Reader, blockReader
// implements io.ReadByte and buffers blocks into the decoder's "tmp" buffer.
type blockReader struct {
d *decoder
i, j uint8 // d.tmp[i:j] contains the buffered bytes
err error
}
func (b *blockReader) fill() {
if b.err != nil {
return
}
b.j, b.err = readByte(b.d.r)
if b.j == 0 && b.err == nil {
b.err = io.EOF
}
if b.err != nil {
return
}
b.i = 0
b.err = readFull(b.d.r, b.d.tmp[:b.j])
if b.err != nil {
b.j = 0
}
}
func (b *blockReader) ReadByte() (byte, error) {
if b.i == b.j {
b.fill()
if b.err != nil {
return 0, b.err
}
}
c := b.d.tmp[b.i]
b.i++
return c, nil
}
// blockReader must implement io.Reader, but its Read shouldn't ever actually
// be called in practice. The compress/lzw package will only call ReadByte.
func (b *blockReader) Read(p []byte) (int, error) {
if len(p) == 0 || b.err != nil {
return 0, b.err
}
if b.i == b.j {
b.fill()
if b.err != nil {
return 0, b.err
}
}
n := copy(p, b.d.tmp[b.i:b.j])
b.i += uint8(n)
return n, nil
}
// close primarily detects whether or not a block terminator was encountered
// after reading a sequence of data sub-blocks. It allows at most one trailing
// sub-block worth of data. I.e., if some number of bytes exist in one sub-block
// following the end of LZW data, the very next sub-block must be the block
// terminator. If the very end of LZW data happened to fill one sub-block, at
// most one more sub-block of length 1 may exist before the block-terminator.
// These accommodations allow us to support GIFs created by less strict encoders.
// See https://golang.org/issue/16146.
func (b *blockReader) close() error {
if b.err == io.EOF {
// A clean block-sequence terminator was encountered while reading.
return nil
} else if b.err != nil {
// Some other error was encountered while reading.
return b.err
}
if b.i == b.j {
// We reached the end of a sub block reading LZW data. We'll allow at
// most one more sub block of data with a length of 1 byte.
b.fill()
if b.err == io.EOF {
return nil
} else if b.err != nil {
return b.err
} else if b.j > 1 {
return errTooMuch
}
}
// Part of a sub-block remains buffered. We expect that the next attempt to
// buffer a sub-block will reach the block terminator.
b.fill()
if b.err == io.EOF {
return nil
} else if b.err != nil {
return b.err
}
return errTooMuch
}
// decode reads a GIF image from r and stores the result in d.
func (d *decoder) decode(r io.Reader, configOnly bool) error {
// Add buffering if r does not provide ReadByte.
if rr, ok := r.(reader); ok {
d.r = rr
} else {
d.r = bufio.NewReader(r)
}
d.loopCount = -1
err := d.readHeaderAndScreenDescriptor()
if err != nil {
return err
}
if configOnly {
return nil
}
for {
c, err := readByte(d.r)
if err != nil {
return fmt.Errorf("gif: reading frames: %v", err)
}
switch c {
case sExtension:
if err = d.readExtension(); err != nil {
return err
}
case sImageDescriptor:
if err = d.readImageDescriptor(); err != nil {
return err
}
case sTrailer:
if d.imageCount == 0 {
return fmt.Errorf("gif: missing image data")
}
return nil
default:
return fmt.Errorf("gif: unknown block type: 0x%.2x", c)
}
}
}
func (d *decoder) readHeaderAndScreenDescriptor() error {
err := readFull(d.r, d.tmp[:13])
if err != nil {
return fmt.Errorf("gif: reading header: %v", err)
}
d.vers = string(d.tmp[:6])
if d.vers != "GIF87a" && d.vers != "GIF89a" {
return fmt.Errorf("gif: can't recognize format %q", d.vers)
}
d.width = int(d.tmp[6]) + int(d.tmp[7])<<8
d.height = int(d.tmp[8]) + int(d.tmp[9])<<8
if fields := d.tmp[10]; fields&fColorTable != 0 {
d.backgroundIndex = d.tmp[11]
// readColorTable overwrites the contents of d.tmp, but that's OK.
if err = d.readColorTable(fields); err != nil {
return err
}
d.hasGlobalColorTable = true
}
// d.tmp[12] is the Pixel Aspect Ratio, which is ignored.
return nil
}
func (d *decoder) readColorTable(fields byte) error {
n := 1 << (1 + uint(fields&fColorTableBitsMask))
err := readFull(d.r, d.tmp[:3*n])
if err != nil {
return fmt.Errorf("gif: reading color table: %s", err)
}
return nil
}
func (d *decoder) readExtension() error {
extension, err := readByte(d.r)
if err != nil {
return fmt.Errorf("gif: reading extension: %v", err)
}
size := 0
switch extension {
case eText:
size = 13
case eGraphicControl:
return d.readGraphicControl()
case eComment:
// nothing to do but read the data.
case eApplication:
b, err := readByte(d.r)
if err != nil {
return fmt.Errorf("gif: reading extension: %v", err)
}
// The spec requires size be 11, but Adobe sometimes uses 10.
size = int(b)
default:
return fmt.Errorf("gif: unknown extension 0x%.2x", extension)
}
if size > 0 {
if err := readFull(d.r, d.tmp[:size]); err != nil {
return fmt.Errorf("gif: reading extension: %v", err)
}
}
// Application Extension with "NETSCAPE2.0" as string and 1 in data means
// this extension defines a loop count.
if extension == eApplication && string(d.tmp[:size]) == "NETSCAPE2.0" {
n, err := d.readBlock()
if err != nil {
return fmt.Errorf("gif: reading extension: %v", err)
}
if n == 0 {
return nil
}
if n == 3 && d.tmp[0] == 1 {
d.loopCount = int(d.tmp[1]) | int(d.tmp[2])<<8
}
}
for {
n, err := d.readBlock()
if err != nil {
return fmt.Errorf("gif: reading extension: %v", err)
}
if n == 0 {
return nil
}
}
}
func (d *decoder) readGraphicControl() error {
if err := readFull(d.r, d.tmp[:6]); err != nil {
return fmt.Errorf("gif: can't read graphic control: %s", err)
}
if d.tmp[0] != 4 {
return fmt.Errorf("gif: invalid graphic control extension block size: %d", d.tmp[0])
}
flags := d.tmp[1]
d.disposalMethod = (flags & gcDisposalMethodMask) >> 2
d.delayTime = int(d.tmp[2]) | int(d.tmp[3])<<8
if flags&gcTransparentColorSet != 0 {
d.transparentIndex = d.tmp[4]
d.hasTransparentIndex = true
}
if d.tmp[5] != 0 {
return fmt.Errorf("gif: invalid graphic control extension block terminator: %d", d.tmp[5])
}
return nil
}
func (d *decoder) readImageDescriptor() error {
err := d.checkImageFromDescriptor()
if err != nil {
return err
}
useLocalColorTable := d.imageFields&fColorTable != 0
if useLocalColorTable {
if err = d.readColorTable(d.imageFields); err != nil {
return err
}
} else if !d.hasGlobalColorTable {
return errors.New("gif: no color table")
}
litWidth, err := readByte(d.r)
if err != nil {
return fmt.Errorf("gif: reading image data: %v", err)
}
if litWidth < 2 || litWidth > 8 {
return fmt.Errorf("gif: pixel size in decode out of range: %d", litWidth)
}
// A wonderfully Go-like piece of magic.
br := &blockReader{d: d}
lzwr := lzw.NewReader(br, lzw.LSB, int(litWidth))
defer lzwr.Close()
if _, err := io.Copy(io.Discard, lzwr); err != nil {
if err != io.ErrUnexpectedEOF {
return fmt.Errorf("gif: reading image data: %v", err)
}
return errNotEnough
}
// In theory, both lzwr and br should be exhausted. Reading from them
// should yield (0, io.EOF).
//
// The spec (Appendix F - Compression), says that "An End of
// Information code... must be the last code output by the encoder
// for an image". In practice, though, giflib (a widely used C
// library) does not enforce this, so we also accept lzwr returning
// io.ErrUnexpectedEOF (meaning that the encoded stream hit io.EOF
// before the LZW decoder saw an explicit end code), provided that
// the io.ReadFull call above successfully read len(m.Pix) bytes.
// See https://golang.org/issue/9856 for an example GIF.
if n, err := lzwr.Read(d.tmp[256:257]); n != 0 || (err != io.EOF && err != io.ErrUnexpectedEOF) {
if err != nil {
return fmt.Errorf("gif: reading image data: %v", err)
}
return errTooMuch
}
// In practice, some GIFs have an extra byte in the data sub-block
// stream, which we ignore. See https://golang.org/issue/16146.
if err := br.close(); err == errTooMuch {
return errTooMuch
} else if err != nil {
return fmt.Errorf("gif: reading image data: %v", err)
}
d.imageCount += 1
return nil
}
func (d *decoder) checkImageFromDescriptor() error {
if err := readFull(d.r, d.tmp[:9]); err != nil {
return fmt.Errorf("gif: can't read image descriptor: %s", err)
}
left := int(d.tmp[0]) + int(d.tmp[1])<<8
top := int(d.tmp[2]) + int(d.tmp[3])<<8
width := int(d.tmp[4]) + int(d.tmp[5])<<8
height := int(d.tmp[6]) + int(d.tmp[7])<<8
d.imageFields = d.tmp[8]
// The GIF89a spec, Section 20 (Image Descriptor) says: "Each image must
// fit within the boundaries of the Logical Screen, as defined in the
// Logical Screen Descriptor."
//
// This is conceptually similar to testing
// frameBounds := image.Rect(left, top, left+width, top+height)
// imageBounds := image.Rect(0, 0, d.width, d.height)
// if !frameBounds.In(imageBounds) { etc }
// but the semantics of the Go image.Rectangle type is that r.In(s) is true
// whenever r is an empty rectangle, even if r.Min.X > s.Max.X. Here, we
// want something stricter.
//
// Note that, by construction, left >= 0 && top >= 0, so we only have to
// explicitly compare frameBounds.Max (left+width, top+height) against
// imageBounds.Max (d.width, d.height) and not frameBounds.Min (left, top)
// against imageBounds.Min (0, 0).
if left+width > d.width || top+height > d.height {
return errors.New("gif: frame bounds larger than image bounds")
}
return nil
}
func (d *decoder) readBlock() (int, error) {
n, err := readByte(d.r)
if n == 0 || err != nil {
return 0, err
}
if err := readFull(d.r, d.tmp[:n]); err != nil {
return 0, err
}
return int(n), nil
}
func CountGIFFrames(r io.Reader) (int, error) {
var d decoder
if err := d.decode(r, false); err != nil {
return -1, err
}
return d.imageCount, nil
}
func GenGIFData(width, height uint16, nFrames int) []byte {
header := []byte{
'G', 'I', 'F', '8', '9', 'a', // header
0, 0, 0, 0, // width and height
128, 0, 0, // other header information
0, 0, 0, 1, 1, 1, // color table
}
binary.LittleEndian.PutUint16(header[6:], width)
binary.LittleEndian.PutUint16(header[8:], height)
frame := []byte{
0x2c, // block introducer
0, 0, 0, 0, 1, 0, 1, 0, // position and dimensions of the frame
0, // other frame information
0x2, 0x2, 0x4c, 0x1, 0, // encoded pixel data
}
trailer := []byte{0x3b}
gifData := header
for i := 0; i < nFrames; i++ {
gifData = append(gifData, frame...)
}
gifData = append(gifData, trailer...)
return gifData
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package jsonutils
import (
"bytes"
"encoding/json"
"github.com/pkg/errors"
)
type HumanizedJSONError struct {
Err error
Line int
Character int
}
func (e *HumanizedJSONError) Error() string {
return e.Err.Error()
}
// HumanizeJSONError extracts error offsets and annotates the error with useful context
func HumanizeJSONError(err error, data []byte) error {
if syntaxError, ok := err.(*json.SyntaxError); ok {
return NewHumanizedJSONError(syntaxError, data, syntaxError.Offset)
} else if unmarshalError, ok := err.(*json.UnmarshalTypeError); ok {
return NewHumanizedJSONError(unmarshalError, data, unmarshalError.Offset)
} else {
return err
}
}
func NewHumanizedJSONError(err error, data []byte, offset int64) *HumanizedJSONError {
if err == nil {
return nil
}
if offset < 0 || offset > int64(len(data)) {
return &HumanizedJSONError{
Err: errors.Wrapf(err, "invalid offset %d", offset),
}
}
lineSep := []byte{'\n'}
line := bytes.Count(data[:offset], lineSep) + 1
lastLineOffset := bytes.LastIndex(data[:offset], lineSep)
character := int(offset) - (lastLineOffset + 1) + 1
return &HumanizedJSONError{
Line: line,
Character: character,
Err: errors.Wrapf(err, "parsing error at line %d, character %d", line, character),
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"crypto"
"crypto/rsa"
"crypto/sha512"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var LicenseValidator LicenseValidatorIface
func init() {
if LicenseValidator == nil {
LicenseValidator = &LicenseValidatorImpl{}
}
}
type LicenseValidatorIface interface {
LicenseFromBytes(licenseBytes []byte) (*model.License, *model.AppError)
ValidateLicense(signed []byte) (bool, string)
}
type LicenseValidatorImpl struct {
}
func (l *LicenseValidatorImpl) LicenseFromBytes(licenseBytes []byte) (*model.License, *model.AppError) {
success, licenseStr := l.ValidateLicense(licenseBytes)
if !success {
return nil, model.NewAppError("LicenseFromBytes", model.InvalidLicenseError, nil, "", http.StatusBadRequest)
}
var license model.License
if jsonErr := json.Unmarshal([]byte(licenseStr), &license); jsonErr != nil {
return nil, model.NewAppError("LicenseFromBytes", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
return &license, nil
}
func (l *LicenseValidatorImpl) ValidateLicense(signed []byte) (bool, string) {
decoded := make([]byte, base64.StdEncoding.DecodedLen(len(signed)))
_, err := base64.StdEncoding.Decode(decoded, signed)
if err != nil {
mlog.Error("Encountered error decoding license", mlog.Err(err))
return false, ""
}
// remove null terminator
for len(decoded) > 0 && decoded[len(decoded)-1] == byte(0) {
decoded = decoded[:len(decoded)-1]
}
if len(decoded) <= 256 {
mlog.Error("Signed license not long enough")
return false, ""
}
plaintext := decoded[:len(decoded)-256]
signature := decoded[len(decoded)-256:]
block, _ := pem.Decode(publicKey)
public, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
mlog.Error("Encountered error signing license", mlog.Err(err))
return false, ""
}
rsaPublic := public.(*rsa.PublicKey)
h := sha512.New()
h.Write(plaintext)
d := h.Sum(nil)
err = rsa.VerifyPKCS1v15(rsaPublic, crypto.SHA512, d, signature)
if err != nil {
mlog.Error("Invalid signature", mlog.Err(err))
return false, ""
}
return true, string(plaintext)
}
func GetAndValidateLicenseFileFromDisk(location string) (*model.License, []byte) {
fileName := GetLicenseFileLocation(location)
if _, err := os.Stat(fileName); err != nil {
mlog.Debug("We could not find the license key in the database or on disk at", mlog.String("filename", fileName))
return nil, nil
}
mlog.Info("License key has not been uploaded. Loading license key from disk at", mlog.String("filename", fileName))
licenseBytes := GetLicenseFileFromDisk(fileName)
success, licenseStr := LicenseValidator.ValidateLicense(licenseBytes)
if !success {
mlog.Error("Found license key at %v but it appears to be invalid.", mlog.String("filename", fileName))
return nil, nil
}
var license model.License
if jsonErr := json.Unmarshal([]byte(licenseStr), &license); jsonErr != nil {
mlog.Error("Failed to decode license from JSON", mlog.Err(jsonErr))
return nil, nil
}
return &license, licenseBytes
}
func GetLicenseFileFromDisk(fileName string) []byte {
file, err := os.Open(fileName)
if err != nil {
mlog.Error("Failed to open license key from disk at", mlog.String("filename", fileName), mlog.Err(err))
return nil
}
defer file.Close()
licenseBytes, err := io.ReadAll(file)
if err != nil {
mlog.Error("Failed to read license key from disk at", mlog.String("filename", fileName), mlog.Err(err))
return nil
}
return licenseBytes
}
func GetLicenseFileLocation(fileLocation string) string {
if fileLocation == "" {
configDir, _ := fileutils.FindDir("config")
return filepath.Join(configDir, "mattermost.mattermost-license")
}
return fileLocation
}
func GetClientLicense(l *model.License) map[string]string {
props := make(map[string]string)
props["IsLicensed"] = strconv.FormatBool(l != nil)
if l != nil {
props["Id"] = l.Id
props["SkuName"] = l.SkuName
props["SkuShortName"] = l.SkuShortName
props["Users"] = strconv.Itoa(*l.Features.Users)
props["LDAP"] = strconv.FormatBool(*l.Features.LDAP)
props["LDAPGroups"] = strconv.FormatBool(*l.Features.LDAPGroups)
props["MFA"] = strconv.FormatBool(*l.Features.MFA)
props["SAML"] = strconv.FormatBool(*l.Features.SAML)
props["Cluster"] = strconv.FormatBool(*l.Features.Cluster)
props["Metrics"] = strconv.FormatBool(*l.Features.Metrics)
props["GoogleOAuth"] = strconv.FormatBool(*l.Features.GoogleOAuth)
props["Office365OAuth"] = strconv.FormatBool(*l.Features.Office365OAuth)
props["OpenId"] = strconv.FormatBool(*l.Features.OpenId)
props["Compliance"] = strconv.FormatBool(*l.Features.Compliance)
props["MHPNS"] = strconv.FormatBool(*l.Features.MHPNS)
props["Announcement"] = strconv.FormatBool(*l.Features.Announcement)
props["Elasticsearch"] = strconv.FormatBool(*l.Features.Elasticsearch)
props["DataRetention"] = strconv.FormatBool(*l.Features.DataRetention)
props["IDLoadedPushNotifications"] = strconv.FormatBool(*l.Features.IDLoadedPushNotifications)
props["IssuedAt"] = strconv.FormatInt(l.IssuedAt, 10)
props["StartsAt"] = strconv.FormatInt(l.StartsAt, 10)
props["ExpiresAt"] = strconv.FormatInt(l.ExpiresAt, 10)
props["Name"] = l.Customer.Name
props["Email"] = l.Customer.Email
props["Company"] = l.Customer.Company
props["EmailNotificationContents"] = strconv.FormatBool(*l.Features.EmailNotificationContents)
props["MessageExport"] = strconv.FormatBool(*l.Features.MessageExport)
props["CustomPermissionsSchemes"] = strconv.FormatBool(*l.Features.CustomPermissionsSchemes)
props["GuestAccounts"] = strconv.FormatBool(*l.Features.GuestAccounts)
props["GuestAccountsPermissions"] = strconv.FormatBool(*l.Features.GuestAccountsPermissions)
props["CustomTermsOfService"] = strconv.FormatBool(*l.Features.CustomTermsOfService)
props["LockTeammateNameDisplay"] = strconv.FormatBool(*l.Features.LockTeammateNameDisplay)
props["Cloud"] = strconv.FormatBool(*l.Features.Cloud)
props["SharedChannels"] = strconv.FormatBool(*l.Features.SharedChannels)
props["RemoteClusterService"] = strconv.FormatBool(*l.Features.RemoteClusterService)
props["IsTrial"] = strconv.FormatBool(l.IsTrial)
props["IsGovSku"] = strconv.FormatBool(l.IsGovSku)
}
return props
}
func GetSanitizedClientLicense(l map[string]string) map[string]string {
sanitizedLicense := make(map[string]string)
for k, v := range l {
sanitizedLicense[k] = v
}
delete(sanitizedLicense, "Id")
delete(sanitizedLicense, "Name")
delete(sanitizedLicense, "Email")
delete(sanitizedLicense, "IssuedAt")
delete(sanitizedLicense, "StartsAt")
delete(sanitizedLicense, "ExpiresAt")
delete(sanitizedLicense, "SkuName")
delete(sanitizedLicense, "SkuShortName")
return sanitizedLicense
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"html"
"regexp"
"strings"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
astExt "github.com/yuin/goldmark/extension/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/util"
)
// StripMarkdown remove some markdown syntax
func StripMarkdown(markdown string) (string, error) {
md := goldmark.New(
goldmark.WithExtensions(extension.Strikethrough),
goldmark.WithRenderer(
renderer.NewRenderer(renderer.WithNodeRenderers(
util.Prioritized(newNotificationRenderer(), 500),
)),
),
)
var buf strings.Builder
if err := md.Convert([]byte(markdown), &buf); err != nil {
return "", err
}
return strings.TrimSpace(buf.String()), nil
}
var relLinkReg = regexp.MustCompile(`\[(.*)]\((/.*)\)`)
var blockquoteReg = regexp.MustCompile(`^|\n(>)`)
// MarkdownToHTML takes a string containing Markdown and returns a string with HTML tagged version
func MarkdownToHTML(markdown, siteURL string) (string, error) {
// Turn relative links into absolute links
absLinkMarkdown := relLinkReg.ReplaceAllStringFunc(markdown, func(s string) string {
return relLinkReg.ReplaceAllString(s, "[$1]("+siteURL+"$2)")
})
// Unescape any blockquote text to be parsed by the markdown parser.
markdownClean := blockquoteReg.ReplaceAllStringFunc(absLinkMarkdown, func(s string) string {
return html.UnescapeString(s)
})
md := goldmark.New(
goldmark.WithExtensions(extension.GFM),
)
var b strings.Builder
err := md.Convert([]byte(markdownClean), &b)
if err != nil {
return "", err
}
return b.String(), nil
}
type notificationRenderer struct {
}
func newNotificationRenderer() *notificationRenderer {
return ¬ificationRenderer{}
}
func (r *notificationRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
// block
reg.Register(ast.KindDocument, r.renderDefault)
reg.Register(ast.KindHeading, r.renderItem)
reg.Register(ast.KindBlockquote, r.renderDefault)
reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock)
reg.Register(ast.KindHTMLBlock, r.renderDefault)
reg.Register(ast.KindList, r.renderDefault)
reg.Register(ast.KindListItem, r.renderItem)
reg.Register(ast.KindParagraph, r.renderItem)
reg.Register(ast.KindTextBlock, r.renderTextBlock)
reg.Register(ast.KindThematicBreak, r.renderDefault)
// inlines
reg.Register(ast.KindAutoLink, r.renderDefault)
reg.Register(ast.KindCodeSpan, r.renderDefault)
reg.Register(ast.KindEmphasis, r.renderDefault)
reg.Register(ast.KindImage, r.renderDefault)
reg.Register(ast.KindLink, r.renderDefault)
reg.Register(ast.KindRawHTML, r.renderDefault)
reg.Register(ast.KindText, r.renderText)
reg.Register(ast.KindString, r.renderString)
// strikethrough
reg.Register(astExt.KindStrikethrough, r.renderDefault)
}
// renderDefault renderer function to renderDefault without changes
func (r *notificationRenderer) renderDefault(_ util.BufWriter, _ []byte, _ ast.Node, _ bool) (ast.WalkStatus, error) {
return ast.WalkContinue, nil
}
func (r *notificationRenderer) renderItem(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
if node.NextSibling() != nil {
_ = w.WriteByte(' ')
}
}
return ast.WalkContinue, nil
}
func (r *notificationRenderer) renderCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.CodeBlock)
if entering {
r.writeLines(w, source, n)
}
return ast.WalkContinue, nil
}
func (r *notificationRenderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.FencedCodeBlock)
if entering {
r.writeLines(w, source, n)
}
return ast.WalkContinue, nil
}
func (r *notificationRenderer) renderText(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
n := node.(*ast.Text)
segment := n.Segment
_, _ = w.Write(segment.Value(source))
if !n.IsRaw() {
if n.HardLineBreak() || n.SoftLineBreak() {
_ = w.WriteByte('\n')
}
}
return ast.WalkContinue, nil
}
func (r *notificationRenderer) renderTextBlock(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
if node.NextSibling() != nil && node.FirstChild() != nil {
_ = w.WriteByte(' ')
}
}
return ast.WalkContinue, nil
}
func (r *notificationRenderer) renderString(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
n := node.(*ast.String)
_, _ = w.Write(n.Value)
return ast.WalkContinue, nil
}
func (r *notificationRenderer) writeLines(w util.BufWriter, source []byte, n ast.Node) {
for i := 0; i < n.Lines().Len(); i++ {
line := n.Lines().At(i)
value := line.Value(source)
_, _ = w.Write(value)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"fmt"
"reflect"
)
// StructFieldFilter defines a callback function used to decide if a patch value should be applied.
type StructFieldFilter func(structField reflect.StructField, base reflect.Value, patch reflect.Value) bool
// MergeConfig allows for optional merge customizations.
type MergeConfig struct {
StructFieldFilter StructFieldFilter
}
// Merge will return a new value of the same type as base and patch, recursively merging non-nil values from patch on top of base.
//
// Restrictions/guarantees:
// - base and patch must be the same type
// - base and patch will never be modified
// - values from patch are always selected when non-nil
// - structs are merged recursively
// - maps and slices are treated as pointers, and merged as a single value
//
// Note that callers need to cast the returned interface back into the original type:
//
// func mergeTestStruct(base, patch *testStruct) (*testStruct, error) {
// ret, err := merge(base, patch)
// if err != nil {
// return nil, err
// }
//
// retTS := ret.(testStruct)
// return &retTS, nil
// }
func Merge(base any, patch any, mergeConfig *MergeConfig) (any, error) {
if reflect.TypeOf(base) != reflect.TypeOf(patch) {
return nil, fmt.Errorf(
"cannot merge different types. base type: %s, patch type: %s",
reflect.TypeOf(base),
reflect.TypeOf(patch),
)
}
commonType := reflect.TypeOf(base)
baseVal := reflect.ValueOf(base)
patchVal := reflect.ValueOf(patch)
if commonType.Kind() == reflect.Ptr {
commonType = commonType.Elem()
baseVal = baseVal.Elem()
patchVal = patchVal.Elem()
}
ret := reflect.New(commonType)
val, ok := merge(baseVal, patchVal, mergeConfig)
if ok {
ret.Elem().Set(val)
}
return ret.Elem().Interface(), nil
}
// merge recursively merges patch into base and returns the new struct, ptr, slice/map, or value
func merge(base, patch reflect.Value, mergeConfig *MergeConfig) (reflect.Value, bool) {
commonType := base.Type()
switch commonType.Kind() {
case reflect.Struct:
merged := reflect.New(commonType).Elem()
for i := 0; i < base.NumField(); i++ {
if !merged.Field(i).CanSet() {
continue
}
if mergeConfig != nil && mergeConfig.StructFieldFilter != nil {
if !mergeConfig.StructFieldFilter(commonType.Field(i), base.Field(i), patch.Field(i)) {
merged.Field(i).Set(base.Field(i))
continue
}
}
val, ok := merge(base.Field(i), patch.Field(i), mergeConfig)
if ok {
merged.Field(i).Set(val)
}
}
return merged, true
case reflect.Ptr:
mergedPtr := reflect.New(commonType.Elem())
if base.IsNil() && patch.IsNil() {
return mergedPtr, false
}
// clone reference values (if any)
if base.IsNil() {
val, _ := merge(patch.Elem(), patch.Elem(), mergeConfig)
mergedPtr.Elem().Set(val)
} else if patch.IsNil() {
val, _ := merge(base.Elem(), base.Elem(), mergeConfig)
mergedPtr.Elem().Set(val)
} else {
val, _ := merge(base.Elem(), patch.Elem(), mergeConfig)
mergedPtr.Elem().Set(val)
}
return mergedPtr, true
case reflect.Slice:
if base.IsNil() && patch.IsNil() {
return reflect.Zero(commonType), false
}
if !patch.IsNil() {
// use patch
merged := reflect.MakeSlice(commonType, 0, patch.Len())
for i := 0; i < patch.Len(); i++ {
// recursively merge patch with itself. This will clone reference values.
val, _ := merge(patch.Index(i), patch.Index(i), mergeConfig)
merged = reflect.Append(merged, val)
}
return merged, true
}
// use base
merged := reflect.MakeSlice(commonType, 0, base.Len())
for i := 0; i < base.Len(); i++ {
// recursively merge base with itself. This will clone reference values.
val, _ := merge(base.Index(i), base.Index(i), mergeConfig)
merged = reflect.Append(merged, val)
}
return merged, true
case reflect.Map:
// maps are merged according to these rules:
// - if patch is not nil, replace the base map completely
// - otherwise, keep the base map
// - reference values (eg. slice/ptr/map) will be cloned
if base.IsNil() && patch.IsNil() {
return reflect.Zero(commonType), false
}
merged := reflect.MakeMap(commonType)
mapPtr := base
if !patch.IsNil() {
mapPtr = patch
}
for _, key := range mapPtr.MapKeys() {
// clone reference values
val, ok := merge(mapPtr.MapIndex(key), mapPtr.MapIndex(key), mergeConfig)
if !ok {
val = reflect.New(mapPtr.MapIndex(key).Type()).Elem()
}
merged.SetMapIndex(key, val)
}
return merged, true
case reflect.Interface:
var val reflect.Value
if base.IsNil() && patch.IsNil() {
return reflect.Zero(commonType), false
}
// clone reference values (if any)
if base.IsNil() {
val, _ = merge(patch.Elem(), patch.Elem(), mergeConfig)
} else if patch.IsNil() {
val, _ = merge(base.Elem(), base.Elem(), mergeConfig)
} else {
val, _ = merge(base.Elem(), patch.Elem(), mergeConfig)
}
return val, true
default:
return patch, true
}
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make misc-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// LicenseValidatorIface is an autogenerated mock type for the LicenseValidatorIface type
type LicenseValidatorIface struct {
mock.Mock
}
// LicenseFromBytes provides a mock function with given fields: licenseBytes
func (_m *LicenseValidatorIface) LicenseFromBytes(licenseBytes []byte) (*model.License, *model.AppError) {
ret := _m.Called(licenseBytes)
var r0 *model.License
if rf, ok := ret.Get(0).(func([]byte) *model.License); ok {
r0 = rf(licenseBytes)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.License)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func([]byte) *model.AppError); ok {
r1 = rf(licenseBytes)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// ValidateLicense provides a mock function with given fields: signed
func (_m *LicenseValidatorIface) ValidateLicense(signed []byte) (bool, string) {
ret := _m.Called(signed)
var r0 bool
if rf, ok := ret.Get(0).(func([]byte) bool); ok {
r0 = rf(signed)
} else {
r0 = ret.Get(0).(bool)
}
var r1 string
if rf, ok := ret.Get(1).(func([]byte) string); ok {
r1 = rf(signed)
} else {
r1 = ret.Get(1).(string)
}
return r0, r1
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"math/rand"
)
type Range struct {
Begin int
End int
}
func RandIntFromRange(r Range) int {
if r.End-r.Begin <= 0 {
return r.Begin
}
return rand.Intn((r.End-r.Begin)+1) + r.Begin
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// getSubpathScript renders the inline script that defines window.publicPath to change how webpack loads assets.
func getSubpathScript(subpath string) string {
if subpath == "" {
subpath = "/"
}
newPath := path.Join(subpath, "static") + "/"
return fmt.Sprintf("window.publicPath='%s'", newPath)
}
// GetSubpathScriptHash computes the script-src addition required for the subpath script to bypass CSP protections.
func GetSubpathScriptHash(subpath string) string {
// No hash is required for the default subpath.
if subpath == "" || subpath == "/" {
return ""
}
scriptHash := sha256.Sum256([]byte(getSubpathScript(subpath)))
return fmt.Sprintf(" 'sha256-%s'", base64.StdEncoding.EncodeToString(scriptHash[:]))
}
// UpdateAssetsSubpathInDir rewrites assets in the given directory to assume the application is
// hosted at the given subpath instead of at the root. No changes are written unless necessary.
func UpdateAssetsSubpathInDir(subpath, directory string) error {
if subpath == "" {
subpath = "/"
}
staticDir, found := fileutils.FindDir(directory)
if !found {
return errors.New("failed to find client dir")
}
staticDir, err := filepath.EvalSymlinks(staticDir)
if err != nil {
return errors.Wrapf(err, "failed to resolve symlinks to %s", staticDir)
}
rootHTMLPath := filepath.Join(staticDir, "root.html")
oldRootHTML, err := os.ReadFile(rootHTMLPath)
if err != nil {
return errors.Wrap(err, "failed to open root.html")
}
oldSubpath := "/"
// Determine if a previous subpath had already been rewritten into the assets.
reWebpackPublicPathScript := regexp.MustCompile("window.publicPath='([^']+/)static/'")
alreadyRewritten := false
if matches := reWebpackPublicPathScript.FindStringSubmatch(string(oldRootHTML)); matches != nil {
oldSubpath = matches[1]
alreadyRewritten = true
}
pathToReplace := path.Join(oldSubpath, "static") + "/"
newPath := path.Join(subpath, "static") + "/"
mlog.Debug("Rewriting static assets", mlog.String("from_subpath", oldSubpath), mlog.String("to_subpath", subpath))
newRootHTML := string(oldRootHTML)
reCSP := regexp.MustCompile(`<meta http-equiv="Content-Security-Policy" content="script-src 'self' cdn.rudderlabs.com/ js.stripe.com/v3([^"]*)">`)
if results := reCSP.FindAllString(newRootHTML, -1); len(results) == 0 {
return fmt.Errorf("failed to find 'Content-Security-Policy' meta tag to rewrite")
}
newRootHTML = reCSP.ReplaceAllLiteralString(newRootHTML, fmt.Sprintf(
`<meta http-equiv="Content-Security-Policy" content="script-src 'self' cdn.rudderlabs.com/ js.stripe.com/v3%s">`,
GetSubpathScriptHash(subpath),
))
// Rewrite the root.html references to `/static/*` to include the given subpath.
// This potentially includes a previously injected inline script that needs to
// be updated (and isn't covered by the cases above).
newRootHTML = strings.Replace(newRootHTML, pathToReplace, newPath, -1)
if alreadyRewritten && subpath == "/" {
// Remove the injected script since no longer required. Note that the rewrite above
// will have affected the script, so look for the new subpath, not the old one.
oldScript := getSubpathScript(subpath)
newRootHTML = strings.Replace(newRootHTML, fmt.Sprintf("</style><script>%s</script>", oldScript), "</style>", 1)
} else if !alreadyRewritten && subpath != "/" {
// Otherwise, inject the script to define `window.publicPath`.
script := getSubpathScript(subpath)
newRootHTML = strings.Replace(newRootHTML, "</style>", fmt.Sprintf("</style><script>%s</script>", script), 1)
}
// Write out the updated root.html.
if err = os.WriteFile(rootHTMLPath, []byte(newRootHTML), 0); err != nil {
return errors.Wrapf(err, "failed to update root.html with subpath %s", subpath)
}
// Rewrite the manifest.json and *.css references to `/static/*` (or a previously rewritten subpath).
err = filepath.Walk(staticDir, func(walkPath string, info os.FileInfo, err error) error {
if filepath.Base(walkPath) == "manifest.json" || filepath.Ext(walkPath) == ".css" {
old, err := os.ReadFile(walkPath)
if err != nil {
return errors.Wrapf(err, "failed to open %s", walkPath)
}
new := strings.Replace(string(old), pathToReplace, newPath, -1)
if err = os.WriteFile(walkPath, []byte(new), 0); err != nil {
return errors.Wrapf(err, "failed to update %s with subpath %s", walkPath, subpath)
}
}
return nil
})
if err != nil {
return errors.Wrapf(err, "error walking %s", staticDir)
}
return nil
}
// UpdateAssetsSubpath rewrites assets in the /client directory to assume the application is hosted
// at the given subpath instead of at the root. No changes are written unless necessary.
func UpdateAssetsSubpath(subpath string) error {
return UpdateAssetsSubpathInDir(subpath, model.ClientDir)
}
// UpdateAssetsSubpathFromConfig uses UpdateAssetsSubpath and any path defined in the SiteURL.
func UpdateAssetsSubpathFromConfig(config *model.Config) error {
// Don't rewrite in development environments, since webpack in developer mode constantly
// updates the assets and must be configured separately.
if model.BuildNumber == "dev" {
mlog.Debug("Skipping update to assets subpath since dev build")
return nil
}
// Similarly, don't rewrite during a CI build, when the assets may not even be present.
if os.Getenv("IS_CI") == "true" {
mlog.Debug("Skipping update to assets subpath since CI build")
return nil
}
subpath, err := GetSubpathFromConfig(config)
if err != nil {
return err
}
return UpdateAssetsSubpath(subpath)
}
func GetSubpathFromConfig(config *model.Config) (string, error) {
if config == nil {
return "", errors.New("no config provided")
} else if config.ServiceSettings.SiteURL == nil {
return "/", nil
}
u, err := url.Parse(*config.ServiceSettings.SiteURL)
if err != nil {
return "", errors.Wrap(err, "failed to parse SiteURL from config")
}
if u.Path == "" {
return "/", nil
}
return path.Clean(u.Path), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/require"
)
func CompileGo(t *testing.T, sourceCode, outputPath string) {
dir, err := os.MkdirTemp(".", "")
require.NoError(t, err)
defer os.RemoveAll(dir)
dir, err = filepath.Abs(dir)
require.NoError(t, err)
// Write out main.go given the source code.
main := filepath.Join(dir, "main.go")
err = os.WriteFile(main, []byte(sourceCode), 0600)
require.NoError(t, err)
_, sourceFile, _, ok := runtime.Caller(0)
require.True(t, ok)
serverPath := filepath.Dir(filepath.Dir(sourceFile))
out := &bytes.Buffer{}
cmd := exec.Command("go", "build", "-o", outputPath, main)
cmd.Dir = serverPath
cmd.Stdout = out
cmd.Stderr = out
err = cmd.Run()
if err != nil {
t.Log("Go compile errors:\n", out.String())
}
require.NoError(t, err, "failed to compile go")
}
func CompileGoTest(t *testing.T, sourceCode, outputPath string) {
dir, err := os.MkdirTemp(".", "")
require.NoError(t, err)
defer os.RemoveAll(dir)
dir, err = filepath.Abs(dir)
require.NoError(t, err)
// Write out main.go given the source code.
main := filepath.Join(dir, "main_test.go")
err = os.WriteFile(main, []byte(sourceCode), 0600)
require.NoError(t, err)
_, sourceFile, _, ok := runtime.Caller(0)
require.True(t, ok)
serverPath := filepath.Dir(filepath.Dir(sourceFile))
out := &bytes.Buffer{}
cmd := exec.Command("go", "test", "-c", "-o", outputPath, main)
cmd.Dir = serverPath
cmd.Stdout = out
cmd.Stderr = out
err = cmd.Run()
if err != nil {
t.Log("Go compile errors:\n", out.String())
}
require.NoError(t, err, "failed to compile go")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package testutils
import (
"crypto/ecdsa"
"github.com/mattermost/mattermost-server/v6/model"
)
type StaticConfigService struct {
Cfg *model.Config
}
func (s StaticConfigService) Config() *model.Config {
return s.Cfg
}
func (StaticConfigService) AddConfigListener(func(old, current *model.Config)) string {
return ""
}
func (StaticConfigService) RemoveConfigListener(string) {
}
func (StaticConfigService) AsymmetricSigningKey() *ecdsa.PrivateKey {
return &ecdsa.PrivateKey{}
}
func (StaticConfigService) PostActionCookieSecret() []byte {
return make([]byte, 32)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package testutils
import (
"bytes"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
)
func ReadTestFile(name string) ([]byte, error) {
path, _ := fileutils.FindDir("tests")
file, err := os.Open(filepath.Join(path, name))
if err != nil {
return nil, err
}
defer file.Close()
data := &bytes.Buffer{}
if _, err := io.Copy(data, file); err != nil {
return nil, err
}
return data.Bytes(), nil
}
// GetInterface returns the best match of an interface that might be listening on a given port.
// This is helpful when a test is being run in a CI environment under docker.
func GetInterface(port int) string {
dial := func(iface string, port int) bool {
c, err := net.DialTimeout("tcp", iface+":"+strconv.Itoa(port), time.Second)
if err != nil {
return false
}
c.Close()
return true
}
// First, we check dockerhost
iface := "dockerhost"
if ok := dial(iface, port); ok {
return iface
}
// If not, we check localhost
iface = "localhost"
if ok := dial(iface, port); ok {
return iface
}
// If nothing works, we just attempt to use a hack and get the interface IP.
// https://stackoverflow.com/a/37212665/4962526.
cmdStr := ""
switch runtime.GOOS {
// Using ip address for Linux, ifconfig for Darwin.
case "linux":
cmdStr = `ip address | grep -E "([0-9]{1,3}\.){3}[0-9]{1,3}" | grep -v 127.0.0.1 | awk '{ print $2 }' | cut -f2 -d: | cut -f1 -d/ | head -n1`
case "darwin":
cmdStr = `ifconfig | grep -E "([0-9]{1,3}\.){3}[0-9]{1,3}" | grep -v 127.0.0.1 | awk '{ print $2 }' | cut -f2 -d: | head -n1`
default:
return ""
}
cmd := exec.Command("bash", "-c", cmdStr)
out, err := cmd.CombinedOutput()
if err != nil {
return ""
}
return string(out)
}
func ResetLicenseValidator() {
utils.LicenseValidator = &utils.LicenseValidatorImpl{}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"math/rand"
"strings"
)
const (
ALPHANUMERIC = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890"
LOWERCASE = "abcdefghijklmnopqrstuvwxyz"
)
// Strings that should pass as acceptable posts
var FuzzyStringsPosts = []string{
`**[1] - [Markdown Tests]**
_italics_
more _italics_
**bold**
more **bold**
**_bold-italic_**
more **_bold-italic_*8
~~strikethrough~~
more ~~strikethrough~~
` + "```" + `
multi-line code block<enter here>
multi-line code block
emoji that should not render in code block: :ice_cream:
` + "```" + `
` + "`monospace`" + `
[Link to Mattermost](www.mattermost.com)
Inline Image with link, alt text, and hover text: ](https://travis-ci.org/mattermost/mattermost-server)
Three types of lines:
***
___
---
`,
` **[2] - **[More Markdown Tests]**
> i am a blockquote!
> i am a 2nd multiline
> quote.
i am text right after a multiline quote, but not in the quote
* list item
* another list item
* indented list item
1. numbered list, item number 1
2. item number two
`,
` **[3]** - **[More Markdown Tests]**
Table
| Left-Aligned | Center Aligned | Right Aligned |
| :------------ |:---------------:| -----:|
| Left column 1 | this text | $100 |
| Left column 2 | is | $10 |
| Left column 3 | centered | $1 |
Ugly table
Markdown | Less | Pretty
--- | --- | ---
*Still* | ~~renders~~ | **nicely**
1 | 2 | 3
# Large heading
## Smaller heading
### Even smaller heading
# Large heading
## Smaller heading
### Even smaller heading
`,
` **[4]** - **[More Markdown Tests]**
# This is a heading
I am a multiline
text.
#### I am a level four heading
` + "```tex" + `
f(x) = \int_{-\infty}^\infty
\hat f(\xi)\,e^{2 \pi i \xi x}
\,d\xi
` + "```" + `
* This was some tex code*
`,
`**[5]** - **[Markdown and automatic preview of content test]**
## This should display a preview for the given vine url
Some text *before* the link
And a smiley :)
https://vine.co/v/eDeVgbFrt9L
Some more text here
and here
and even more here
`,
`**[6]** - **[More markdown and automatic preview of content test]**
## Only the first given url should render an "attachment"
Lets also add a table here, because why not
| Left-Aligned | Center Aligned | Right Aligned |
| :------------ |:---------------:| -----:|
| Left column 1 | this text | $100 |
| Left column 2 | is | $10 |
| Left column 3 | centered | $1 |
Wiki should render:
http://en.wikipedia.org/wiki/Foo
https://vine.co/v/eDeVgbFrt9L
`,
`**[7] [Image Test]**
## this *should* display an image
http://37.media.tumblr.com/tumblr_mavsumGGAd1qboaw8o1_500.jpg
`,
/* `**[2] [Username Linking Test]**
I saw @alice--and I said "Hi @alice!" then "What's up @alice?" and then @alice, was totally @alice; she just "@alice"'d me and walked on by. That's @alice...
@alice‽‽
`,
`**[3] [Mention Highlighting Test]**
`,*/
`**[8] [Emoji Display Test 1]**
:+1: :-1: :100: :1234: :8ball: :a: :ab: :abc: :abcd: :accept:
:aerial_tramway: :airplane: :alarm_clock: :ambulance: :anchor: :angel: :anger: :angry: :anguished: :ant:
:apple: :aquarius: :aries: :arrow_backward: :arrow_double_down: :arrow_double_up: :arrow_down: :arrow_down_small: :arrow_forward: :arrow_heading_down:
:arrow_heading_up: :arrow_left: :arrow_lower_left: :arrow_lower_right: :arrow_right: :arrow_right_hook: :arrow_up: :arrow_up_down:
:arrow_upper_left: :arrow_upper_right: :arrows_clockwise: :arrows_counterclockwise: :art: :articulated_lorry: :astonished: :atm: :arrow_up_small: :b:
:baby: :baby_bottle: :baby_chick: :baby_symbol: :back: :baggage_claim: :balloon: :ballot_box_with_check: :bamboo: :banana:
:bangbang: :bank: :bar_chart: :barber: :baseball: :basketball: :bath: :bathtub: :battery: :bear:
:bee: :beer: :beers: :beetle: :beginner: :bell: :bento: :bicyclist: :bike: :bikini:
:bird: :birthday: :black_circle: :black_joker: :black_medium_small_square: :black_medium_square: :black_nib: :black_small_square: :black_square: :black_square_button:
:blossom: :blowfish: :blue_book: :blue_car: :blue_heart: :blush: :boar: :boat: :bomb: :book:
:bookmark: :bookmark_tabs: :books: :boom: :boot: :bouquet: :bow: :bowling: :bowtie: :boy:
:bread: :bride_with_veil: :bridge_at_night: :briefcase: :broken_heart: :bug: :bulb: :bullettrain_front: :bullettrain_side: :bus:
:busstop: :bust_in_silhouette: :busts_in_silhouette: :cactus: :cake: :calendar: :calling: :camel: :camera: :cancer:
:candy: :capital_abcd: :capricorn: :car: :card_index: :carousel_horse: :cat: :cat2: :cd: :chart:
:chart_with_downwards_trend: :chart_with_upwards_trend: :checkered_flag: :cherries: :cherry_blossom: :chestnut: :chicken: :children_crossing: :chocolate_bar: :christmas_tree:
:church: :cinema: :circus_tent: :city_sunrise: :city_sunset: :cl: :clap: :clapper: :clipboard: :clock1:
:clock10: :clock1030: :clock11: :clock1130: :clock12: :clock1230: :clock130: :clock2: :clock230: :clock3:
:clock330: :clock4: :clock430: :clock5: :clock530: :clock6: :clock630: :clock7: :clock730: :clock8:
:clock830: :clock9: :clock930: :closed_book: :closed_lock_with_key: :closed_umbrella: :cloud: :clubs: :cn: :cocktail:
:coffee: :cold_sweat: :collision: :computer: :confetti_ball: :confounded: :confused: :congratulations: :construction: :construction_worker:
:convenience_store: :cookie: :cool: :cop: :copyright: :corn: :couple: :couple_with_heart: :couplekiss: :cow:
:cow2: :credit_card: :crescent_moon: :crocodile: :crossed_flags: :crown: :cry: :crying_cat_face: :crystal_ball: :cupid:
:curly_loop: :currency_exchange: :curry: :custard: :customs: :cyclone: :dancer: :dancers: :dango: :dart:
:dash: :date: :de: :deciduous_tree: :department_store: :diamond_shape_with_a_dot_inside: :diamonds: :disappointed: :disappointed_relieved: :dizzy:
:dizzy_face: :do_not_litter: :dog: :dog2: :dollar: :dolls: :dolphin: :donut: :door: :doughnut:
:dragon: :dragon_face: :dress: :dromedary_camel: :droplet: :dvd: :e-mail: :ear: :ear_of_rice: :earth_africa:
:earth_americas: :earth_asia: :egg: :eggplant: :eight: :eight_pointed_black_star: :eight_spoked_asterisk: :electric_plug: :elephant: :email:
:end: :envelope: :es: :euro: :european_castle: :european_post_office: :evergreen_tree: :exclamation: :expressionless: :eyeglasses:
:eyes: :facepunch: :factory: :fallen_leaf: :family: :fast_forward: :fax: :fearful: :feelsgood: :feet:
:ferris_wheel: :file_folder: :finnadie: :fire: :fire_engine: :fireworks: :first_quarter_moon: :first_quarter_moon_with_face: :fish: :fish_cake:
:fishing_pole_and_fish: :fist: :five: :flags: :flashlight: :floppy_disk: :flower_playing_cards: :flushed: :foggy: :football:
:fork_and_knife: :fountain: :four: :four_leaf_clover: :fr: :free: :fried_shrimp: :fries: :frog: :frowning:
:fu: :fuelpump: :full_moon: :full_moon_with_face: :game_die: :gb: :gem: :gemini: :ghost: :gift:`,
`**[9] [Emoji Display Test 2]**
:gift_heart: :girl: :globe_with_meridians: :goat: :goberserk: :godmode: :golf: :grapes: :green_apple: :green_book:
:green_heart: :grey_exclamation: :grey_question: :grimacing: :grin: :grinning: :guardsman: :guitar: :gun: :haircut:
:hamburger: :hammer: :hamster: :hand: :handbag: :hankey: :hash: :hatched_chick: :hatching_chick: :headphones:
:hear_no_evil: :heart: :heart_decoration: :heart_eyes: :heart_eyes_cat: :heartbeat: :heartpulse: :hearts: :heavy_check_mark: :heavy_division_sign:
:heavy_dollar_sign: :heavy_exclamation_mark: :heavy_minus_sign: :heavy_multiplication_x: :heavy_plus_sign: :helicopter: :herb: :hibiscus: :high_brightness: :high_heel:
:hocho: :honey_pot: :honeybee: :horse: :horse_racing: :hospital: :hotel: :hotsprings: :hourglass: :hourglass_flowing_sand:
:house: :house_with_garden: :hurtrealbad: :hushed: :ice_cream: :icecream: :id: :ideograph_advantage: :imp: :inbox_tray:
:incoming_envelope: :information_desk_person: :information_source: :innocent: :interrobang: :iphone: :it: :izakaya_lantern: :jack_o_lantern:
:japan: :japanese_castle: :japanese_goblin: :japanese_ogre: :jeans: :joy: :joy_cat: :jp: :key: :keycap_ten:
:kimono: :kiss: :kissing: :kissing_cat: :kissing_closed_eyes: :kissing_face: :kissing_heart: :kissing_smiling_eyes: :koala: :koko:
:kr: :large_blue_circle: :large_blue_diamond: :large_orange_diamond: :last_quarter_moon: :last_quarter_moon_with_face: :laughing: :leaves: :ledger: :left_luggage:
:left_right_arrow: :leftwards_arrow_with_hook: :lemon: :leo: :leopard: :libra: :light_rail: :link: :lips: :lipstick:
:lock: :lock_with_ink_pen: :lollipop: :loop: :loudspeaker: :love_hotel: :love_letter: :low_brightness: :m: :mag:
:mag_right: :mahjong: :mailbox: :mailbox_closed: :mailbox_with_mail: :mailbox_with_no_mail: :man: :man_with_gua_pi_mao: :man_with_turban: :mans_shoe:
:maple_leaf: :mask: :massage: :meat_on_bone: :mega: :melon: :memo: :mens: :metal: :metro:
:microphone: :microscope: :milky_way: :minibus: :minidisc: :mobile_phone_off: :money_with_wings: :moneybag: :monkey: :monkey_face:
:monorail: :mortar_board: :mount_fuji: :mountain_bicyclist: :mountain_cableway: :mountain_railway: :mouse: :mouse2: :movie_camera: :moyai:
:muscle: :mushroom: :musical_keyboard: :musical_note: :musical_score: :mute: :nail_care: :name_badge: :neckbeard: :necktie:
:negative_squared_cross_mark: :neutral_face: :new: :new_moon: :new_moon_with_face: :newspaper: :ng: :nine: :no_bell:
:no_bicycles: :no_entry: :no_entry_sign: :no_good: :no_mobile_phones: :no_mouth: :no_pedestrians: :no_smoking: :non-potable_water: :nose:
:notebook: :notebook_with_decorative_cover: :notes: :nut_and_bolt: :o: :o2: :ocean: :octocat: :octopus: :oden:
:office: :ok: :ok_hand: :ok_woman: :older_man: :older_woman: :on: :oncoming_automobile: :oncoming_bus: :oncoming_police_car:
:oncoming_taxi: :one: :open_file_folder: :open_hands: :open_mouth: :ophiuchus: :orange_book: :outbox_tray: :ox: :package:
:page_facing_up: :page_with_curl: :pager: :palm_tree: :panda_face: :paperclip: :parking: :part_alternation_mark: :partly_sunny: :passport_control:
:paw_prints: :peach: :pear: :pencil: :pencil2: :penguin: :pensive: :performing_arts: :persevere: :person_frowning:
:person_with_blond_hair: :person_with_pouting_face: :phone: :pig: :pig2: :pig_nose: :pill: :pineapple: :pisces: :pizza:
`,
`**[10] [Emoji Display Test 3]**
:plus1: :point_down: :point_left: :point_right: :point_up: :point_up_2: :police_car: :poodle: :poop: :post_office:
:postal_horn: :postbox: :potable_water: :pouch: :poultry_leg: :pound: :pouting_cat: :pray: :princess: :punch:
:purple_heart: :purse: :pushpin: :put_litter_in_its_place: :question: :rabbit: :rabbit2: :racehorse: :radio: :radio_button:
:rage: :rage1: :rage2: :rage3: :rage4: :railway_car: :rainbow: :raised_hand: :raised_hands: :raising_hand:
:ram: :ramen: :rat: :recycle: :red_car: :red_circle: :registered: :relaxed: :relieved: :repeat:
:repeat_one: :restroom: :revolving_hearts: :rewind: :ribbon: :rice: :rice_ball: :rice_cracker: :rice_scene: :ring:
:rocket: :roller_coaster: :rooster: :rose: :rotating_light: :round_pushpin: :rowboat: :ru:
:rugby_football: :runner: :running: :running_shirt_with_sash: :sa: :sagittarius: :sailboat: :sake: :sandal: :santa:
:satellite: :satisfied: :saxophone: :school: :school_satchel: :scissors: :scorpius: :scream: :scream_cat: :scroll:
:seat: :secret: :see_no_evil: :seedling: :seven: :shaved_ice: :sheep: :shell: :ship: :shipit:
:shirt: :shit: :shoe: :shower: :signal_strength: :six: :six_pointed_star: :ski: :skull: :sleeping:
:sleepy: :slot_machine: :small_blue_diamond: :small_orange_diamond: :small_red_triangle: :small_red_triangle_down: :smile: :smile_cat: :smiley: :smiley_cat:
:smiling_imp: :smirk: :smirk_cat: :smoking: :snail: :snake: :snowboarder: :snowflake: :snowman: :sob:
:soccer: :soon: :sos: :sound: :space_invader: :spades: :spaghetti: :sparkle: :sparkler: :sparkles:
:sparkling_heart: :speak_no_evil: :speaker: :speech_balloon: :speedboat: :squirrel: :star: :star2: :stars: :station:
:statue_of_liberty: :steam_locomotive: :stew: :straight_ruler: :strawberry: :stuck_out_tongue: :stuck_out_tongue_closed_eyes: :stuck_out_tongue_winking_eye: :sun_with_face: :sunflower:
:sunglasses: :sunny: :sunrise: :sunrise_over_mountains: :surfer: :sushi: :suspect: :suspension_railway: :sweat: :sweat_drops:
:sweat_smile: :sweet_potato: :swimmer: :symbols: :syringe: :tada: :tanabata_tree: :tangerine: :taurus: :taxi:
:tea: :telephone: :telephone_receiver: :telescope: :tennis: :tent: :thought_balloon: :three: :thumbsdown: :thumbsup:
:ticket: :tiger: :tiger2: :tired_face: :tm: :toilet: :tokyo_tower: :tomato: :tongue: :top:
:tophat: :tractor: :traffic_light: :train: :train2: :tram: :triangular_flag_on_post: :triangular_ruler: :trident: :triumph:
:trolleybus: :trollface: :trophy: :tropical_drink: :tropical_fish: :truck: :trumpet: :tshirt: :tulip: :turtle:
:tv: :twisted_rightwards_arrows: :two: :two_hearts: :two_men_holding_hands: :two_women_holding_hands:
:uk: :umbrella: :unamused: :underage: :unlock: :up: :us: :v: :vertical_traffic_light: :vhs:
:vibration_mode: :video_camera: :video_game: :violin: :virgo: :volcano: :vs: :walking: :waning_crescent_moon: :waning_gibbous_moon:
:warning: :watch: :water_buffalo: :watermelon: :wave: :wavy_dash: :waxing_crescent_moon: :waxing_gibbous_moon: :wc: :weary:
:wedding: :whale: :whale2: :wheelchair: :white_check_mark: :white_circle: :white_flower: :white_large_square: :white_medium_small_square: :white_medium_square:
:white_small_square: :white_square_button: :wind_chime: :wine_glass: :wink: :wolf: :woman: :womans_clothes: :womans_hat: :womens:
:worried: :wrench: :x: :yellow_heart: :yen: :yum: :zap: :zero: :zzz:
Unnamed: :u5272: :u5408: :u55b6: :u6307: :u6708: :u6709: :u6e80: :u7121: :u7533: :u7981: :u7a7a:
`,
`**[11] [Auto Linking]**
#### should be turned into links:
http://example.com
https://example.com
www.example.com
www.example.com/index
www.example.com/index.html
www.example.com/index/sub
www.example.com/index?params=1
www.example.com/index?params=1&other=2
www.example.com/index?params=1;other=2
http://example.com:8065
<http://example.com>
<www.example.com>
http://www.example.com/_/page
www.example.com/_/page
https://en.wikipedia.org/wiki/🐬
https://en.wikipedia.org/wiki/Rendering_(computer_graphics)
http://127.0.0.1
http://192.168.1.1:4040
http://[::1]:80
http://[::1]:8065
https://[::1]:80
http://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:80
http://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:8065
https://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:443
http://username:password@example.com
http://username:password@127.0.0.1
http://username:password@[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:80
test@example.com
#### should be turned into links which link to the correct place:
[example link](example.com) links to ` + "`" + `http://example.com` + "`" + `
[example.com](example.com) links to ` + "`" + `http://example.com` + "`" + `
[example.com/other](example.com) links to ` + "`" + `http://example.com` + "`" + `
[example.com/other_link](example.com/example) links to ` + "`" + `http://example.com/example` + "`" + `
www.example.com links to ` + "`" + `http://www.example.com` + "`" + `
https://example.com links to ` + "`" + `https://example.com` + "`" + `and not ` + "`" + `http://example.com` + "`" + `
https://en.wikipedia.org/wiki/🐬 links to the Wikipedia article on dolphins
https://en.wikipedia.org/wiki/URLs#Syntax links to the Syntax section of the Wikipedia article on URLs
test@example.com links to ` + "`" + `mailto:test@example.com` + "`" + `
[email link](mailto:test@example.com) links to ` + "`" + `mailto:test@example.com` + "`" + `and not ` + "`" + `http://mailto:test@example.com` + "`" + `
[other link](ts3server://example.com) links to ` + "`" + `ts3server://example.com` + "`" + `and not ` + "`" + `http://ts3server://example.com` + "`" + `
#### should not be turned into links:
example.com
readme.md
<example.com>
http://
@example.com
#### should only turn the actual link into a link and not change surrounding text
(http://example.com)
(test@example.com)
This is a sentence with a http://example.com in it.
This is a sentence with a [link](http://example.com) in it.
This is a sentence with a http://example.com/_/underscore in it.
This is a sentence with a link (http://example.com) in it.
This is a sentence with a (https://en.wikipedia.org/wiki/Rendering_(computer_graphics)) in it.
This is a sentence with a http://192.168.1.1:4040 in it.
This is a sentence with a https://::1 in it.
This is a link to http://example.com.
`,
"*", "?", ".", "}{][)(><", "{}[]()<>",
"qahwah ( قهوة)",
"שָׁלוֹם עֲלֵיכֶם",
"Ramen チャーシュー chāshū",
"言而无信",
"Ṫ͌ó̍ ̍͂̓̍̍̀i̊ͯ͒",
"& < &qu",
"' or '1'='1' -- ",
"' or '1'='1' ({ ",
"' or '1'='1' /* ",
"1;DROP TABLE users",
"<b><i><u><strong><em>",
"sue@thatmightbe",
"sue@thatmightbe.",
"sue@thatmightbe.c",
"sue@thatmightbe.co",
"su+san@thatmightbe.com",
"a@b.中国",
"1@2.am",
"a@b.co.uk",
"a@b.cancerresearch",
"local@[127.0.0.1]",
"!@$%^&:*.,/|;'\"+=?`~#",
"'\"/\\\"\"''\\/",
"gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg",
"gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg",
"ą ć ę ł ń ó ś ź ż č ď ě ň ř š ť ž ă î ø å æ á é í ó ú Ç Ğ İ Ö Ş Ü",
"abcdefghijklmnopqrstuvwrxyz0123456789 -_",
"Ṫ͌ó̍ ̍͂̓̍̍̀i̊ͯ͒nͧ̍̓̃͋vok̂̓ͤ̓̂ěͬ ͆tͬ̐́̐͆h̒̏͌̓e͂ ̎̊h̽͆ͯ̄ͮi͊̂ͧͫ̇̃vͥͦ́ẻͤ-͒m̈́̀i̓ͮ͗̑͌̆̅n̓̓ͨd̊̑͛̔̚ ͨͮ̊̾rͪeͭͭ͑ͧ́͋p̈́̅̚rͧe̒̈̌s̍̽ͩ̓̇e͗n̏͊ͬͭtͨ͆ͤ̚iͪ͗̍n͐͒g̾ͦ̎ ͥ͌̽̊ͩͥ͗c̀ͬͣha̍̏̉ͪ̈̚o̊̏s̊̋̀̏̽̚.͒ͫ͛͛̎ͥ",
"H҉̵̞̟̠̖̗̘Ȅ̐̑̒̚̕̚ IS C̒̓̔̿̿̿̕̚̚̕̚̕̚̕̚̕̚̕̚OMI҉̵̞̟̠̖̗̘NG > ͡҉҉ ̵̡̢̛̗̘̙̜̝̞̟̠͇̊̋̌̍̎̏̿̿̿̚ ҉ ҉҉̡̢̡̢̛̛̖̗̘̙̜̝̞̟̠̖̗̘̙̜̝̞̟̠̊̋̌̍̎̏̐̑̒̓̔̊̋̌̍̎̏̐̑ ͡҉҉",
"<a href=\"//www.google.com\">Teh Googles</a>",
"<img src=\"//upload.wikimedia.org/wikipedia/meta/b/be/Wikipedia-logo-v2_2x.png\" />",
"& < " '",
" %21 %23 %24 %26 %27 %28 %29 %2A %2B %2C %2F %3A %3B %3D %3F %40 %5B %5D %0D %0A %0D%0A %20 %22 %25 %2D %2E %3C %3E %5C %5E %5F %60 %7B %7C %7D %7E",
";alert('Well this is awkward.');",
"<script type='text/javascript'>alert('yay puppies');</script>",
"http?q=foobar%0d%0aContent-\nLength:%200%0d%0a%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-\nType:%20text/html%0d%0aContent-Length:%2019%0d%0a%0d%0a<html>Shazam</html>",
"apos'trophe@thatmightbe.com",
"apos''''trophe@thatmightbe.com",
"su+s+an@thatmightbe.com",
"per.iod@thatmightbe.com",
"per..iods@thatmightbe.com",
".period@thatmightbe.com",
"tom(comment)@thatmightbe.com",
"(comment)tom@thatmightbe.com",
"\"quotes\"@thatmightbe.com",
"\"\\\"(),:;<>@[\\]\"@thatmightbe.com",
"a!#$%&'*+-/=?^_`{|}~b@thatmightbe.com",
"jill@(comment)example.com",
"jill@example.com(comment)",
"ben@ggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg.com",
"judy@gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg.com",
"ggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.com",
}
// Strings that should pass as acceptable team names
var FuzzyStringsNames = []string{
"*",
"?",
".",
"}{][)(><",
"{}[]()<>",
"qahwah ( قهوة)",
"שָׁלוֹם עֲלֵיכֶם",
"Ramen チャーシュー chāshū",
"言而无信",
"Ṫ͌ó̍ ̍͂̓̍̍̀i̊ͯ͒",
"& < &qu",
"' or '1'='1' -- ",
"' or '1'='1' ({ ",
"' or '1'='1' /* ",
"1;DROP TABLE users",
"<b><i><u><strong><em>",
"sue@thatmightbe",
"sue@thatmightbe.",
"sue@thatmightbe.c",
"sue@thatmightbe.co",
"sue @ thatmightbe.com",
"apos'trophe@thatmightbe.com",
"apos''''trophe@thatmightbe.com",
"su+san@thatmightbe.com",
"su+s+an@thatmightbe.com",
"per.iod@thatmightbe.com",
"per..iods@thatmightbe.com",
".period@thatmightbe.com",
"tom(comment)@thatmightbe.com",
"(comment)tom@thatmightbe.com",
"\"quotes\"@thatmightbe.com",
"\"\\\"(),:;<>@[\\]\"@thatmightbe.com",
"a!#$%&'*+-/=?^_`{|}~b@thatmightbe.com",
"local@[127.0.0.1]",
"jill@(comment)example.com",
"jill@example.com(comment)",
"a@b.中国",
"1@2.am",
"a@b.co.uk",
"a@b.cancerresearch",
"<a href=\"//www.google.com\">Teh Googles</a>",
"<img src=\"//upload.wikimedia.org/wikipelogo-v2_2x.png\" />",
"<b><i><u><strong><em>",
"& < " '",
";alert('Well this is awkward.');",
"<script type='text/javascript'>alert('yay puppies');</script>",
"Ṫ͌ó̍ ̍͂̓̍̍̀i̊ͯ͒nͧ̍̓̃͋v",
"H҉̵̞̟̠̖̗̘Ȅ̐̐̑̒̚OMI҉̵̞̟̠",
}
// Strings that should pass as acceptable emails
var FuzzyStringsEmails = []string{
"sue@thatmightbe",
"sue@thatmightbe.c",
"sue@thatmightbe.co",
"su+san@thatmightbe.com",
"1@2.am",
"a@b.co.uk",
"a@b.cancerresearch",
"su+s+an@thatmightbe.com",
"per.iod@thatmightbe.com",
}
// Lovely giberish for all to use
const GibberishText = `
Thus one besides much goodness shyly far some hyena overtook since rhinoceros nodded withdrew wombat before deserved apart a alongside the far dalmatian less ouch where yet a salmon.
Then jeez far marginal hey aboard more as leaned much oversold that inside spoke showed much went crud close save so and and after and informally much lion commendably less conductive oh excepting conductive compassionate jeepers hey a much leopard alas woolly untruthful outside snug rashly one cunning past fabulous adjusted far woodchuck and and indecisive crud loving exotic less resolute ladybug sprang drank under following far the as hence passably stolidly jeez the inset spaciously more cozily fishily the hey alas petted one audible yikes dear preparatory darn goldfinch gosh a then as moth more guinea.
Timid mislaid as salamander yikes alas ouch much that goldfinch shark in before instead dear one swore vivid versus one until regardless sang panther tolerable much preparatory hardily shuddered where coquettish far sheep coarsely exaggerated preparatory because cordial awesome gradually nutria that dear mocking behind off staunchly regarding a the komodo crud shrewd well jeez iguanodon strove strived and moodily and sought and and mounted gosh aboard crud spitefully boa.
One as highhanded fortuitous angelfish so one woodchuck dazedly kangaroo nasty instead far parrot away the worm yet testy where caribou a cuckoo onto dear reined because less tranquil kindhearted and shuddered plankton astride monkey methodically above evasive otter this wrung and courageous iguana wayward along cowered prior a.
Freely since ouch octopus the heated apart on hey the some pending placed fearless jeepers hardheadedly more that less jolly bit cuddled.
Caterpillar laboriously far wistful spilled aside far oriole newt and immeasurably yikes revealed raptly obdurately definitely scallop titilatingly one alongside monumentally ouch much wretched the spoke a before alas insolent abortive that turned hey hare much poignantly re-laid goodness yet the dear compassionate a hey scooped sped darn warmly oh and more darn craven that overtook fell and bluebird misheard that needless less ravenously in positively far romantically some babbled that rose honey then immaturely this and jollily irresistible much rarely earthworm parrot wow.
Less less bluntly jeez at goodness panther opposite oh purred a pathetically mildly less cat badly much much on from obscure in gull off manatee hatchet goodness euphemistically hence or understandable after this so that thus shook hence that mindfully yellow behind far bat wayward thanks more wrote so the flapped however alas and mallard that temperately irritably yikes squirrel.
Some reset some therefore demonstrably considering dachshund kindhearted far wow far whispered far clung this by partook much upon fit inscrutably so affirmative diligently far grinned and manifestly hummingbird hello caudal considering when aboard much buoyantly that unfitting far attractively far during much crud baneful jeez one toneless cynically oh spurious athletic meadowlark much generously one subconsciously arguable much forthrightly hawk inoffensively.
Snorted tidy stiffly against one fiendishly began burst hey revealed a beside the soothingly ceremonially affirmatively cowered when fitted this static hello emoted assenting however while far that gross besides because and dear.
Far therefore the blushed momentously the however one a wholeheartedly and considering incessantly that neurotically wore firefly grouped impotently dear one abjectly goodness so far a honey far insolently far so greyhound between above raucously echidna more halfhearted thankful squid one.
Raccoon cockatoo this while but this a far among ouch and hey alas scallop black sane as yikes hello sexy far tacky and balked wrongly more near shrewdly the yet gosh much caribou ruthlessly a on far a threw well less at the one after.
Spoke touched barbarously before much thus therefore darn scratched oh howled the less much hello after and jeez flagrantly weirdly crud komodo fabulous the much some cow jeering much egregiously a bucolically a admirably jeepers essential when ouch and tapir this while and wolverine.
Cm more much in this rewrote ouch on from aside wildebeest crane saddled where much opposite endearingly hummingbird together some beside a the goodness dear ouch ouch struck the input smooched shrugged until slick as waked hawk sincere irksomely.
Camel the pulled this richly grimaced leopard more false thought dear militant added yikes supp infallibly set orca beat hello while accurately reliably while lorikeet one strategic less hello without and smooched across plankton but jeepers pangolin the rich seal sneered pre-set lynx on radical nasty alas onto more hence flabby outbid murkily congenially dived much lubber added far eccentrically turtle before outsold onto ouch thus much and hawk tolerable much knitted yikes shot much limpet one this woolly much however hence up angry up well.
Unicorn yawned hello boundless this when express jaded closed wept tranquil after came airily merry much dismounted for much extensively less interminably far one far armadillo pled dolphin alas nutria and more oh positively koala grizzly after falcon goat strict hooted next browbeat split more far far antagonistic lingering the depending pending sheared since up before jeepers distant mastodon dropped as this more some much set far infinitesimal well shark grasshopper as hey one via some fishy and immaturely remote where weasel leopard annoying correctly wherever that sniffled much mandrill on jeez adventurous much.
Jeepers before spitefully buoyant concentric the reset moth a darn decidedly baboon giraffe outrageously groundhog on one at more overslept gosh worm away far far less much hysteric showed on so rattlesnake the and immature yikes baneful hence wow lynx hence past scornfully groaned pounded dived this one outside dachshund scowled one prior tenable therefore before scratched much much drank hey while added rabbit shark and supp cut this ironic limpet hedgehog bound more rebuking the jeepers thorough while more far due but yikes nastily brave dangerous opened tangibly aside after acrimoniously one cackled scratched.
Canny salmon hatchet more far opposite much coughed excited expedient far lizard one indiscriminate yikes jeez powerlessly forcefully tiger rooster and brought far more during this sank onto after then less amorally rude unerring some alongside irrespective bat hungrily kangaroo extravagantly inside ouch much gosh dreadfully oh much darn prior as fired guinea.
Irksomely upon up for amicably one since contrary one until flamingo tarantula far koala despite easy well gazelle ungracefully rose less that under hey more criminal unique furrowed so disbanded normal where one a a hey circuitous ouch feverish for the kookaburra and pithy far far then more the versus cliquishly across oh and explicitly much therefore as tamely alongside underlay much yikes imminently off however far across instantaneous therefore wallaby evidently foul foretold as far a jeepers invidious bearish.
More and until scandalously after wallaby petted oh much as poked much caterpillar drank beside rode actively walking scooped weird this duteous that far before human during dear house thrust more flinched opposite that ahead in far.
The painful essential jeepers merrily proudly essential and less far dismounted inside mongoose beyond confessedly robin shined heron the during since according suggestively and less some strident combed alas much man-of-war forgave so and to then inanimately.
Beside far this this a crud polite cantankerous exclusively misheard pled far circuitously and frugal less more temperately gauche goldfinch oh against this along excitedly goodhearted more classically quit serenely outside vulture ouch after one a this yet.
Less and handsomely manatee some amidst much reined komodo busted exultingly but fatuously less across mighty goodness objective alas glaringly gregariously hello the since one pridefully much well placed far less goodness jellyfish unnecessary reciprocating a far stylistic gazed one.
Hey rethought excepting lamely much and naughtily amidst more since jeez then bluebird hence less bald by some brought left the across logic loyal brightly jeez capitally that less more forward rebound a yikes chose convulsively confidently repeated broadcast much dipped when awesomely or some some regal the scowled merry zebra since more credible so inescapably fetchingly and lantern that due dear one went gosh wow well furrowed much much specially spoiled as vitally instead the seriously some rooster irrespective well imprecisely rapidly more llama.
Up to and hey without pill that this squid alas brusque on inventoried and spread the more excepting aristocratically due piquant wove beneath that macaw in more until much grimaced far and jeez enticingly unicorn some far crab more barring purely jeepers clear groomed glaring hey dear hence before the this hello.`
func RandString(l int, charset string) string {
ret := make([]byte, l)
for i := 0; i < l; i++ {
ret[i] = charset[rand.Intn(len(charset))]
}
return string(ret)
}
// func RandomEmail(length Range, charset string) string {
// emaillen := RandIntFromRange(length)
// username := RandString(emaillen, charset)
// domain := "simulator.amazonses.com"
// return "success+" + username + "@" + domain
// }
// func FuzzEmail() string {
// return FuzzyStringsEmails[RandIntFromRange(Range{0, len(FuzzyStringsEmails) - 1})]
// }
func RandomName(length Range, charset string) string {
namelen := RandIntFromRange(length)
return RandString(namelen, charset)
}
func FuzzName() string {
return FuzzyStringsNames[RandIntFromRange(Range{0, len(FuzzyStringsNames) - 1})]
}
// Random selection of text for post
func RandomText(length Range, hashtags Range, mentions Range, users []string) string {
textLength := RandIntFromRange(length)
numHashtags := RandIntFromRange(hashtags)
numMentions := RandIntFromRange(mentions)
if textLength > len(GibberishText) || textLength < 0 {
textLength = len(GibberishText)
}
startPosition := RandIntFromRange(Range{0, len(GibberishText) - textLength - 1})
words := strings.Split(GibberishText[startPosition:startPosition+textLength], " ")
for i := 0; i < numHashtags; i++ {
randword := RandIntFromRange(Range{0, len(words) - 1})
words = append(words, " #"+words[randword])
}
if len(users) > 0 {
for i := 0; i < numMentions; i++ {
randuser := RandIntFromRange(Range{0, len(users) - 1})
words = append(words, " @"+users[randuser])
}
}
// Shuffle the words
for i := range words {
j := rand.Intn(i + 1)
words[i], words[j] = words[j], words[i]
}
return strings.Join(words, " ")
}
func FuzzPost() string {
return FuzzyStringsPosts[RandIntFromRange(Range{0, len(FuzzyStringsPosts) - 1})]
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"time"
)
func MillisFromTime(t time.Time) int64 {
return t.UnixNano() / int64(time.Millisecond)
}
func TimeFromMillis(millis int64) time.Time {
return time.Unix(0, millis*int64(time.Millisecond))
}
func StartOfDay(t time.Time) time.Time {
year, month, day := t.Date()
return time.Date(year, month, day, 0, 0, 0, 0, t.Location())
}
func EndOfDay(t time.Time) time.Time {
year, month, day := t.Date()
return time.Date(year, month, day, 23, 59, 59, 999999999, t.Location())
}
func Yesterday() time.Time {
return time.Now().AddDate(0, 0, -1)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"time"
)
const trueUpReviewDueDay = 15
const day = time.Hour * 24
type DueDateWindow struct {
Start time.Time
End time.Time
}
func GetNextTrueUpReviewDueDate(now time.Time) time.Time {
nowYear := now.Year()
nowMonth := now.Month()
nowDay := now.Day()
finalQuarterYear := nowYear
if nowMonth >= time.October && nowMonth <= time.December {
finalQuarterYear = nowYear + 1
}
trueUpSubmissionWindows := []DueDateWindow{
{
Start: time.Date(now.Year(), time.January, 16, 0, 0, 0, 0, now.Location()),
End: time.Date(now.Year(), time.April, 15, 0, 0, 0, 0, now.Location()),
},
{
Start: time.Date(now.Year(), time.April, 16, 0, 0, 0, 0, now.Location()),
End: time.Date(now.Year(), time.July, 15, 0, 0, 0, 0, now.Location()),
},
{
Start: time.Date(now.Year(), time.July, 16, 0, 0, 0, 0, now.Location()),
End: time.Date(now.Year(), time.October, 15, 0, 0, 0, 0, now.Location()),
},
{
Start: time.Date(now.Year(), time.October, 16, 0, 0, 0, 0, now.Location()),
End: time.Date(finalQuarterYear, time.January, 15, 0, 0, 0, 0, now.Location()),
},
}
for _, window := range trueUpSubmissionWindows {
withinWindow := false
// Our due dates "wrap" around (i.e. can go into the next year), so we'll need to check the months different. Since January = 1 and December = 12, the checks
// for the current month being greater or equal to the start month and less than or equal to the end month will not work.
if window.End.Month() == time.January {
withinWindow = (nowMonth != time.January && nowMonth >= window.Start.Month()) || nowMonth == window.End.Month()
} else {
withinWindow = nowMonth >= window.Start.Month() && nowMonth <= window.End.Month()
}
// Only check the days if the current month is equal to the start or end months.
// The dates of the middle month(s) don't matter so much.
isFirstMonth := nowMonth == window.Start.Month()
if isFirstMonth {
withinWindow = withinWindow && nowDay >= window.Start.Day()
}
isFinalMonth := nowMonth == window.End.Month()
if isFinalMonth {
withinWindow = withinWindow && nowDay <= window.End.Day()
}
if withinWindow {
return window.End
}
}
return trueUpSubmissionWindows[0].End
}
func IsTrueUpReviewDueDateWithinTheNext30Days(now time.Time, dueDate time.Time) bool {
dueDateWindow := dueDate.Add(-day * 30)
if now.Before(dueDateWindow) || now.After(dueDate) {
return false
}
return true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"net/url"
"strings"
)
func URLEncode(str string) string {
strs := strings.Split(str, " ")
for i, s := range strs {
strs[i] = url.QueryEscape(s)
}
return strings.Join(strs, "%20")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"io"
"math"
"net"
"net/http"
"net/url"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
)
func StringInSlice(a string, slice []string) bool {
for _, b := range slice {
if b == a {
return true
}
}
return false
}
// RemoveStringFromSlice removes the first occurrence of a from slice.
func RemoveStringFromSlice(a string, slice []string) []string {
for i, str := range slice {
if str == a {
return append(slice[:i], slice[i+1:]...)
}
}
return slice
}
// RemoveStringsFromSlice removes all occurrences of strings from slice.
func RemoveStringsFromSlice(slice []string, strings ...string) []string {
newSlice := []string{}
for _, item := range slice {
if !StringInSlice(item, strings) {
newSlice = append(newSlice, item)
}
}
return newSlice
}
func StringArrayIntersection(arr1, arr2 []string) []string {
arrMap := map[string]bool{}
result := []string{}
for _, value := range arr1 {
arrMap[value] = true
}
for _, value := range arr2 {
if arrMap[value] {
result = append(result, value)
}
}
return result
}
func RemoveDuplicatesFromStringArray(arr []string) []string {
result := make([]string, 0, len(arr))
seen := make(map[string]bool)
for _, item := range arr {
if !seen[item] {
result = append(result, item)
seen[item] = true
}
}
return result
}
func StringSliceDiff(a, b []string) []string {
m := make(map[string]bool)
result := []string{}
for _, item := range b {
m[item] = true
}
for _, item := range a {
if !m[item] {
result = append(result, item)
}
}
return result
}
func GetIPAddress(r *http.Request, trustedProxyIPHeader []string) string {
address := ""
for _, proxyHeader := range trustedProxyIPHeader {
header := r.Header.Get(proxyHeader)
if header != "" {
addresses := strings.Split(header, ",")
if len(addresses) > 0 {
address = strings.TrimSpace(addresses[0])
}
}
if address != "" {
return address
}
}
if address == "" {
address, _, _ = net.SplitHostPort(r.RemoteAddr)
}
return address
}
func GetHostnameFromSiteURL(siteURL string) string {
u, err := url.Parse(siteURL)
if err != nil {
return ""
}
return u.Hostname()
}
type RequestCache struct {
Data []byte
Date string
Key string
}
// Fetch JSON data from the notices server
// if skip is passed, does a fetch without touching the cache
func GetURLWithCache(url string, cache *RequestCache, skip bool) ([]byte, error) {
// Build a GET Request, including optional If-None-Match header.
req, err := http.NewRequest("GET", url, nil)
if err != nil {
cache.Data = nil
return nil, err
}
if !skip && cache.Data != nil {
req.Header.Add("If-None-Match", cache.Key)
req.Header.Add("If-Modified-Since", cache.Date)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
cache.Data = nil
return nil, err
}
defer resp.Body.Close()
// No change from latest known Etag?
if resp.StatusCode == http.StatusNotModified {
return cache.Data, nil
}
if resp.StatusCode != 200 {
cache.Data = nil
return nil, errors.Errorf("Fetching notices failed with status code %d", resp.StatusCode)
}
cache.Data, err = io.ReadAll(resp.Body)
if err != nil {
cache.Data = nil
return nil, err
}
// If etags headers are missing, ignore.
cache.Key = resp.Header.Get("ETag")
cache.Date = resp.Header.Get("Date")
return cache.Data, err
}
// Append tokens to passed baseURL as query params
func AppendQueryParamsToURL(baseURL string, params map[string]string) string {
u, err := url.Parse(baseURL)
if err != nil {
return ""
}
q, err := url.ParseQuery(u.RawQuery)
if err != nil {
return ""
}
for key, value := range params {
q.Add(key, value)
}
u.RawQuery = q.Encode()
return u.String()
}
// Validates RedirectURL passed during OAuth or SAML
func IsValidWebAuthRedirectURL(config *model.Config, redirectURL string) bool {
u, err := url.Parse(redirectURL)
if err == nil && (u.Scheme == "http" || u.Scheme == "https") {
if config.ServiceSettings.SiteURL != nil {
siteURL := *config.ServiceSettings.SiteURL
return strings.Index(strings.ToLower(redirectURL), strings.ToLower(siteURL)) == 0
}
return false
}
return true
}
// Validates Mobile Custom URL Scheme passed during OAuth or SAML
func IsValidMobileAuthRedirectURL(config *model.Config, redirectURL string) bool {
for _, URLScheme := range config.NativeAppSettings.AppCustomURLSchemes {
if strings.Index(strings.ToLower(redirectURL), strings.ToLower(URLScheme)) == 0 {
return true
}
}
return false
}
// RoundOffToZeroes converts all digits to 0 except the 1st one.
// Special case: If there is only 1 digit, then returns 0.
func RoundOffToZeroes(n float64) int64 {
if n >= -9 && n <= 9 {
return 0
}
zeroes := int(math.Log10(math.Abs(n)))
tens := int64(math.Pow10(zeroes))
firstDigit := int64(n) / tens
return firstDigit * tens
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
// RoundOffToZeroesResolution truncates off at most minResolution zero places.
// It implicitly sets the lowest minResolution to 0.
// e.g. 0 reports 1s, 1 reports 10s, 2 reports 100s, 3 reports 1000s
func RoundOffToZeroesResolution(n float64, minResolution int) int64 {
resolution := max(0, minResolution)
if n >= -9 && n <= 9 {
if resolution == 0 {
return int64(n)
}
return 0
}
zeroes := int(math.Log10(math.Abs(n)))
resolution = min(zeroes, resolution)
tens := int64(math.Pow10(resolution))
significantDigits := int64(n) / tens
return significantDigits * tens
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"net/http"
"path"
"regexp"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type Context struct {
App app.AppIface
AppContext *request.Context
Logger *mlog.Logger
Params *Params
Err *model.AppError
// This is used to track the graphQL query that's being executed,
// so that we can monitor the timings in Grafana.
GraphQLOperationName string
siteURLHeader string
}
// LogAuditRec logs an audit record using default LevelAPI.
func (c *Context) LogAuditRec(rec *audit.Record) {
c.LogAuditRecWithLevel(rec, app.LevelAPI)
}
// LogAuditRec logs an audit record using specified Level.
// If the context is flagged with a permissions error then `level`
// is ignored and the audit record is emitted with `LevelPerms`.
func (c *Context) LogAuditRecWithLevel(rec *audit.Record, level mlog.Level) {
if rec == nil {
return
}
if c.Err != nil {
rec.AddErrorCode(c.Err.StatusCode)
rec.AddErrorDesc(c.Err.Error())
if c.Err.Id == "api.context.permissions.app_error" {
level = app.LevelPerms
}
rec.Fail()
}
c.App.Srv().Audit.LogRecord(level, *rec)
}
// MakeAuditRecord creates a audit record pre-populated with data from this context.
func (c *Context) MakeAuditRecord(event string, initialStatus string) *audit.Record {
rec := &audit.Record{
EventName: event,
Status: initialStatus,
Actor: audit.EventActor{
UserId: c.AppContext.Session().UserId,
SessionId: c.AppContext.Session().Id,
Client: c.AppContext.UserAgent(),
IpAddress: c.AppContext.IPAddress(),
},
Meta: map[string]interface{}{
audit.KeyAPIPath: c.AppContext.Path(),
audit.KeyClusterID: c.App.GetClusterId(),
},
EventData: audit.EventData{
Parameters: map[string]interface{}{},
PriorState: map[string]interface{}{},
ResultState: map[string]interface{}{},
ObjectType: "",
},
}
return rec
}
func (c *Context) LogAudit(extraInfo string) {
audit := &model.Audit{UserId: c.AppContext.Session().UserId, IpAddress: c.AppContext.IPAddress(), Action: c.AppContext.Path(), ExtraInfo: extraInfo, SessionId: c.AppContext.Session().Id}
if err := c.App.Srv().Store().Audit().Save(audit); err != nil {
appErr := model.NewAppError("LogAudit", "app.audit.save.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
c.LogErrorByCode(appErr)
}
}
func (c *Context) LogAuditWithUserId(userId, extraInfo string) {
if c.AppContext.Session().UserId != "" {
extraInfo = strings.TrimSpace(extraInfo + " session_user=" + c.AppContext.Session().UserId)
}
audit := &model.Audit{UserId: userId, IpAddress: c.AppContext.IPAddress(), Action: c.AppContext.Path(), ExtraInfo: extraInfo, SessionId: c.AppContext.Session().Id}
if err := c.App.Srv().Store().Audit().Save(audit); err != nil {
appErr := model.NewAppError("LogAuditWithUserId", "app.audit.save.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
c.LogErrorByCode(appErr)
}
}
func (c *Context) LogErrorByCode(err *model.AppError) {
code := err.StatusCode
msg := err.SystemMessage(i18n.TDefault)
fields := []mlog.Field{
mlog.String("err_where", err.Where),
mlog.Int("http_code", err.StatusCode),
mlog.String("error", err.Error()),
}
switch {
case (code >= http.StatusBadRequest && code < http.StatusInternalServerError) ||
err.Id == "web.check_browser_compatibility.app_error":
c.Logger.Debug(msg, fields...)
case code == http.StatusNotImplemented:
c.Logger.Info(msg, fields...)
default:
c.Logger.Error(msg, fields...)
}
}
func (c *Context) IsSystemAdmin() bool {
return c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem)
}
func (c *Context) SessionRequired() {
if !*c.App.Config().ServiceSettings.EnableUserAccessTokens &&
c.AppContext.Session().Props[model.SessionPropType] == model.SessionTypeUserAccessToken &&
c.AppContext.Session().Props[model.SessionPropIsBot] != model.SessionPropIsBotValue {
c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "UserAccessToken", http.StatusUnauthorized)
return
}
if c.AppContext.Session().UserId == "" {
c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "UserRequired", http.StatusUnauthorized)
return
}
}
func (c *Context) CloudKeyRequired() {
if license := c.App.Channels().License(); license == nil || !license.IsCloud() || c.AppContext.Session().Props[model.SessionPropType] != model.SessionTypeCloudKey {
c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "TokenRequired", http.StatusUnauthorized)
return
}
}
func (c *Context) RemoteClusterTokenRequired() {
if license := c.App.Channels().License(); license == nil || !license.HasRemoteClusterService() || c.AppContext.Session().Props[model.SessionPropType] != model.SessionTypeRemoteclusterToken {
c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "TokenRequired", http.StatusUnauthorized)
return
}
}
func (c *Context) MfaRequired() {
// Must be licensed for MFA and have it configured for enforcement
if license := c.App.Channels().License(); license == nil || !*license.Features.MFA || !*c.App.Config().ServiceSettings.EnableMultifactorAuthentication || !*c.App.Config().ServiceSettings.EnforceMultifactorAuthentication {
return
}
// OAuth integrations are excepted
if c.AppContext.Session().IsOAuth {
return
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("MfaRequired", "api.context.get_user.app_error", nil, "", http.StatusUnauthorized).Wrap(err)
return
}
if user.IsGuest() && !*c.App.Config().GuestAccountsSettings.EnforceMultifactorAuthentication {
return
}
// Only required for email and ldap accounts
if user.AuthService != "" &&
user.AuthService != model.UserAuthServiceEmail &&
user.AuthService != model.UserAuthServiceLdap {
return
}
// Special case to let user get themself
subpath, _ := utils.GetSubpathFromConfig(c.App.Config())
if c.AppContext.Path() == path.Join(subpath, "/api/v4/users/me") {
return
}
// Bots are exempt
if user.IsBot {
return
}
if !user.MfaActive {
c.Err = model.NewAppError("MfaRequired", "api.context.mfa_required.app_error", nil, "", http.StatusForbidden)
return
}
}
// ExtendSessionExpiryIfNeeded will update Session.ExpiresAt based on session lengths in config.
// Session cookies will be resent to the client with updated max age.
func (c *Context) ExtendSessionExpiryIfNeeded(w http.ResponseWriter, r *http.Request) {
if ok := c.App.ExtendSessionExpiryIfNeeded(c.AppContext.Session()); ok {
c.App.AttachSessionCookies(c.AppContext, w, r)
}
}
func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) {
subpath, _ := utils.GetSubpathFromConfig(c.App.Config())
cookie := &http.Cookie{
Name: model.SessionCookieToken,
Value: "",
Path: subpath,
MaxAge: -1,
HttpOnly: true,
}
http.SetCookie(w, cookie)
}
func (c *Context) SetInvalidParam(parameter string) {
c.Err = NewInvalidParamError(parameter)
}
func (c *Context) SetInvalidParamWithErr(parameter string, err error) {
c.Err = NewInvalidParamError(parameter).Wrap(err)
}
func (c *Context) SetInvalidURLParam(parameter string) {
c.Err = NewInvalidURLParamError(parameter)
}
func (c *Context) SetServerBusyError() {
c.Err = NewServerBusyError()
}
func (c *Context) SetInvalidRemoteIdError(id string) {
c.Err = NewInvalidRemoteIdError(id)
}
func (c *Context) SetInvalidRemoteClusterTokenError() {
c.Err = NewInvalidRemoteClusterTokenError()
}
func (c *Context) SetJSONEncodingError(err error) {
c.Err = NewJSONEncodingError(err)
}
func (c *Context) SetCommandNotFoundError() {
c.Err = model.NewAppError("GetCommand", "store.sql_command.save.get.app_error", nil, "", http.StatusNotFound)
}
func (c *Context) HandleEtag(etag string, routeName string, w http.ResponseWriter, r *http.Request) bool {
metrics := c.App.Metrics()
if et := r.Header.Get(model.HeaderEtagClient); etag != "" {
if et == etag {
w.Header().Set(model.HeaderEtagServer, etag)
w.WriteHeader(http.StatusNotModified)
if metrics != nil {
metrics.IncrementEtagHitCounter(routeName)
}
return true
}
}
if metrics != nil {
metrics.IncrementEtagMissCounter(routeName)
}
return false
}
func NewInvalidParamError(parameter string) *model.AppError {
err := model.NewAppError("Context", "api.context.invalid_body_param.app_error", map[string]any{"Name": parameter}, "", http.StatusBadRequest)
return err
}
func NewInvalidURLParamError(parameter string) *model.AppError {
err := model.NewAppError("Context", "api.context.invalid_url_param.app_error", map[string]any{"Name": parameter}, "", http.StatusBadRequest)
return err
}
func NewServerBusyError() *model.AppError {
err := model.NewAppError("Context", "api.context.server_busy.app_error", nil, "", http.StatusServiceUnavailable)
return err
}
func NewInvalidRemoteIdError(parameter string) *model.AppError {
err := model.NewAppError("Context", "api.context.remote_id_invalid.app_error", map[string]any{"RemoteId": parameter}, "", http.StatusBadRequest)
return err
}
func NewInvalidRemoteClusterTokenError() *model.AppError {
err := model.NewAppError("Context", "api.context.remote_id_invalid.app_error", nil, "", http.StatusUnauthorized)
return err
}
func NewJSONEncodingError(err error) *model.AppError {
appErr := model.NewAppError("Context", "api.context.json_encoding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return appErr
}
func (c *Context) SetPermissionError(permissions ...*model.Permission) {
c.Err = c.App.MakePermissionError(c.AppContext.Session(), permissions)
}
func (c *Context) SetSiteURLHeader(url string) {
c.siteURLHeader = strings.TrimRight(url, "/")
}
func (c *Context) GetSiteURLHeader() string {
return c.siteURLHeader
}
func (c *Context) RequireUserId() *Context {
if c.Err != nil {
return c
}
if c.Params.UserId == model.Me {
c.Params.UserId = c.AppContext.Session().UserId
}
if !model.IsValidId(c.Params.UserId) {
c.SetInvalidURLParam("user_id")
}
return c
}
func (c *Context) RequireTeamId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.TeamId) {
c.SetInvalidURLParam("team_id")
}
return c
}
func (c *Context) RequireCategoryId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidCategoryId(c.Params.CategoryId) {
c.SetInvalidURLParam("category_id")
}
return c
}
func (c *Context) RequireInviteId() *Context {
if c.Err != nil {
return c
}
if c.Params.InviteId == "" {
c.SetInvalidURLParam("invite_id")
}
return c
}
func (c *Context) RequireTokenId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.TokenId) {
c.SetInvalidURLParam("token_id")
}
return c
}
func (c *Context) RequireThreadId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.ThreadId) {
c.SetInvalidURLParam("thread_id")
}
return c
}
func (c *Context) RequireTimestamp() *Context {
if c.Err != nil {
return c
}
if c.Params.Timestamp == 0 {
c.SetInvalidURLParam("timestamp")
}
return c
}
func (c *Context) RequireChannelId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.ChannelId) {
c.SetInvalidURLParam("channel_id")
}
return c
}
func (c *Context) RequireUsername() *Context {
if c.Err != nil {
return c
}
if !model.IsValidUsername(c.Params.Username) {
c.SetInvalidParam("username")
}
return c
}
func (c *Context) RequirePostId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.PostId) {
c.SetInvalidURLParam("post_id")
}
return c
}
func (c *Context) RequirePolicyId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.PolicyId) {
c.SetInvalidURLParam("policy_id")
}
return c
}
func (c *Context) RequireAppId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.AppId) {
c.SetInvalidURLParam("app_id")
}
return c
}
func (c *Context) RequireFileId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.FileId) {
c.SetInvalidURLParam("file_id")
}
return c
}
func (c *Context) RequireUploadId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.UploadId) {
c.SetInvalidURLParam("upload_id")
}
return c
}
func (c *Context) RequireFilename() *Context {
if c.Err != nil {
return c
}
if c.Params.Filename == "" {
c.SetInvalidURLParam("filename")
}
return c
}
func (c *Context) RequirePluginId() *Context {
if c.Err != nil {
return c
}
if c.Params.PluginId == "" {
c.SetInvalidURLParam("plugin_id")
}
return c
}
func (c *Context) RequireReportId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.ReportId) {
c.SetInvalidURLParam("report_id")
}
return c
}
func (c *Context) RequireEmojiId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.EmojiId) {
c.SetInvalidURLParam("emoji_id")
}
return c
}
func (c *Context) RequireTeamName() *Context {
if c.Err != nil {
return c
}
if !model.IsValidTeamName(c.Params.TeamName) {
c.SetInvalidURLParam("team_name")
}
return c
}
func (c *Context) RequireChannelName() *Context {
if c.Err != nil {
return c
}
if !model.IsValidChannelIdentifier(c.Params.ChannelName) {
c.SetInvalidURLParam("channel_name")
}
return c
}
func (c *Context) SanitizeEmail() *Context {
if c.Err != nil {
return c
}
c.Params.Email = strings.ToLower(c.Params.Email)
if !model.IsValidEmail(c.Params.Email) {
c.SetInvalidURLParam("email")
}
return c
}
func (c *Context) RequireCategory() *Context {
if c.Err != nil {
return c
}
if !model.IsValidAlphaNumHyphenUnderscore(c.Params.Category, true) {
c.SetInvalidURLParam("category")
}
return c
}
func (c *Context) RequireService() *Context {
if c.Err != nil {
return c
}
if c.Params.Service == "" {
c.SetInvalidURLParam("service")
}
return c
}
func (c *Context) RequirePreferenceName() *Context {
if c.Err != nil {
return c
}
if !model.IsValidAlphaNumHyphenUnderscore(c.Params.PreferenceName, true) {
c.SetInvalidURLParam("preference_name")
}
return c
}
func (c *Context) RequireEmojiName() *Context {
if c.Err != nil {
return c
}
validName := regexp.MustCompile(`^[a-zA-Z0-9\-\+_]+$`)
if c.Params.EmojiName == "" || len(c.Params.EmojiName) > model.EmojiNameMaxLength || !validName.MatchString(c.Params.EmojiName) {
c.SetInvalidURLParam("emoji_name")
}
return c
}
func (c *Context) RequireHookId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.HookId) {
c.SetInvalidURLParam("hook_id")
}
return c
}
func (c *Context) RequireCommandId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.CommandId) {
c.SetInvalidURLParam("command_id")
}
return c
}
func (c *Context) RequireJobId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.JobId) {
c.SetInvalidURLParam("job_id")
}
return c
}
func (c *Context) RequireJobType() *Context {
if c.Err != nil {
return c
}
if c.Params.JobType == "" || len(c.Params.JobType) > 32 {
c.SetInvalidURLParam("job_type")
}
return c
}
func (c *Context) RequireRoleId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.RoleId) {
c.SetInvalidURLParam("role_id")
}
return c
}
func (c *Context) RequireSchemeId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.SchemeId) {
c.SetInvalidURLParam("scheme_id")
}
return c
}
func (c *Context) RequireRoleName() *Context {
if c.Err != nil {
return c
}
if !model.IsValidRoleName(c.Params.RoleName) {
c.SetInvalidURLParam("role_name")
}
return c
}
func (c *Context) RequireGroupId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.GroupId) {
c.SetInvalidURLParam("group_id")
}
return c
}
func (c *Context) RequireRemoteId() *Context {
if c.Err != nil {
return c
}
if c.Params.RemoteId == "" {
c.SetInvalidURLParam("remote_id")
}
return c
}
func (c *Context) RequireSyncableId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.SyncableId) {
c.SetInvalidURLParam("syncable_id")
}
return c
}
func (c *Context) RequireSyncableType() *Context {
if c.Err != nil {
return c
}
if c.Params.SyncableType != model.GroupSyncableTypeTeam && c.Params.SyncableType != model.GroupSyncableTypeChannel {
c.SetInvalidURLParam("syncable_type")
}
return c
}
func (c *Context) RequireBotUserId() *Context {
if c.Err != nil {
return c
}
if !model.IsValidId(c.Params.BotUserId) {
c.SetInvalidURLParam("bot_user_id")
}
return c
}
func (c *Context) RequireInvoiceId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.InvoiceId) != 27 && c.Params.InvoiceId != model.UpcomingInvoice {
c.SetInvalidURLParam("invoice_id")
}
return c
}
func (c *Context) GetRemoteID(r *http.Request) string {
return r.Header.Get(model.HeaderRemoteclusterId)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"bytes"
"context"
"fmt"
"net/http"
"os"
"reflect"
"runtime"
"strconv"
"strings"
"time"
"github.com/mattermost/gziphandler"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
spanlog "github.com/opentracing/opentracing-go/log"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
app_opentracing "github.com/mattermost/mattermost-server/v6/server/channels/app/opentracing"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store/opentracinglayer"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/services/tracing"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func GetHandlerName(h func(*Context, http.ResponseWriter, *http.Request)) string {
handlerName := runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name()
pos := strings.LastIndex(handlerName, ".")
if pos != -1 && len(handlerName) > pos {
handlerName = handlerName[pos+1:]
}
return handlerName
}
func (w *Web) NewHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
return &Handler{
Srv: w.srv,
HandleFunc: h,
HandlerName: GetHandlerName(h),
RequireSession: false,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
}
func (w *Web) NewStaticHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
// Determine the CSP SHA directive needed for subpath support, if any. This value is fixed
// on server start and intentionally requires a restart to take effect.
subpath, _ := utils.GetSubpathFromConfig(w.srv.Config())
return &Handler{
Srv: w.srv,
HandleFunc: h,
HandlerName: GetHandlerName(h),
RequireSession: false,
TrustRequester: false,
RequireMfa: false,
IsStatic: true,
cspShaDirective: utils.GetSubpathScriptHash(subpath),
}
}
type Handler struct {
Srv *app.Server
HandleFunc func(*Context, http.ResponseWriter, *http.Request)
HandlerName string
RequireSession bool
RequireCloudKey bool
RequireRemoteClusterToken bool
TrustRequester bool
RequireMfa bool
IsStatic bool
IsLocal bool
DisableWhenBusy bool
cspShaDirective string
}
func generateDevCSP(c Context) string {
var devCSP []string
// Add unsafe-eval to the content security policy for faster source maps in development mode
if model.BuildNumber == "dev" {
devCSP = append(devCSP, "'unsafe-eval'")
}
// Add unsafe-inline to unlock extensions like React & Redux DevTools in Firefox
// see https://github.com/reduxjs/redux-devtools/issues/380
if model.BuildNumber == "dev" {
devCSP = append(devCSP, "'unsafe-inline'")
}
// Add supported flags for debugging during development, even if not on a dev build.
if *c.App.Config().ServiceSettings.DeveloperFlags != "" {
for _, devFlagKVStr := range strings.Split(*c.App.Config().ServiceSettings.DeveloperFlags, ",") {
devFlagKVSplit := strings.SplitN(devFlagKVStr, "=", 2)
if len(devFlagKVSplit) != 2 {
c.Logger.Warn("Unable to parse developer flag", mlog.String("developer_flag", devFlagKVStr))
continue
}
devFlagKey := devFlagKVSplit[0]
devFlagValue := devFlagKVSplit[1]
// Ignore disabled keys
if devFlagValue != "true" {
continue
}
// Honour only supported keys
switch devFlagKey {
case "unsafe-eval", "unsafe-inline":
if model.BuildNumber == "dev" {
// These flags are added automatically for dev builds
continue
}
devCSP = append(devCSP, "'"+devFlagKey+"'")
default:
c.Logger.Warn("Unrecognized developer flag", mlog.String("developer_flag", devFlagKVStr))
}
}
}
// Add flags for Webpack dev servers used by other products during development
if model.BuildNumber == "dev" {
boardsURL := os.Getenv("MM_BOARDS_DEV_SERVER_URL")
if boardsURL == "" {
// Focalboard runs on http://localhost:9006 by default
boardsURL = "http://localhost:9006"
}
devCSP = append(devCSP, boardsURL)
playbooksURL := os.Getenv("MM_PLAYBOOKS_DEV_SERVER_URL")
if playbooksURL == "" {
// Playbooks runs on http://localhost:9007 by default
playbooksURL = "http://localhost:9007"
}
devCSP = append(devCSP, playbooksURL)
}
if len(devCSP) == 0 {
return ""
}
return " " + strings.Join(devCSP, " ")
}
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w = newWrappedWriter(w)
now := time.Now()
appInstance := app.New(app.ServerConnector(h.Srv.Channels()))
requestID := model.NewId()
var statusCode string
defer func() {
responseLogFields := []mlog.Field{
mlog.String("method", r.Method),
mlog.String("url", r.URL.Path),
mlog.String("request_id", requestID),
}
// Websockets are returning status code 0 to requests after closing the socket
if statusCode != "0" {
responseLogFields = append(responseLogFields, mlog.String("status_code", statusCode))
}
mlog.Debug("Received HTTP request", responseLogFields...)
}()
c := &Context{
AppContext: &request.Context{},
App: appInstance,
}
t, _ := i18n.GetTranslationsAndLocaleFromRequest(r)
c.AppContext.SetT(t)
c.AppContext.SetRequestId(requestID)
c.AppContext.SetIPAddress(utils.GetIPAddress(r, c.App.Config().ServiceSettings.TrustedProxyIPHeader))
c.AppContext.SetUserAgent(r.UserAgent())
c.AppContext.SetAcceptLanguage(r.Header.Get("Accept-Language"))
c.AppContext.SetPath(r.URL.Path)
c.AppContext.SetContext(context.Background())
c.Params = ParamsFromRequest(r)
c.Logger = c.App.Log()
if *c.App.Config().ServiceSettings.EnableOpenTracing {
span, ctx := tracing.StartRootSpanByContext(context.Background(), "web:ServeHTTP")
carrier := opentracing.HTTPHeadersCarrier(r.Header)
_ = opentracing.GlobalTracer().Inject(span.Context(), opentracing.HTTPHeaders, carrier)
ext.HTTPMethod.Set(span, r.Method)
ext.HTTPUrl.Set(span, c.AppContext.Path())
ext.PeerAddress.Set(span, c.AppContext.IPAddress())
span.SetTag("request_id", c.AppContext.RequestId())
span.SetTag("user_agent", c.AppContext.UserAgent())
defer func() {
if c.Err != nil {
span.LogFields(spanlog.Error(c.Err))
ext.HTTPStatusCode.Set(span, uint16(c.Err.StatusCode))
ext.Error.Set(span, true)
}
span.Finish()
}()
c.AppContext.SetContext(ctx)
tmpSrv := *c.App.Srv()
tmpSrv.SetStore(opentracinglayer.New(c.App.Srv().Store(), ctx))
c.App.SetServer(&tmpSrv)
c.App = app_opentracing.NewOpenTracingAppLayer(c.App, ctx)
}
// Set the max request body size to be equal to MaxFileSize.
// Ideally, non-file request bodies should be smaller than file request bodies,
// but we don't have a clean way to identify all file upload handlers.
// So to keep it simple, we clamp it to the max file size.
// We add a buffer of bytes.MinRead so that file sizes close to max file size
// do not get cut off.
r.Body = http.MaxBytesReader(w, r.Body, *c.App.Config().FileSettings.MaxFileSize+bytes.MinRead)
subpath, _ := utils.GetSubpathFromConfig(c.App.Config())
siteURLHeader := app.GetProtocol(r) + "://" + r.Host + subpath
if c.App.Channels().License().IsCloud() {
siteURLHeader = *c.App.Config().ServiceSettings.SiteURL + subpath
}
c.SetSiteURLHeader(siteURLHeader)
w.Header().Set(model.HeaderRequestId, c.AppContext.RequestId())
w.Header().Set(model.HeaderVersionId, fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, c.App.ClientConfigHash(), c.App.Channels().License() != nil))
if *c.App.Config().ServiceSettings.TLSStrictTransport {
w.Header().Set("Strict-Transport-Security", fmt.Sprintf("max-age=%d", *c.App.Config().ServiceSettings.TLSStrictTransportMaxAge))
}
// Hardcoded sensible default values for these security headers. Feel free to override in proxy or ingress
w.Header().Set("Permissions-Policy", "")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Referrer-Policy", "no-referrer")
cloudCSP := ""
if c.App.Channels().License().IsCloud() || *c.App.Config().ServiceSettings.SelfHostedPurchase {
cloudCSP = " js.stripe.com/v3"
}
if h.IsStatic {
// Instruct the browser not to display us in an iframe unless is the same origin for anti-clickjacking
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
devCSP := generateDevCSP(*c)
// Set content security policy. This is also specified in the root.html of the webapp in a meta tag.
w.Header().Set("Content-Security-Policy", fmt.Sprintf(
"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com%s%s%s",
cloudCSP,
h.cspShaDirective,
devCSP,
))
} else {
// All api response bodies will be JSON formatted by default
w.Header().Set("Content-Type", "application/json")
if r.Method == "GET" {
w.Header().Set("Expires", "0")
}
}
token, tokenLocation := app.ParseAuthTokenFromRequest(r)
if token != "" && tokenLocation != app.TokenLocationCloudHeader && tokenLocation != app.TokenLocationRemoteClusterHeader {
session, err := c.App.GetSession(token)
defer c.App.ReturnSessionToPool(session)
if err != nil {
c.Logger.Info("Invalid session", mlog.Err(err))
if err.StatusCode == http.StatusInternalServerError {
c.Err = err
} else if h.RequireSession {
c.RemoveSessionCookie(w, r)
c.Err = model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token, http.StatusUnauthorized)
}
} else if !session.IsOAuth && tokenLocation == app.TokenLocationQueryString {
c.Err = model.NewAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token, http.StatusUnauthorized)
} else {
c.AppContext.SetSession(session)
}
// Rate limit by UserID
if c.App.Srv().RateLimiter != nil && c.App.Srv().RateLimiter.UserIdRateLimit(c.AppContext.Session().UserId, w) {
return
}
h.checkCSRFToken(c, r, token, tokenLocation, session)
} else if token != "" && c.App.Channels().License().IsCloud() && tokenLocation == app.TokenLocationCloudHeader {
// Check to see if this provided token matches our CWS Token
session, err := c.App.GetCloudSession(token)
if err != nil {
c.Logger.Warn("Invalid CWS token", mlog.Err(err))
c.Err = err
} else {
c.AppContext.SetSession(session)
}
} else if token != "" && c.App.Channels().License() != nil && c.App.Channels().License().HasRemoteClusterService() && tokenLocation == app.TokenLocationRemoteClusterHeader {
// Get the remote cluster
if remoteId := c.GetRemoteID(r); remoteId == "" {
c.Logger.Warn("Missing remote cluster id") //
c.Err = model.NewAppError("ServeHTTP", "api.context.remote_id_missing.app_error", nil, "", http.StatusUnauthorized)
} else {
// Check the token is correct for the remote cluster id.
session, err := c.App.GetRemoteClusterSession(token, remoteId)
if err != nil {
c.Logger.Warn("Invalid remote cluster token", mlog.Err(err))
c.Err = err
} else {
c.AppContext.SetSession(session)
}
}
}
c.Logger = c.App.Log().With(
mlog.String("path", c.AppContext.Path()),
mlog.String("request_id", c.AppContext.RequestId()),
mlog.String("ip_addr", c.AppContext.IPAddress()),
mlog.String("user_id", c.AppContext.Session().UserId),
mlog.String("method", r.Method),
)
c.AppContext.SetLogger(c.Logger)
if c.Err == nil && h.RequireSession {
c.SessionRequired()
}
if c.Err == nil && h.RequireMfa {
c.MfaRequired()
}
if c.Err == nil && h.DisableWhenBusy && c.App.Srv().Platform().Busy.IsBusy() {
c.SetServerBusyError()
}
if c.Err == nil && h.RequireCloudKey {
c.CloudKeyRequired()
}
if c.Err == nil && h.RequireRemoteClusterToken {
c.RemoteClusterTokenRequired()
}
if c.Err == nil && h.IsLocal {
// if the connection is local, RemoteAddr shouldn't have the
// shape IP:PORT (it will be "@" in Linux, for example)
isLocalOrigin := !strings.Contains(r.RemoteAddr, ":")
if *c.App.Config().ServiceSettings.EnableLocalMode && isLocalOrigin {
c.AppContext.SetSession(&model.Session{Local: true})
} else if !isLocalOrigin {
c.Err = model.NewAppError("", "api.context.local_origin_required.app_error", nil, "LocalOriginRequired", http.StatusUnauthorized)
}
}
if c.Err == nil {
h.HandleFunc(c, w, r)
}
// Handle errors that have occurred
if c.Err != nil {
c.Err.RequestId = c.AppContext.RequestId()
c.LogErrorByCode(c.Err)
// The locale translation needs to happen after we have logged it.
// We don't want the server logs to be translated as per user locale.
c.Err.Translate(c.AppContext.T)
c.Err.Where = r.URL.Path
// Block out detailed error when not in developer mode
if !*c.App.Config().ServiceSettings.EnableDeveloper {
c.Err.DetailedError = ""
}
// Sanitize all 5xx error messages in hardened mode
if *c.App.Config().ServiceSettings.ExperimentalEnableHardenedMode && c.Err.StatusCode >= 500 {
c.Err.Id = ""
c.Err.Message = "Internal Server Error"
c.Err.DetailedError = ""
c.Err.StatusCode = 500
c.Err.Where = ""
c.Err.IsOAuth = false
}
if IsAPICall(c.App, r) || IsWebhookCall(c.App, r) || IsOAuthAPICall(c.App, r) || r.Header.Get("X-Mobile-App") != "" {
w.WriteHeader(c.Err.StatusCode)
w.Write([]byte(c.Err.ToJSON()))
} else {
utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey())
}
if c.App.Metrics() != nil {
c.App.Metrics().IncrementHTTPError()
}
}
statusCode = strconv.Itoa(w.(*responseWriterWrapper).StatusCode())
if c.App.Metrics() != nil {
c.App.Metrics().IncrementHTTPRequest()
if r.URL.Path != model.APIURLSuffix+"/websocket" {
elapsed := float64(time.Since(now)) / float64(time.Second)
var endpoint string
if strings.HasPrefix(r.URL.Path, model.APIURLSuffixV5) {
// It's a graphQL query, so use the operation name.
endpoint = c.GraphQLOperationName
} else {
endpoint = h.HandlerName
}
c.App.Metrics().ObserveAPIEndpointDuration(endpoint, r.Method, statusCode, elapsed)
}
}
}
// checkCSRFToken performs a CSRF check on the provided request with the given CSRF token. Returns whether or not
// a CSRF check occurred and whether or not it succeeded.
func (h *Handler) checkCSRFToken(c *Context, r *http.Request, token string, tokenLocation app.TokenLocation, session *model.Session) (checked bool, passed bool) {
csrfCheckNeeded := session != nil && c.Err == nil && tokenLocation == app.TokenLocationCookie && !h.TrustRequester && r.Method != "GET"
csrfCheckPassed := false
if csrfCheckNeeded {
csrfHeader := r.Header.Get(model.HeaderCsrfToken)
if csrfHeader == session.GetCSRF() {
csrfCheckPassed = true
} else if r.Header.Get(model.HeaderRequestedWith) == model.HeaderRequestedWithXML {
// ToDo(DSchalla) 2019/01/04: Remove after deprecation period and only allow CSRF Header (MM-13657)
csrfErrorMessage := "CSRF Header check failed for request - Please upgrade your web application or custom app to set a CSRF Header"
sid := ""
userId := ""
if session != nil {
sid = session.Id
userId = session.UserId
}
fields := []mlog.Field{
mlog.String("path", r.URL.Path),
mlog.String("ip", r.RemoteAddr),
mlog.String("session_id", sid),
mlog.String("user_id", userId),
}
if *c.App.Config().ServiceSettings.ExperimentalStrictCSRFEnforcement {
c.Logger.Warn(csrfErrorMessage, fields...)
} else {
c.Logger.Debug(csrfErrorMessage, fields...)
csrfCheckPassed = true
}
}
if !csrfCheckPassed {
c.AppContext.SetSession(&model.Session{})
c.Err = model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token+" Appears to be a CSRF attempt", http.StatusUnauthorized)
}
}
return csrfCheckNeeded, csrfCheckPassed
}
// APIHandler provides a handler for API endpoints which do not require the user to be logged in order for access to be
// granted.
func (w *Web) APIHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
handler := &Handler{
Srv: w.srv,
HandleFunc: h,
HandlerName: GetHandlerName(h),
RequireSession: false,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
if *w.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// APIHandlerTrustRequester provides a handler for API endpoints which do not require the user to be logged in and are
// allowed to be requested directly rather than via javascript/XMLHttpRequest, such as site branding images or the
// websocket.
func (w *Web) APIHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
handler := &Handler{
Srv: w.srv,
HandleFunc: h,
HandlerName: GetHandlerName(h),
RequireSession: false,
TrustRequester: true,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
if *w.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// APISessionRequired provides a handler for API endpoints which require the user to be logged in in order for access to
// be granted.
func (w *Web) APISessionRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
handler := &Handler{
Srv: w.srv,
HandleFunc: h,
HandlerName: GetHandlerName(h),
RequireSession: true,
TrustRequester: false,
RequireMfa: true,
IsStatic: false,
IsLocal: false,
}
if *w.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"encoding/json"
"html"
"net/http"
"net/url"
"path/filepath"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (w *Web) InitOAuth() {
// API version independent OAuth 2.0 as a service provider endpoints
w.MainRouter.Handle("/oauth/authorize", w.APIHandlerTrustRequester(authorizeOAuthPage)).Methods("GET")
w.MainRouter.Handle("/oauth/authorize", w.APISessionRequired(authorizeOAuthApp)).Methods("POST")
w.MainRouter.Handle("/oauth/deauthorize", w.APISessionRequired(deauthorizeOAuthApp)).Methods("POST")
w.MainRouter.Handle("/oauth/access_token", w.APIHandlerTrustRequester(getAccessToken)).Methods("POST")
// API version independent OAuth as a client endpoints
w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/complete", w.APIHandler(completeOAuth)).Methods("GET")
w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/login", w.APIHandler(loginWithOAuth)).Methods("GET")
w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/mobile_login", w.APIHandler(mobileLoginWithOAuth)).Methods("GET")
w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/signup", w.APIHandler(signupWithOAuth)).Methods("GET")
// Old endpoints for backwards compatibility, needed to not break SSO for any old setups
w.MainRouter.Handle("/api/v3/oauth/{service:[A-Za-z0-9]+}/complete", w.APIHandler(completeOAuth)).Methods("GET")
w.MainRouter.Handle("/signup/{service:[A-Za-z0-9]+}/complete", w.APIHandler(completeOAuth)).Methods("GET")
w.MainRouter.Handle("/login/{service:[A-Za-z0-9]+}/complete", w.APIHandler(completeOAuth)).Methods("GET")
w.MainRouter.Handle("/api/v4/oauth_test", w.APISessionRequired(testHandler)).Methods("GET")
}
func testHandler(c *Context, w http.ResponseWriter, r *http.Request) {
ReturnStatusOK(w)
}
func authorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
var authRequest *model.AuthorizeRequest
err := json.NewDecoder(r.Body).Decode(&authRequest)
if err != nil || authRequest == nil {
c.SetInvalidParamWithErr("authorize_request", err)
return
}
if err := authRequest.IsValid(); err != nil {
c.Err = err
return
}
if c.AppContext.Session().IsOAuth {
c.SetPermissionError(model.PermissionEditOtherUsers)
c.Err.DetailedError += ", attempted access by oauth app"
return
}
auditRec := c.MakeAuditRecord("authorizeOAuthApp", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
redirectURL, appErr := c.App.AllowOAuthAppAccessToUser(c.AppContext.Session().UserId, authRequest)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
c.LogAudit("")
w.Write([]byte(model.MapToJSON(map[string]string{"redirect": redirectURL})))
}
func deauthorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
requestData := model.MapFromJSON(r.Body)
clientId := requestData["client_id"]
if !model.IsValidId(clientId) {
c.SetInvalidParam("client_id")
return
}
auditRec := c.MakeAuditRecord("deauthorizeOAuthApp", audit.Fail)
defer c.LogAuditRec(auditRec)
err := c.App.DeauthorizeOAuthAppForUser(c.AppContext.Session().UserId, clientId)
if err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}
func authorizeOAuthPage(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().ServiceSettings.EnableOAuthServiceProvider {
err := model.NewAppError("authorizeOAuth", "api.oauth.authorize_oauth.disabled.app_error", nil, "", http.StatusNotImplemented)
utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey())
return
}
authRequest := &model.AuthorizeRequest{
ResponseType: r.URL.Query().Get("response_type"),
ClientId: r.URL.Query().Get("client_id"),
RedirectURI: r.URL.Query().Get("redirect_uri"),
Scope: r.URL.Query().Get("scope"),
State: r.URL.Query().Get("state"),
}
loginHint := r.URL.Query().Get("login_hint")
if err := authRequest.IsValid(); err != nil {
utils.RenderWebError(c.App.Config(), w, r, err.StatusCode,
url.Values{
"type": []string{"oauth_invalid_param"},
"message": []string{err.Message},
}, c.App.AsymmetricSigningKey())
return
}
oauthApp, err := c.App.GetOAuthApp(authRequest.ClientId)
if err != nil {
utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey())
return
}
// here we should check if the user is logged in
if c.AppContext.Session().UserId == "" {
if loginHint == model.UserAuthServiceSaml {
http.Redirect(w, r, c.GetSiteURLHeader()+"/login/sso/saml?redirect_to="+url.QueryEscape(r.RequestURI), http.StatusFound)
} else {
http.Redirect(w, r, c.GetSiteURLHeader()+"/login?redirect_to="+url.QueryEscape(r.RequestURI), http.StatusFound)
}
return
}
if !oauthApp.IsValidRedirectURL(authRequest.RedirectURI) {
err := model.NewAppError("authorizeOAuthPage", "api.oauth.allow_oauth.redirect_callback.app_error", nil, "", http.StatusBadRequest)
utils.RenderWebError(c.App.Config(), w, r, err.StatusCode,
url.Values{
"type": []string{"oauth_invalid_redirect_url"},
"message": []string{i18n.T("api.oauth.allow_oauth.redirect_callback.app_error")},
}, c.App.AsymmetricSigningKey())
return
}
isAuthorized := false
if _, err := c.App.GetPreferenceByCategoryAndNameForUser(c.AppContext.Session().UserId, model.PreferenceCategoryAuthorizedOAuthApp, authRequest.ClientId); err == nil {
// when we support scopes we should check if the scopes match
isAuthorized = true
}
// Automatically allow if the app is trusted
if oauthApp.IsTrusted || isAuthorized {
redirectURL, err := c.App.AllowOAuthAppAccessToUser(c.AppContext.Session().UserId, authRequest)
if err != nil {
utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey())
return
}
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
w.Header().Set("Content-Security-Policy", "frame-ancestors 'self'")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache, max-age=31556926")
staticDir, _ := fileutils.FindDir(model.ClientDir)
http.ServeFile(w, r, filepath.Join(staticDir, "root.html"))
}
func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
r.ParseForm()
code := r.FormValue("code")
refreshToken := r.FormValue("refresh_token")
grantType := r.FormValue("grant_type")
switch grantType {
case model.AccessTokenGrantType:
if code == "" {
c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.missing_code.app_error", nil, "", http.StatusBadRequest)
return
}
case model.RefreshTokenGrantType:
if refreshToken == "" {
c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.missing_refresh_token.app_error", nil, "", http.StatusBadRequest)
return
}
default:
c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.bad_grant.app_error", nil, "", http.StatusBadRequest)
return
}
clientId := r.FormValue("client_id")
if !model.IsValidId(clientId) {
c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.bad_client_id.app_error", nil, "", http.StatusBadRequest)
return
}
secret := r.FormValue("client_secret")
if secret == "" {
c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.bad_client_secret.app_error", nil, "", http.StatusBadRequest)
return
}
redirectURI := r.FormValue("redirect_uri")
auditRec := c.MakeAuditRecord("getAccessToken", audit.Fail)
defer c.LogAuditRec(auditRec)
auditRec.AddMeta("grant_type", grantType)
auditRec.AddMeta("client_id", clientId)
c.LogAudit("attempt")
accessRsp, err := c.App.GetOAuthAccessTokenForCodeFlow(clientId, grantType, redirectURI, code, secret, refreshToken)
if err != nil {
c.Err = err
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Pragma", "no-cache")
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(accessRsp); err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireService()
if c.Err != nil {
return
}
service := c.Params.Service
oauthError := r.URL.Query().Get("error")
if oauthError == "access_denied" {
utils.RenderWebError(c.App.Config(), w, r, http.StatusTemporaryRedirect, url.Values{
"type": []string{"oauth_access_denied"},
"service": []string{strings.Title(service)},
}, c.App.AsymmetricSigningKey())
return
}
code := r.URL.Query().Get("code")
if code == "" {
utils.RenderWebError(c.App.Config(), w, r, http.StatusTemporaryRedirect, url.Values{
"type": []string{"oauth_missing_code"},
"service": []string{strings.Title(service)},
}, c.App.AsymmetricSigningKey())
return
}
state := r.URL.Query().Get("state")
uri := c.GetSiteURLHeader() + "/signup/" + service + "/complete"
body, teamId, props, tokenUser, err := c.App.AuthorizeOAuthUser(w, r, service, code, state, uri)
action := ""
hasRedirectURL := false
isMobile := false
redirectURL := ""
if props != nil {
action = props["action"]
isMobile = action == model.OAuthActionMobile
if val, ok := props["redirect_to"]; ok {
redirectURL = val
hasRedirectURL = redirectURL != ""
}
}
redirectURL = fullyQualifiedRedirectURL(c.GetSiteURLHeader(), redirectURL)
renderError := func(err *model.AppError) {
if isMobile && hasRedirectURL {
utils.RenderMobileError(c.App.Config(), w, err, redirectURL)
} else {
utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey())
}
}
if err != nil {
err.Translate(c.AppContext.T)
c.LogErrorByCode(err)
renderError(err)
return
}
user, err := c.App.CompleteOAuth(c.AppContext, service, body, teamId, props, tokenUser)
if err != nil {
err.Translate(c.AppContext.T)
c.LogErrorByCode(err)
renderError(err)
return
}
if action == model.OAuthActionEmailToSSO {
redirectURL = c.GetSiteURLHeader() + "/login?extra=signin_change"
} else if action == model.OAuthActionSSOToEmail {
redirectURL = app.GetProtocol(r) + "://" + r.Host + "/claim?email=" + url.QueryEscape(props["email"])
} else {
err = c.App.DoLogin(c.AppContext, w, r, user, "", isMobile, false, false)
if err != nil {
err.Translate(c.AppContext.T)
mlog.Error(err.Error())
renderError(err)
return
}
// Old mobile version
if isMobile && !hasRedirectURL {
c.App.AttachSessionCookies(c.AppContext, w, r)
return
} else
// New mobile version
if isMobile && hasRedirectURL {
redirectURL = utils.AppendQueryParamsToURL(redirectURL, map[string]string{
model.SessionCookieToken: c.AppContext.Session().Token,
model.SessionCookieCsrf: c.AppContext.Session().GetCSRF(),
})
utils.RenderMobileAuthComplete(w, redirectURL)
return
} else { // For web
c.App.AttachSessionCookies(c.AppContext, w, r)
}
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect)
}
func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireService()
if c.Err != nil {
return
}
loginHint := r.URL.Query().Get("login_hint")
redirectURL := r.URL.Query().Get("redirect_to")
if redirectURL != "" && !utils.IsValidWebAuthRedirectURL(c.App.Config(), redirectURL) {
c.Err = model.NewAppError("loginWithOAuth", "api.invalid_redirect_url", nil, "", http.StatusBadRequest)
return
}
teamId, err := c.App.GetTeamIdFromQuery(r.URL.Query())
if err != nil {
c.Err = err
return
}
authURL, err := c.App.GetOAuthLoginEndpoint(w, r, c.Params.Service, teamId, model.OAuthActionLogin, redirectURL, loginHint, false)
if err != nil {
c.Err = err
return
}
http.Redirect(w, r, authURL, http.StatusFound)
}
func mobileLoginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireService()
if c.Err != nil {
return
}
redirectURL := html.EscapeString(r.URL.Query().Get("redirect_to"))
if redirectURL != "" && !utils.IsValidMobileAuthRedirectURL(c.App.Config(), redirectURL) {
err := model.NewAppError("mobileLoginWithOAuth", "api.invalid_custom_url_scheme", nil, "", http.StatusBadRequest)
utils.RenderMobileError(c.App.Config(), w, err, redirectURL)
return
}
teamId, err := c.App.GetTeamIdFromQuery(r.URL.Query())
if err != nil {
c.Err = err
return
}
authURL, err := c.App.GetOAuthLoginEndpoint(w, r, c.Params.Service, teamId, model.OAuthActionMobile, redirectURL, "", true)
if err != nil {
c.Err = err
return
}
http.Redirect(w, r, authURL, http.StatusFound)
}
func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireService()
if c.Err != nil {
return
}
if !*c.App.Config().TeamSettings.EnableUserCreation {
utils.RenderWebError(c.App.Config(), w, r, http.StatusBadRequest, url.Values{
"message": []string{i18n.T("api.oauth.singup_with_oauth.disabled.app_error")},
}, c.App.AsymmetricSigningKey())
return
}
teamId, err := c.App.GetTeamIdFromQuery(r.URL.Query())
if err != nil {
c.Err = err
return
}
authURL, err := c.App.GetOAuthSignupEndpoint(w, r, c.Params.Service, teamId)
if err != nil {
c.Err = err
return
}
http.Redirect(w, r, authURL, http.StatusFound)
}
func fullyQualifiedRedirectURL(siteURLPrefix, targetURL string) string {
parsed, _ := url.Parse(targetURL)
if parsed == nil || parsed.Scheme != "" || parsed.Host != "" {
return targetURL
}
if targetURL != "" && targetURL[0] != '/' {
targetURL = "/" + targetURL
}
return siteURLPrefix + targetURL
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"net/http"
"net/url"
"strconv"
"strings"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/model"
)
const (
PageDefault = 0
PerPageDefault = 60
PerPageMaximum = 200
LogsPerPageDefault = 10000
LogsPerPageMaximum = 10000
LimitDefault = 60
LimitMaximum = 200
)
type Params struct {
UserId string
TeamId string
InviteId string
TokenId string
ThreadId string
Timestamp int64
TimeRange string
ChannelId string
PostId string
PolicyId string
FileId string
Filename string
UploadId string
PluginId string
CommandId string
HookId string
ReportId string
EmojiId string
AppId string
Email string
Username string
TeamName string
ChannelName string
PreferenceName string
EmojiName string
Category string
Service string
JobId string
JobType string
ActionId string
RoleId string
RoleName string
SchemeId string
Scope string
GroupId string
Page int
PerPage int
LogsPerPage int
Permanent bool
RemoteId string
SyncableId string
SyncableType model.GroupSyncableType
BotUserId string
Q string
IsLinked *bool
IsConfigured *bool
NotAssociatedToTeam string
NotAssociatedToChannel string
Paginate *bool
IncludeMemberCount bool
NotAssociatedToGroup string
ExcludeDefaultChannels bool
LimitAfter int
LimitBefore int
GroupIDs string
IncludeTotalCount bool
IncludeDeleted bool
FilterAllowReference bool
FilterParentTeamPermitted bool
CategoryId string
WarnMetricId string
ExportName string
ExcludePolicyConstrained bool
GroupSource model.GroupSource
FilterHasMember string
IncludeChannelMemberCount string
// Cloud
InvoiceId string
}
func ParamsFromRequest(r *http.Request) *Params {
params := &Params{}
props := mux.Vars(r)
query := r.URL.Query()
params.UserId = props["user_id"]
params.TeamId = props["team_id"]
params.CategoryId = props["category_id"]
params.InviteId = props["invite_id"]
params.TokenId = props["token_id"]
params.ThreadId = props["thread_id"]
if val, ok := props["channel_id"]; ok {
params.ChannelId = val
} else {
params.ChannelId = query.Get("channel_id")
}
params.PostId = props["post_id"]
params.PolicyId = props["policy_id"]
params.FileId = props["file_id"]
params.Filename = query.Get("filename")
params.UploadId = props["upload_id"]
params.PluginId = props["plugin_id"]
params.CommandId = props["command_id"]
params.HookId = props["hook_id"]
params.ReportId = props["report_id"]
params.EmojiId = props["emoji_id"]
params.AppId = props["app_id"]
params.Email = props["email"]
params.Username = props["username"]
params.TeamName = strings.ToLower(props["team_name"])
params.ChannelName = strings.ToLower(props["channel_name"])
params.Category = props["category"]
params.Service = props["service"]
params.PreferenceName = props["preference_name"]
params.EmojiName = props["emoji_name"]
params.JobId = props["job_id"]
params.JobType = props["job_type"]
params.ActionId = props["action_id"]
params.RoleId = props["role_id"]
params.RoleName = props["role_name"]
params.SchemeId = props["scheme_id"]
params.GroupId = props["group_id"]
params.RemoteId = props["remote_id"]
params.InvoiceId = props["invoice_id"]
params.Scope = query.Get("scope")
if val, err := strconv.Atoi(query.Get("page")); err != nil || val < 0 {
params.Page = PageDefault
} else {
params.Page = val
}
if val, err := strconv.ParseInt(props["timestamp"], 10, 64); err != nil || val < 0 {
params.Timestamp = 0
} else {
params.Timestamp = val
}
params.TimeRange = query.Get("time_range")
params.Permanent, _ = strconv.ParseBool(query.Get("permanent"))
params.PerPage = getPerPageFromQuery(query)
if val, err := strconv.Atoi(query.Get("logs_per_page")); err != nil || val < 0 {
params.LogsPerPage = LogsPerPageDefault
} else if val > LogsPerPageMaximum {
params.LogsPerPage = LogsPerPageMaximum
} else {
params.LogsPerPage = val
}
if val, err := strconv.Atoi(query.Get("limit_after")); err != nil || val < 0 {
params.LimitAfter = LimitDefault
} else if val > LimitMaximum {
params.LimitAfter = LimitMaximum
} else {
params.LimitAfter = val
}
if val, err := strconv.Atoi(query.Get("limit_before")); err != nil || val < 0 {
params.LimitBefore = LimitDefault
} else if val > LimitMaximum {
params.LimitBefore = LimitMaximum
} else {
params.LimitBefore = val
}
params.SyncableId = props["syncable_id"]
switch props["syncable_type"] {
case "teams":
params.SyncableType = model.GroupSyncableTypeTeam
case "channels":
params.SyncableType = model.GroupSyncableTypeChannel
}
params.BotUserId = props["bot_user_id"]
params.Q = query.Get("q")
if val, err := strconv.ParseBool(query.Get("is_linked")); err == nil {
params.IsLinked = &val
}
if val, err := strconv.ParseBool(query.Get("is_configured")); err == nil {
params.IsConfigured = &val
}
params.NotAssociatedToTeam = query.Get("not_associated_to_team")
params.NotAssociatedToChannel = query.Get("not_associated_to_channel")
params.FilterAllowReference, _ = strconv.ParseBool(query.Get("filter_allow_reference"))
params.FilterParentTeamPermitted, _ = strconv.ParseBool(query.Get("filter_parent_team_permitted"))
params.IncludeChannelMemberCount = query.Get("include_channel_member_count")
if val, err := strconv.ParseBool(query.Get("paginate")); err == nil {
params.Paginate = &val
}
params.IncludeMemberCount, _ = strconv.ParseBool(query.Get("include_member_count"))
params.NotAssociatedToGroup = query.Get("not_associated_to_group")
params.ExcludeDefaultChannels, _ = strconv.ParseBool(query.Get("exclude_default_channels"))
params.GroupIDs = query.Get("group_ids")
params.IncludeTotalCount, _ = strconv.ParseBool(query.Get("include_total_count"))
params.IncludeDeleted, _ = strconv.ParseBool(query.Get("include_deleted"))
params.WarnMetricId = props["warn_metric_id"]
params.ExportName = props["export_name"]
params.ExcludePolicyConstrained, _ = strconv.ParseBool(query.Get("exclude_policy_constrained"))
if val := query.Get("group_source"); val != "" {
switch val {
case "custom":
params.GroupSource = model.GroupSourceCustom
default:
params.GroupSource = model.GroupSourceLdap
}
}
params.FilterHasMember = query.Get("filter_has_member")
return params
}
// getPerPageFromQuery returns the PerPage value from the given query.
// This function should be removed and the support for `pageSize`
// should be dropped after v1.46 of the mobile app is no longer supported
// https://mattermost.atlassian.net/browse/MM-38131
func getPerPageFromQuery(query url.Values) int {
val, err := strconv.Atoi(query.Get("per_page"))
if err != nil {
val, err = strconv.Atoi(query.Get("pageSize"))
}
if err != nil || val < 0 {
return PerPageDefault
} else if val > PerPageMaximum {
return PerPageMaximum
}
return val
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"bufio"
"errors"
"net"
"net/http"
)
type responseWriterWrapper struct {
http.ResponseWriter
statusCode int
statusCodeWritten bool
hijacker http.Hijacker
flusher http.Flusher
}
func newWrappedWriter(original http.ResponseWriter) *responseWriterWrapper {
hijacker, _ := original.(http.Hijacker)
flusher, _ := original.(http.Flusher)
return &responseWriterWrapper{
ResponseWriter: original,
statusCodeWritten: false,
hijacker: hijacker,
flusher: flusher,
}
}
func (rw *responseWriterWrapper) StatusCode() int {
return rw.statusCode
}
func (rw *responseWriterWrapper) WriteHeader(statusCode int) {
rw.statusCode = statusCode
rw.statusCodeWritten = true
rw.ResponseWriter.WriteHeader(statusCode)
}
func (rw *responseWriterWrapper) Write(data []byte) (int, error) {
if !rw.statusCodeWritten {
rw.statusCode = http.StatusOK
}
return rw.ResponseWriter.Write(data)
}
// Using as embedded makes the ResponseWrite be stored as interface and that way
// it loses the access to the implementation for Hijack or Flush
func (rw *responseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if rw.hijacker == nil {
return nil, nil, errors.New("Hijacker interface not supported by the wrapped ResponseWriter")
}
return rw.hijacker.Hijack()
}
func (rw *responseWriterWrapper) Flush() {
if rw.flusher != nil {
rw.flusher.Flush()
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
b64 "encoding/base64"
"html"
"net/http"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const maxSAMLResponseSize = 2 * 1024 * 1024 // 2MB
func (w *Web) InitSaml() {
w.MainRouter.Handle("/login/sso/saml", w.APIHandler(loginWithSaml)).Methods("GET")
w.MainRouter.Handle("/login/sso/saml", w.APIHandlerTrustRequester(completeSaml)).Methods("POST")
}
func loginWithSaml(c *Context, w http.ResponseWriter, r *http.Request) {
samlInterface := c.App.Saml()
if samlInterface == nil {
c.Err = model.NewAppError("loginWithSaml", "api.user.saml.not_available.app_error", nil, "", http.StatusFound)
return
}
teamId, err := c.App.GetTeamIdFromQuery(r.URL.Query())
if err != nil {
c.Err = err
return
}
action := r.URL.Query().Get("action")
isMobile := action == model.OAuthActionMobile
redirectURL := html.EscapeString(r.URL.Query().Get("redirect_to"))
relayProps := map[string]string{}
relayState := ""
if action != "" {
relayProps["team_id"] = teamId
relayProps["action"] = action
if action == model.OAuthActionEmailToSSO {
relayProps["email"] = r.URL.Query().Get("email")
}
}
if redirectURL != "" {
if isMobile && !utils.IsValidMobileAuthRedirectURL(c.App.Config(), redirectURL) {
invalidSchemeErr := model.NewAppError("loginWithOAuth", "api.invalid_custom_url_scheme", nil, "", http.StatusBadRequest)
utils.RenderMobileError(c.App.Config(), w, invalidSchemeErr, redirectURL)
return
}
relayProps["redirect_to"] = redirectURL
}
relayProps[model.UserAuthServiceIsMobile] = strconv.FormatBool(isMobile)
if len(relayProps) > 0 {
relayState = b64.StdEncoding.EncodeToString([]byte(model.MapToJSON(relayProps)))
}
data, err := samlInterface.BuildRequest(relayState)
if err != nil {
c.Err = err
return
}
w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
http.Redirect(w, r, data.URL, http.StatusFound)
}
func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
samlInterface := c.App.Saml()
if samlInterface == nil {
c.Err = model.NewAppError("completeSaml", "api.user.saml.not_available.app_error", nil, "", http.StatusFound)
return
}
//Validate that the user is with SAML and all that
encodedXML := r.FormValue("SAMLResponse")
relayState := r.FormValue("RelayState")
relayProps := make(map[string]string)
if relayState != "" {
stateStr := ""
b, err := b64.StdEncoding.DecodeString(relayState)
if err != nil {
c.Err = model.NewAppError("completeSaml", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "", http.StatusFound).Wrap(err)
return
}
stateStr = string(b)
relayProps = model.MapFromJSON(strings.NewReader(stateStr))
}
auditRec := c.MakeAuditRecord("completeSaml", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
action := relayProps["action"]
auditRec.AddMeta("action", action)
isMobile := action == model.OAuthActionMobile
redirectURL := ""
hasRedirectURL := false
if val, ok := relayProps["redirect_to"]; ok {
redirectURL = val
hasRedirectURL = val != ""
}
redirectURL = fullyQualifiedRedirectURL(c.GetSiteURLHeader(), redirectURL)
handleError := func(err *model.AppError) {
if isMobile && hasRedirectURL {
err.Translate(c.AppContext.T)
utils.RenderMobileError(c.App.Config(), w, err, redirectURL)
} else {
c.Err = err
c.Err.StatusCode = http.StatusFound
}
}
if len(encodedXML) > maxSAMLResponseSize {
err := model.NewAppError("completeSaml", "api.user.authorize_oauth_user.saml_response_too_long.app_error", nil, "SAML response is too long", http.StatusBadRequest)
mlog.Error(err.Error())
handleError(err)
return
}
user, err := samlInterface.DoLogin(c.AppContext, encodedXML, relayProps)
if err != nil {
c.LogAudit("fail")
mlog.Error(err.Error())
handleError(err)
return
}
if err = c.App.CheckUserAllAuthenticationCriteria(user, ""); err != nil {
mlog.Error(err.Error())
handleError(err)
return
}
switch action {
case model.OAuthActionSignup:
if teamId := relayProps["team_id"]; teamId != "" {
if err = c.App.AddUserToTeamByTeamId(c.AppContext, teamId, user); err != nil {
c.LogErrorByCode(err)
break
}
c.App.AddDirectChannels(c.AppContext, teamId, user)
}
case model.OAuthActionEmailToSSO:
if err = c.App.RevokeAllSessions(user.Id); err != nil {
c.Err = err
return
}
auditRec.AddMeta("revoked_user_id", user.Id)
auditRec.AddMeta("revoked", "Revoked all sessions for user")
c.LogAuditWithUserId(user.Id, "Revoked all sessions for user")
c.App.Srv().Go(func() {
if err := c.App.Srv().EmailService.SendSignInChangeEmail(user.Email, strings.Title(model.UserAuthServiceSaml)+" SSO", user.Locale, c.App.GetSiteURL()); err != nil {
c.LogErrorByCode(model.NewAppError("SendSignInChangeEmail", "api.user.send_sign_in_change_email_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err))
}
})
}
auditRec.AddMeta("obtained_user_id", user.Id)
c.LogAuditWithUserId(user.Id, "obtained user")
err = c.App.DoLogin(c.AppContext, w, r, user, "", isMobile, false, true)
if err != nil {
mlog.Error(err.Error())
handleError(err)
return
}
auditRec.Success()
c.LogAuditWithUserId(user.Id, "success")
c.App.AttachSessionCookies(c.AppContext, w, r)
if hasRedirectURL {
if isMobile {
// Mobile clients with redirect url support
redirectURL = utils.AppendQueryParamsToURL(redirectURL, map[string]string{
model.SessionCookieToken: c.AppContext.Session().Token,
model.SessionCookieCsrf: c.AppContext.Session().GetCSRF(),
})
utils.RenderMobileAuthComplete(w, redirectURL)
} else {
http.Redirect(w, r, redirectURL, http.StatusFound)
}
return
}
switch action {
// Mobile clients with web view implementation
case model.OAuthActionMobile:
ReturnStatusOK(w)
case model.OAuthActionEmailToSSO:
http.Redirect(w, r, c.GetSiteURLHeader()+"/login?extra=signin_change", http.StatusFound)
default:
http.Redirect(w, r, c.GetSiteURLHeader(), http.StatusFound)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"bytes"
"fmt"
"html"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"github.com/mattermost/gziphandler"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/templates"
)
var robotsTxt = []byte("User-agent: *\nDisallow: /\n")
func (w *Web) InitStatic() {
if *w.srv.Config().ServiceSettings.WebserverMode != "disabled" {
if err := utils.UpdateAssetsSubpathFromConfig(w.srv.Config()); err != nil {
mlog.Error("Failed to update assets subpath from config", mlog.Err(err))
}
staticDir, _ := fileutils.FindDir(model.ClientDir)
mlog.Debug("Using client directory", mlog.String("clientDir", staticDir))
subpath, _ := utils.GetSubpathFromConfig(w.srv.Config())
staticHandler := staticFilesHandler(http.StripPrefix(path.Join(subpath, "static"), http.FileServer(http.Dir(staticDir))))
pluginHandler := staticFilesHandler(http.StripPrefix(path.Join(subpath, "static", "plugins"), http.FileServer(http.Dir(*w.srv.Config().PluginSettings.ClientDirectory))))
if *w.srv.Config().ServiceSettings.WebserverMode == "gzip" {
staticHandler = gziphandler.GzipHandler(staticHandler)
pluginHandler = gziphandler.GzipHandler(pluginHandler)
}
w.MainRouter.PathPrefix("/static/plugins/").Handler(pluginHandler)
w.MainRouter.PathPrefix("/static/").Handler(staticHandler)
w.MainRouter.Handle("/robots.txt", http.HandlerFunc(robotsHandler))
w.MainRouter.Handle("/unsupported_browser.js", http.HandlerFunc(unsupportedBrowserScriptHandler))
w.MainRouter.Handle("/{anything:.*}", w.NewStaticHandler(root)).Methods("GET")
// When a subpath is defined, it's necessary to handle redirects without a
// trailing slash. We don't want to use StrictSlash on the w.MainRouter and affect
// all routes, just /subpath -> /subpath/.
w.MainRouter.HandleFunc("", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.URL.Path += "/"
http.Redirect(w, r, r.URL.String(), http.StatusFound)
}))
}
}
func root(c *Context, w http.ResponseWriter, r *http.Request) {
if !CheckClientCompatibility(r.UserAgent()) {
w.Header().Set("Cache-Control", "no-store")
data := renderUnsupportedBrowser(c.AppContext, r)
c.App.Srv().TemplatesContainer().Render(w, "unsupported_browser", data)
return
}
if IsAPICall(c.App, r) {
Handle404(c.App, w, r)
return
}
w.Header().Set("Cache-Control", "no-cache, max-age=31556926, public")
staticDir, _ := fileutils.FindDir(model.ClientDir)
contents, err := os.ReadFile(filepath.Join(staticDir, "root.html"))
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
titleTemplate := "<title>%s</title>"
originalHTML := fmt.Sprintf(titleTemplate, html.EscapeString(model.TeamSettingsDefaultSiteName))
modifiedHTML := getOpenGraphMetaTags(c)
if originalHTML != modifiedHTML {
contents = bytes.ReplaceAll(contents, []byte(originalHTML), []byte(modifiedHTML))
}
w.Header().Set("Content-Type", "text/html")
w.Write(contents)
}
func staticFilesHandler(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//wrap our ResponseWriter with our no-cache 404-handler
w = ¬FoundNoCacheResponseWriter{ResponseWriter: w}
if path.Base(r.URL.Path) == "remote_entry.js" {
w.Header().Set("Cache-Control", "no-cache, max-age=31556926, public")
} else {
w.Header().Set("Cache-Control", "max-age=31556926, public")
}
// Hardcoded sensible default values for these security headers. Feel free to override in proxy or ingress
w.Header().Set("Permissions-Policy", "")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Referrer-Policy", "no-referrer")
if strings.HasSuffix(r.URL.Path, "/") {
http.NotFound(w, r)
return
}
handler.ServeHTTP(w, r)
})
}
type notFoundNoCacheResponseWriter struct {
http.ResponseWriter
}
func (w *notFoundNoCacheResponseWriter) WriteHeader(statusCode int) {
if statusCode == http.StatusNotFound {
// we have a 404, update our cache header first then fall through
w.Header().Set("Cache-Control", "no-cache, public")
}
w.ResponseWriter.WriteHeader(statusCode)
}
func robotsHandler(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/") {
http.NotFound(w, r)
return
}
w.Write(robotsTxt)
}
func unsupportedBrowserScriptHandler(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/") {
http.NotFound(w, r)
return
}
templatesDir, _ := templates.GetTemplateDirectory()
http.ServeFile(w, r, filepath.Join(templatesDir, "unsupported_browser.js"))
}
func getOpenGraphMetaTags(c *Context) string {
siteName := model.TeamSettingsDefaultSiteName
customSiteName := c.App.Srv().Config().TeamSettings.SiteName
if customSiteName != nil && *customSiteName != "" {
siteName = *customSiteName
}
siteDescription := model.TeamSettingsDefaultCustomDescriptionText
customSiteDescription := c.App.Srv().Config().TeamSettings.CustomDescriptionText
if customSiteDescription != nil && *customSiteDescription != "" {
siteDescription = *customSiteDescription
}
titleTemplate := "<title>%s</title>"
titleHTML := fmt.Sprintf(titleTemplate, html.EscapeString(siteName))
descriptionHTML := ""
if siteDescription != "" {
descriptionTemplate := "<meta property=\"og:description\" content=\"%s\" />"
descriptionHTML = fmt.Sprintf(descriptionTemplate, html.EscapeString(siteDescription))
}
return titleHTML + descriptionHTML
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"net/http"
"github.com/avct/uasurfer"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/templates"
)
// MattermostApp describes downloads for the Mattermost App
type MattermostApp struct {
LogoSrc string
Title string
SupportedVersionString string
Label string
Link string
InstallGuide string
InstallGuideLink string
}
// Browser describes a browser with a download link
type Browser struct {
LogoSrc string
Title string
SupportedVersionString string
Src string
GetLatestString string
}
// SystemBrowser describes a browser but includes 2 links: one to open the local browser, and one to make it default
type SystemBrowser struct {
LogoSrc string
Title string
SupportedVersionString string
LabelOpen string
LinkOpen string
LinkMakeDefault string
OrString string
MakeDefaultString string
}
func renderUnsupportedBrowser(ctx *request.Context, r *http.Request) templates.Data {
data := templates.Data{
Props: map[string]any{
"DownloadAppOrUpgradeBrowserString": ctx.T("web.error.unsupported_browser.download_app_or_upgrade_browser"),
"LearnMoreString": ctx.T("web.error.unsupported_browser.learn_more"),
},
}
// User Agent info
ua := uasurfer.Parse(r.UserAgent())
isWindows := ua.OS.Platform.String() == "PlatformWindows"
isWindows10 := isWindows && ua.OS.Version.Major == 10
isMacOSX := ua.OS.Name.String() == "OSMacOSX" && ua.OS.Version.Major == 10
isSafari := ua.Browser.Name.String() == "BrowserSafari"
// Basic heading translations
if isSafari {
data.Props["NoLongerSupportString"] = ctx.T("web.error.unsupported_browser.no_longer_support_version")
} else {
data.Props["NoLongerSupportString"] = ctx.T("web.error.unsupported_browser.no_longer_support")
}
// Mattermost app version
if isWindows {
data.Props["App"] = renderMattermostAppWindows(ctx)
} else if isMacOSX {
data.Props["App"] = renderMattermostAppMac(ctx)
}
// Browsers to download
// Show a link to Safari if you're using safari and it's outdated
// Can't show on Mac all the time because there's no way to open it via URI
browsers := []Browser{renderBrowserChrome(ctx), renderBrowserFirefox(ctx)}
if isSafari {
browsers = append(browsers, renderBrowserSafari(ctx))
}
data.Props["Browsers"] = browsers
// If on Windows 10, show link to Edge
if isWindows10 {
data.Props["SystemBrowser"] = renderSystemBrowserEdge(ctx, r)
}
return data
}
func renderMattermostAppMac(ctx *request.Context) MattermostApp {
return MattermostApp{
"/static/images/browser-icons/mac.png",
ctx.T("web.error.unsupported_browser.download_the_app"),
ctx.T("web.error.unsupported_browser.min_os_version.mac"),
ctx.T("web.error.unsupported_browser.download"),
"https://mattermost.com/download/#mattermostApps",
ctx.T("web.error.unsupported_browser.install_guide.mac"),
"https://docs.mattermost.com/install/desktop.html#mac-os-x-10-9",
}
}
func renderMattermostAppWindows(ctx *request.Context) MattermostApp {
return MattermostApp{
"/static/images/browser-icons/windows.svg",
ctx.T("web.error.unsupported_browser.download_the_app"),
ctx.T("web.error.unsupported_browser.min_os_version.windows"),
ctx.T("web.error.unsupported_browser.download"),
"https://mattermost.com/download/#mattermostApps",
ctx.T("web.error.unsupported_browser.install_guide.windows"),
"https://docs.mattermost.com/install/desktop.html#windows-10-windows-8-1-windows-7",
}
}
func renderBrowserChrome(ctx *request.Context) Browser {
return Browser{
"/static/images/browser-icons/chrome.svg",
ctx.T("web.error.unsupported_browser.browser_title.chrome"),
ctx.T("web.error.unsupported_browser.min_browser_version.chrome"),
"http://www.google.com/chrome",
ctx.T("web.error.unsupported_browser.browser_get_latest.chrome"),
}
}
func renderBrowserFirefox(ctx *request.Context) Browser {
return Browser{
"/static/images/browser-icons/firefox.svg",
ctx.T("web.error.unsupported_browser.browser_title.firefox"),
ctx.T("web.error.unsupported_browser.min_browser_version.firefox"),
"https://www.mozilla.org/firefox/new/",
ctx.T("web.error.unsupported_browser.browser_get_latest.firefox"),
}
}
func renderBrowserSafari(ctx *request.Context) Browser {
return Browser{
"/static/images/browser-icons/safari.svg",
ctx.T("web.error.unsupported_browser.browser_title.safari"),
ctx.T("web.error.unsupported_browser.min_browser_version.safari"),
"macappstore://showUpdatesPage",
ctx.T("web.error.unsupported_browser.browser_get_latest.safari"),
}
}
func renderSystemBrowserEdge(ctx *request.Context, r *http.Request) SystemBrowser {
return SystemBrowser{
"/static/images/browser-icons/edge.svg",
ctx.T("web.error.unsupported_browser.browser_title.edge"),
ctx.T("web.error.unsupported_browser.min_browser_version.edge"),
ctx.T("web.error.unsupported_browser.open_system_browser.edge"),
"microsoft-edge:http://" + r.Host + r.RequestURI, //TODO: Can we get HTTP or HTTPS? If someone's server doesn't have a redirect this won't work
"ms-settings:defaultapps",
ctx.T("web.error.unsupported_browser.system_browser_or"),
ctx.T("web.error.unsupported_browser.system_browser_make_default"),
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"net/http"
"path"
"strings"
"github.com/avct/uasurfer"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type Web struct {
srv *app.Server
MainRouter *mux.Router
}
func New(srv *app.Server) *Web {
mlog.Debug("Initializing web routes")
web := &Web{
srv: srv,
MainRouter: srv.Router,
}
web.InitOAuth()
web.InitWebhooks()
web.InitSaml()
web.InitStatic()
return web
}
// Due to the complexities of UA detection and the ramifications of a misdetection
// only older Safari and IE browsers throw incompatibility errors.
// Map should be of minimum required browser version.
// -1 means that the browser is not supported in any version.
var browserMinimumSupported = map[string]int{
"BrowserIE": 12,
"BrowserSafari": 12,
}
func CheckClientCompatibility(agentString string) bool {
ua := uasurfer.Parse(agentString)
if version, exist := browserMinimumSupported[ua.Browser.Name.String()]; exist && (ua.Browser.Version.Major < version || version < 0) {
return false
}
return true
}
func Handle404(a app.AppIface, w http.ResponseWriter, r *http.Request) {
err := model.NewAppError("Handle404", "api.context.404.app_error", nil, "", http.StatusNotFound)
ipAddress := utils.GetIPAddress(r, a.Config().ServiceSettings.TrustedProxyIPHeader)
mlog.Debug("not found handler triggered", mlog.String("path", r.URL.Path), mlog.Int("code", 404), mlog.String("ip", ipAddress))
if IsAPICall(a, r) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(err.StatusCode)
err.DetailedError = "There doesn't appear to be an api call for the url='" + r.URL.Path + "'. Typo? are you missing a team_id or user_id as part of the url?"
w.Write([]byte(err.ToJSON()))
} else if *a.Config().ServiceSettings.WebserverMode == "disabled" {
http.NotFound(w, r)
} else {
utils.RenderWebAppError(a.Config(), w, r, err, a.AsymmetricSigningKey())
}
}
func IsAPICall(a app.AppIface, r *http.Request) bool {
subpath, _ := utils.GetSubpathFromConfig(a.Config())
return strings.HasPrefix(r.URL.Path, path.Join(subpath, "api")+"/")
}
func IsWebhookCall(a app.AppIface, r *http.Request) bool {
subpath, _ := utils.GetSubpathFromConfig(a.Config())
return strings.HasPrefix(r.URL.Path, path.Join(subpath, "hooks")+"/")
}
func IsOAuthAPICall(a app.AppIface, r *http.Request) bool {
subpath, _ := utils.GetSubpathFromConfig(a.Config())
if r.Method == "POST" && r.URL.Path == path.Join(subpath, "oauth", "authorize") {
return true
}
if r.URL.Path == path.Join(subpath, "oauth", "apps", "authorized") ||
r.URL.Path == path.Join(subpath, "oauth", "deauthorize") ||
r.URL.Path == path.Join(subpath, "oauth", "access_token") {
return true
}
return false
}
func ReturnStatusOK(w http.ResponseWriter) {
m := make(map[string]string)
m[model.STATUS] = model.StatusOk
w.Write([]byte(model.MapToJSON(m)))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"encoding/json"
"io"
"mime"
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/gorilla/schema"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (w *Web) InitWebhooks() {
w.MainRouter.Handle("/hooks/commands/{id:[A-Za-z0-9]+}", w.APIHandlerTrustRequester(commandWebhook)).Methods("POST")
w.MainRouter.Handle("/hooks/{id:[A-Za-z0-9]+}", w.APIHandlerTrustRequester(incomingWebhook)).Methods("POST")
}
func incomingWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
r.ParseForm()
var err *model.AppError
var mediaType string
incomingWebhookPayload := &model.IncomingWebhookRequest{}
contentType := r.Header.Get("Content-Type")
// Content-Type header is optional so could be empty
if contentType != "" {
var mimeErr error
mediaType, _, mimeErr = mime.ParseMediaType(contentType)
if mimeErr != nil && mimeErr != mime.ErrInvalidMediaParameter {
c.Err = model.NewAppError("incomingWebhook",
"api.webhook.incoming.error",
nil,
"webhook_id="+id+", error: "+mimeErr.Error(),
http.StatusBadRequest,
)
return
}
}
defer func() {
if *c.App.Config().LogSettings.EnableWebhookDebugging {
if c.Err != nil {
fields := []mlog.Field{mlog.String("webhook_id", id), mlog.String("request_id", c.AppContext.RequestId())}
payload, err := json.Marshal(incomingWebhookPayload)
if err != nil {
fields = append(fields, mlog.NamedErr("encoding_err", err))
} else {
fields = append(fields, mlog.String("payload", string(payload)))
}
mlog.Debug("Incoming webhook received", fields...)
}
}
}()
if mediaType == "application/x-www-form-urlencoded" {
payload := strings.NewReader(r.FormValue("payload"))
incomingWebhookPayload, err = decodePayload(payload)
if err != nil {
c.Err = err
return
}
} else if mediaType == "multipart/form-data" {
r.ParseMultipartForm(0)
decoder := schema.NewDecoder()
err := decoder.Decode(incomingWebhookPayload, r.PostForm)
if err != nil {
c.Err = model.NewAppError("incomingWebhook",
"api.webhook.incoming.error",
nil,
"webhook_id="+id+", error: "+err.Error(),
http.StatusBadRequest,
)
return
}
} else {
incomingWebhookPayload, err = decodePayload(r.Body)
if err != nil {
c.Err = err
return
}
}
err = c.App.HandleIncomingWebhook(c.AppContext, id, incomingWebhookPayload)
if err != nil {
c.Err = err
return
}
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("ok"))
}
func commandWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
response, err := model.CommandResponseFromHTTPBody(r.Header.Get("Content-Type"), r.Body)
if err != nil {
c.Err = model.NewAppError("commandWebhook", "web.command_webhook.parse.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
appErr := c.App.HandleCommandWebhook(c.AppContext, id, response)
if appErr != nil {
c.Err = appErr
return
}
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("ok"))
}
func decodePayload(payload io.Reader) (*model.IncomingWebhookRequest, *model.AppError) {
incomingWebhookPayload, decodeError := model.IncomingWebhookRequestFromJSON(payload)
if decodeError != nil {
return nil, decodeError
}
return incomingWebhookPayload, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package wsapi
import (
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
)
type API struct {
App *app.App
Router *platform.WebSocketRouter
}
func Init(s *app.Server) {
a := app.New(app.ServerConnector(s.Channels()))
router := s.Platform().WebSocketRouter
api := &API{
App: a,
Router: router,
}
api.InitUser()
api.InitSystem()
api.InitStatus()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package wsapi
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitStatus() {
api.Router.Handle("get_statuses", api.APIWebSocketHandler(api.getStatuses))
api.Router.Handle("get_statuses_by_ids", api.APIWebSocketHandler(api.getStatusesByIds))
}
func (api *API) getStatuses(req *model.WebSocketRequest) (map[string]any, *model.AppError) {
statusMap := api.App.Srv().Platform().GetAllStatuses()
return model.StatusMapToInterfaceMap(statusMap), nil
}
func (api *API) getStatusesByIds(req *model.WebSocketRequest) (map[string]any, *model.AppError) {
var userIds []string
if userIds = model.ArrayFromInterface(req.Data["user_ids"]); len(userIds) == 0 {
mlog.Debug("Error while parsing user_ids", mlog.String("data", model.StringInterfaceToJSON(req.Data)))
return nil, NewInvalidWebSocketParamError(req.Action, "user_ids")
}
statusMap, err := api.App.Srv().Platform().GetStatusesByIds(userIds)
if err != nil {
return nil, err
}
return statusMap, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package wsapi
import (
"github.com/mattermost/mattermost-server/v6/model"
)
func (api *API) InitSystem() {
api.Router.Handle("ping", api.APIWebSocketHandler(ping))
}
func ping(req *model.WebSocketRequest) (map[string]any, *model.AppError) {
data := map[string]any{}
data["text"] = "pong"
data["version"] = model.CurrentVersion
data["server_time"] = model.GetMillis()
data["node_id"] = ""
return data, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package wsapi
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
)
func (api *API) InitUser() {
api.Router.Handle("user_typing", api.APIWebSocketHandler(api.userTyping))
api.Router.Handle("user_update_active_status", api.APIWebSocketHandler(api.userUpdateActiveStatus))
}
func (api *API) userTyping(req *model.WebSocketRequest) (map[string]any, *model.AppError) {
api.App.ExtendSessionExpiryIfNeeded(&req.Session)
if api.App.Srv().Platform().Busy.IsBusy() {
// this is considered a non-critical service and will be disabled when server busy.
return nil, NewServerBusyWebSocketError(req.Action)
}
var ok bool
var channelId string
if channelId, ok = req.Data["channel_id"].(string); !ok || !model.IsValidId(channelId) {
return nil, NewInvalidWebSocketParamError(req.Action, "channel_id")
}
if !api.App.SessionHasPermissionToChannel(request.EmptyContext(api.App.Log()), req.Session, channelId, model.PermissionCreatePost) {
return nil, NewInvalidWebSocketParamError(req.Action, "channel_id")
}
var parentId string
if parentId, ok = req.Data["parent_id"].(string); !ok {
parentId = ""
}
appErr := api.App.PublishUserTyping(req.Session.UserId, channelId, parentId)
return nil, appErr
}
func (api *API) userUpdateActiveStatus(req *model.WebSocketRequest) (map[string]any, *model.AppError) {
var ok bool
var userIsActive bool
if userIsActive, ok = req.Data["user_is_active"].(bool); !ok {
return nil, NewInvalidWebSocketParamError(req.Action, "user_is_active")
}
var manual bool
if manual, ok = req.Data["manual"].(bool); !ok {
manual = false
}
if userIsActive {
api.App.SetStatusOnline(req.Session.UserId, manual)
} else {
api.App.SetStatusAwayIfNeeded(req.Session.UserId, manual)
}
return nil, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package wsapi
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/platform"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) APIWebSocketHandler(wh func(*model.WebSocketRequest) (map[string]any, *model.AppError)) webSocketHandler {
return webSocketHandler{api.App, wh}
}
type webSocketHandler struct {
app *app.App
handlerFunc func(*model.WebSocketRequest) (map[string]any, *model.AppError)
}
func (wh webSocketHandler) ServeWebSocket(conn *platform.WebConn, r *model.WebSocketRequest) {
mlog.Debug("Websocket request", mlog.String("action", r.Action))
hub := wh.app.Srv().Platform().GetHubForUserId(conn.UserId)
if hub == nil {
return
}
session, sessionErr := wh.app.GetSession(conn.GetSessionToken())
defer wh.app.ReturnSessionToPool(session)
if sessionErr != nil {
mlog.Error(
"websocket session error",
mlog.String("action", r.Action),
mlog.Int64("seq", r.Seq),
mlog.String("user_id", conn.UserId),
mlog.String("error_message", sessionErr.SystemMessage(i18n.T)),
mlog.Err(sessionErr),
)
sessionErr.DetailedError = ""
errResp := model.NewWebSocketError(r.Seq, sessionErr)
hub.SendMessage(conn, errResp)
return
}
r.Session = *session
r.T = conn.T
r.Locale = conn.Locale
var data map[string]any
var err *model.AppError
if data, err = wh.handlerFunc(r); err != nil {
mlog.Error(
"websocket request handling error",
mlog.String("action", r.Action),
mlog.Int64("seq", r.Seq),
mlog.String("user_id", conn.UserId),
mlog.String("error_message", err.SystemMessage(i18n.T)),
mlog.Err(err),
)
err.DetailedError = ""
errResp := model.NewWebSocketError(r.Seq, err)
hub.SendMessage(conn, errResp)
return
}
resp := model.NewWebSocketResponse(model.StatusOk, r.Seq, data)
hub.SendMessage(conn, resp)
}
func NewInvalidWebSocketParamError(action string, name string) *model.AppError {
return model.NewAppError("websocket: "+action, "api.websocket_handler.invalid_param.app_error", map[string]any{"Name": name}, "", http.StatusBadRequest)
}
func NewServerBusyWebSocketError(action string) *model.AppError {
return model.NewAppError("websocket: "+action, "api.websocket_handler.server_busy.app_error", nil, "", http.StatusServiceUnavailable)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/api4"
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
"github.com/mattermost/mattermost-server/v6/server/channels/testlib"
)
var coverprofileCounters map[string]int = make(map[string]int)
var mainHelper *testlib.MainHelper
type testHelper struct {
*api4.TestHelper
config *model.Config
tempDir string
configFilePath string
disableAutoConfig bool
}
// Setup creates an instance of testHelper.
func Setup(t testing.TB) *testHelper {
dir, err := testlib.SetupTestResources()
if err != nil {
panic("failed to create temporary directory: " + err.Error())
}
api4TestHelper := api4.Setup(t)
testHelper := &testHelper{
TestHelper: api4TestHelper,
tempDir: dir,
configFilePath: filepath.Join(dir, "config-helper.json"),
}
config := &model.Config{}
config.SetDefaults()
testHelper.SetConfig(config)
return testHelper
}
// Setup creates an instance of testHelper.
func SetupWithStoreMock(t testing.TB) *testHelper {
dir, err := testlib.SetupTestResources()
if err != nil {
panic("failed to create temporary directory: " + err.Error())
}
api4TestHelper := api4.SetupWithStoreMock(t)
systemStore := mocks.SystemStore{}
systemStore.On("Get").Return(make(model.StringMap), nil)
licenseStore := mocks.LicenseStore{}
licenseStore.On("Get", "").Return(&model.LicenseRecord{}, nil)
api4TestHelper.App.Srv().Store().(*mocks.Store).On("System").Return(&systemStore)
api4TestHelper.App.Srv().Store().(*mocks.Store).On("License").Return(&licenseStore)
testHelper := &testHelper{
TestHelper: api4TestHelper,
tempDir: dir,
configFilePath: filepath.Join(dir, "config-helper.json"),
}
config := &model.Config{}
config.SetDefaults()
testHelper.SetConfig(config)
return testHelper
}
// InitBasic simply proxies to api4.InitBasic, while still returning a testHelper.
func (h *testHelper) InitBasic() *testHelper {
h.TestHelper.InitBasic()
return h
}
// TemporaryDirectory returns the temporary directory created for user by the test helper.
func (h *testHelper) TemporaryDirectory() string {
return h.tempDir
}
// Config returns the configuration passed to a running command.
func (h *testHelper) Config() *model.Config {
return h.config.Clone()
}
// ConfigPath returns the path to the temporary config file passed to a running command.
func (h *testHelper) ConfigPath() string {
return h.configFilePath
}
// SetConfig replaces the configuration passed to a running command.
func (h *testHelper) SetConfig(config *model.Config) {
if !testing.Short() {
config.SqlSettings = *mainHelper.GetSQLSettings()
}
// Disable strict password requirements for test
*config.PasswordSettings.MinimumLength = 5
*config.PasswordSettings.Lowercase = false
*config.PasswordSettings.Uppercase = false
*config.PasswordSettings.Symbol = false
*config.PasswordSettings.Number = false
h.config = config
buf, err := json.Marshal(config)
if err != nil {
panic("failed to marshal config: " + err.Error())
}
if err := os.WriteFile(h.configFilePath, buf, 0600); err != nil {
panic("failed to write file " + h.configFilePath + ": " + err.Error())
}
}
// SetAutoConfig configures whether the --config flag is automatically passed to a running command.
func (h *testHelper) SetAutoConfig(autoConfig bool) {
h.disableAutoConfig = !autoConfig
}
// TearDown cleans up temporary files and assets created during the life of the test helper.
func (h *testHelper) TearDown() {
h.TestHelper.TearDown()
os.RemoveAll(h.tempDir)
}
func (h *testHelper) execArgs(t *testing.T, args []string) []string {
ret := []string{"-test.v", "-test.run", "ExecCommand"}
if coverprofile := flag.Lookup("test.coverprofile").Value.String(); coverprofile != "" {
dir := filepath.Dir(coverprofile)
base := filepath.Base(coverprofile)
baseParts := strings.SplitN(base, ".", 2)
name := strings.Replace(t.Name(), "/", "_", -1)
coverprofileCounters[name] = coverprofileCounters[name] + 1
baseParts[0] = fmt.Sprintf("%v-%v-%v", baseParts[0], name, coverprofileCounters[name])
ret = append(ret, "-test.coverprofile", filepath.Join(dir, strings.Join(baseParts, ".")))
}
ret = append(ret, "--")
// Unless the test passes a `--config` of its own, create a temporary one from the default
// configuration with the current test database applied.
hasConfig := h.disableAutoConfig
for _, arg := range args {
if arg == "--config" {
hasConfig = true
break
}
}
if !hasConfig {
ret = append(ret, "--config", h.configFilePath)
}
ret = append(ret, args...)
return ret
}
func (h *testHelper) cmd(t *testing.T, args []string) *exec.Cmd {
path, err := os.Executable()
require.NoError(t, err)
cmd := exec.Command(path, h.execArgs(t, args)...)
cmd.Env = []string{}
for _, env := range os.Environ() {
// Ignore MM_SQLSETTINGS_DATASOURCE from the environment, since we override.
if strings.HasPrefix(env, "MM_SQLSETTINGS_DATASOURCE=") {
continue
}
cmd.Env = append(cmd.Env, env)
}
return cmd
}
// CheckCommand invokes the test binary, returning the output modified for assertion testing.
func (h *testHelper) CheckCommand(t *testing.T, args ...string) string {
output, err := h.cmd(t, args).CombinedOutput()
require.NoError(t, err, string(output))
return strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(string(output)), "PASS"))
}
// RunCommand invokes the test binary, returning only any error.
func (h *testHelper) RunCommand(t *testing.T, args ...string) error {
return h.cmd(t, args).Run()
}
// RunCommandWithOutput is a variant of RunCommand that returns the unmodified output and any error.
func (h *testHelper) RunCommandWithOutput(t *testing.T, args ...string) (string, error) {
cmd := h.cmd(t, args)
var buf bytes.Buffer
reader, writer := io.Pipe()
cmd.Stdout = writer
cmd.Stderr = writer
done := make(chan bool)
go func() {
io.Copy(&buf, reader)
close(done)
}()
err := cmd.Run()
writer.Close()
<-done
return buf.String(), err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"fmt"
"strconv"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/config"
)
var DbCmd = &cobra.Command{
Use: "db",
Short: "Commands related to the database",
}
var InitDbCmd = &cobra.Command{
Use: "init",
Short: "Initialize the database",
Long: `Initialize the database for a given DSN, executing the migrations and loading the custom defaults if any.
This command should be run using a database configuration DSN.`,
Example: ` # you can use the config flag to pass the DSN
$ mattermost db init --config postgres://localhost/mattermost
# or you can use the MM_CONFIG environment variable
$ MM_CONFIG=postgres://localhost/mattermost mattermost db init
# and you can set a custom defaults file to be loaded into the database
$ MM_CUSTOM_DEFAULTS_PATH=custom.json MM_CONFIG=postgres://localhost/mattermost mattermost db init`,
Args: cobra.NoArgs,
RunE: initDbCmdF,
}
var ResetCmd = &cobra.Command{
Use: "reset",
Short: "Reset the database to initial state",
Long: "Completely erases the database causing the loss of all data. This will reset Mattermost to its initial state.",
RunE: resetCmdF,
}
var MigrateCmd = &cobra.Command{
Use: "migrate",
Short: "Migrate the database if there are any unapplied migrations",
Long: "Run the missing migrations from the migrations table.",
RunE: migrateCmdF,
}
var DBVersionCmd = &cobra.Command{
Use: "version",
Short: "Returns the recent applied version number",
RunE: dbVersionCmdF,
}
func init() {
ResetCmd.Flags().Bool("confirm", false, "Confirm you really want to delete everything and a DB backup has been performed.")
DBVersionCmd.Flags().Bool("all", false, "Returns all applied migrations")
DbCmd.AddCommand(
InitDbCmd,
ResetCmd,
MigrateCmd,
DBVersionCmd,
)
RootCmd.AddCommand(
DbCmd,
)
}
func initDbCmdF(command *cobra.Command, _ []string) error {
dsn := getConfigDSN(command, config.GetEnvironment())
if !config.IsDatabaseDSN(dsn) {
return errors.New("this command should be run using a database configuration DSN")
}
customDefaults, err := loadCustomDefaults()
if err != nil {
return errors.Wrap(err, "error loading custom configuration defaults")
}
configStore, err := config.NewStoreFromDSN(getConfigDSN(command, config.GetEnvironment()), false, customDefaults, true)
if err != nil {
return errors.Wrap(err, "failed to load configuration")
}
defer configStore.Close()
sqlStore := sqlstore.New(configStore.Get().SqlSettings, nil)
defer sqlStore.Close()
fmt.Println("Database store correctly initialised")
return nil
}
func resetCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command, app.SkipPostInitialization())
if err != nil {
return err
}
defer a.Srv().Shutdown()
confirmFlag, _ := command.Flags().GetBool("confirm")
if !confirmFlag {
var confirm string
CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ")
fmt.Scanln(&confirm)
if confirm != "YES" {
return errors.New("ABORTED: You did not answer YES exactly, in all capitals.")
}
CommandPrettyPrintln("Are you sure you want to delete everything? All data will be permanently deleted? (YES/NO): ")
fmt.Scanln(&confirm)
if confirm != "YES" {
return errors.New("ABORTED: You did not answer YES exactly, in all capitals.")
}
}
a.Srv().Store().DropAllTables()
CommandPrettyPrintln("Database successfully reset")
auditRec := a.MakeAuditRecord("reset", audit.Success)
a.LogAuditRec(auditRec, nil)
return nil
}
func migrateCmdF(command *cobra.Command, args []string) error {
cfgDSN := getConfigDSN(command, config.GetEnvironment())
cfgStore, err := config.NewStoreFromDSN(cfgDSN, true, nil, true)
if err != nil {
return errors.Wrap(err, "failed to load configuration")
}
config := cfgStore.Get()
store := sqlstore.New(config.SqlSettings, nil)
defer store.Close()
CommandPrettyPrintln("Database successfully migrated")
return nil
}
func dbVersionCmdF(command *cobra.Command, args []string) error {
cfgDSN := getConfigDSN(command, config.GetEnvironment())
cfgStore, err := config.NewStoreFromDSN(cfgDSN, true, nil, true)
if err != nil {
return errors.Wrap(err, "failed to load configuration")
}
config := cfgStore.Get()
store := sqlstore.New(config.SqlSettings, nil)
defer store.Close()
allFlag, _ := command.Flags().GetBool("all")
if allFlag {
applied, err2 := store.GetAppliedMigrations()
if err2 != nil {
return errors.Wrap(err2, "failed to get applied migrations")
}
for _, migration := range applied {
CommandPrettyPrintln(fmt.Sprintf("Varsion: %d, Name: %s", migration.Version, migration.Name))
}
return nil
}
v, err := store.GetDBSchemaVersion()
if err != nil {
return errors.Wrap(err, "failed to get schema version")
}
CommandPrettyPrintln("Current database schema version is: " + strconv.Itoa(v))
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
var ExportCmd = &cobra.Command{
Use: "export",
Short: "Export data from Mattermost",
Long: "Export data from Mattermost in a format suitable for import into a third-party application or another Mattermost instance",
}
var ScheduleExportCmd = &cobra.Command{
Use: "schedule",
Short: "Schedule an export data job in Mattermost",
Long: "Schedule an export data job in Mattermost (this will run asynchronously via a background worker)",
Example: "export schedule --format=actiance --exportFrom=12345 --timeoutSeconds=12345",
RunE: scheduleExportCmdF,
}
var CsvExportCmd = &cobra.Command{
Use: "csv",
Short: "Export data from Mattermost in CSV format",
Long: "Export data from Mattermost in CSV format",
Example: "export csv --exportFrom=12345",
RunE: buildExportCmdF("csv"),
}
var ActianceExportCmd = &cobra.Command{
Use: "actiance",
Short: "Export data from Mattermost in Actiance format",
Long: "Export data from Mattermost in Actiance format",
Example: "export actiance --exportFrom=12345",
RunE: buildExportCmdF("actiance"),
}
var GlobalRelayZipExportCmd = &cobra.Command{
Use: "global-relay-zip",
Short: "Export data from Mattermost into a zip file containing emails to send to Global Relay for debug and testing purposes only.",
Long: "Export data from Mattermost into a zip file containing emails to send to Global Relay for debug and testing purposes only. This does not archive any information in Global Relay.",
Example: "export global-relay-zip --exportFrom=12345",
RunE: buildExportCmdF("globalrelay-zip"),
}
var BulkExportCmd = &cobra.Command{
Use: "bulk [file]",
Short: "Export bulk data.",
Long: "Export data to a file compatible with the Mattermost Bulk Import format.",
Example: "export bulk bulk_data.json",
RunE: bulkExportCmdF,
Args: cobra.ExactArgs(1),
}
func init() {
ScheduleExportCmd.Flags().String("format", "actiance", "The format to export data")
ScheduleExportCmd.Flags().Int64("exportFrom", -1, "The timestamp of the earliest post to export, expressed in seconds since the unix epoch.")
ScheduleExportCmd.Flags().Int("timeoutSeconds", -1, "The maximum number of seconds to wait for the job to complete before timing out.")
CsvExportCmd.Flags().Int64("exportFrom", -1, "The timestamp of the earliest post to export, expressed in seconds since the unix epoch.")
CsvExportCmd.Flags().Int("limit", -1, "The number of posts to export. The default of -1 means no limit.")
ActianceExportCmd.Flags().Int64("exportFrom", -1, "The timestamp of the earliest post to export, expressed in seconds since the unix epoch.")
ActianceExportCmd.Flags().Int("limit", -1, "The number of posts to export. The default of -1 means no limit.")
GlobalRelayZipExportCmd.Flags().Int64("exportFrom", -1, "The timestamp of the earliest post to export, expressed in seconds since the unix epoch.")
GlobalRelayZipExportCmd.Flags().Int("limit", -1, "The number of posts to export. The default of -1 means no limit.")
BulkExportCmd.Flags().Bool("all-teams", true, "Export all teams from the server.")
BulkExportCmd.Flags().Bool("attachments", false, "Also export file attachments.")
BulkExportCmd.Flags().Bool("archive", false, "Outputs a single archive file.")
ExportCmd.AddCommand(ScheduleExportCmd)
ExportCmd.AddCommand(CsvExportCmd)
ExportCmd.AddCommand(ActianceExportCmd)
ExportCmd.AddCommand(GlobalRelayZipExportCmd)
ExportCmd.AddCommand(BulkExportCmd)
RootCmd.AddCommand(ExportCmd)
}
func scheduleExportCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command, app.SkipPostInitialization())
if err != nil {
return err
}
defer a.Srv().Shutdown()
if !*a.Config().MessageExportSettings.EnableExport {
return errors.New("ERROR: The message export feature is not enabled")
}
// for now, format is hard-coded to actiance. In time, we'll have to support other formats and inject them into job data
format, err := command.Flags().GetString("format")
if err != nil {
return errors.New("format flag error")
}
if format != "actiance" {
return errors.New("unsupported export format")
}
startTime, err := command.Flags().GetInt64("exportFrom")
if err != nil {
return errors.New("exportFrom flag error")
}
if startTime < 0 {
return errors.New("exportFrom must be a positive integer")
}
timeoutSeconds, err := command.Flags().GetInt("timeoutSeconds")
if err != nil {
return errors.New("timeoutSeconds error")
}
if timeoutSeconds < 0 {
return errors.New("timeoutSeconds must be a positive integer")
}
if messageExportI := a.MessageExport(); messageExportI != nil {
ctx := context.Background()
if timeoutSeconds > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Second*time.Duration(timeoutSeconds))
defer cancel()
}
job, err := messageExportI.StartSynchronizeJob(ctx, startTime)
if err != nil || job.Status == model.JobStatusError || job.Status == model.JobStatusCanceled {
CommandPrintErrorln("ERROR: Message export job failed. Please check the server logs")
} else {
CommandPrettyPrintln("SUCCESS: Message export job complete")
auditRec := a.MakeAuditRecord("scheduleExport", audit.Success)
auditRec.AddMeta("format", format)
auditRec.AddMeta("start", startTime)
a.LogAuditRec(auditRec, nil)
}
}
return nil
}
func buildExportCmdF(format string) func(command *cobra.Command, args []string) error {
return func(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command, app.SkipPostInitialization())
license := a.Srv().License()
if err != nil {
return err
}
defer a.Srv().Shutdown()
startTime, err := command.Flags().GetInt64("exportFrom")
if err != nil {
return errors.New("exportFrom flag error")
}
if startTime < 0 {
return errors.New("exportFrom must be a positive integer")
}
limit, err := command.Flags().GetInt("limit")
if err != nil {
return errors.New("limit flag error")
}
if a.MessageExport() == nil || license == nil || !*license.Features.MessageExport {
return errors.New("message export feature not available")
}
warningsCount, appErr := a.MessageExport().RunExport(format, startTime, limit)
if appErr != nil {
return appErr
}
if warningsCount == 0 {
CommandPrettyPrintln("SUCCESS: Your data was exported.")
} else {
if format == model.ComplianceExportTypeGlobalrelay || format == model.ComplianceExportTypeGlobalrelayZip {
CommandPrettyPrintln(fmt.Sprintf("WARNING: %d warnings encountered, see logs for details.", warningsCount))
} else {
CommandPrettyPrintln(fmt.Sprintf("WARNING: %d warnings encountered, see warning.txt for details.", warningsCount))
}
}
auditRec := a.MakeAuditRecord("buildExport", audit.Success)
auditRec.AddMeta("format", format)
auditRec.AddMeta("start", startTime)
a.LogAuditRec(auditRec, nil)
return nil
}
}
func bulkExportCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command, app.SkipPostInitialization())
if err != nil {
return err
}
defer a.Srv().Shutdown()
allTeams, err := command.Flags().GetBool("all-teams")
if err != nil {
return errors.Wrap(err, "all-teams flag error")
}
if !allTeams {
return errors.New("Nothing to export. Please specify the --all-teams flag to export all teams.")
}
attachments, err := command.Flags().GetBool("attachments")
if err != nil {
return errors.Wrap(err, "attachments flag error")
}
archive, err := command.Flags().GetBool("archive")
if err != nil {
return errors.Wrap(err, "archive flag error")
}
fileWriter, err := os.Create(args[0])
if err != nil {
return err
}
defer fileWriter.Close()
outPath, err := filepath.Abs(args[0])
if err != nil {
return err
}
var opts model.BulkExportOpts
opts.IncludeAttachments = attachments
opts.CreateArchive = archive
if err := a.BulkExport(request.EmptyContext(a.Log()), fileWriter, filepath.Dir(outPath), nil /* nil job since it's spawned from CLI */, opts); err != nil {
CommandPrintErrorln(err.Error())
return err
}
auditRec := a.MakeAuditRecord("bulkExport", audit.Success)
auditRec.AddMeta("all_teams", allTeams)
auditRec.AddMeta("file", args[0])
a.LogAuditRec(auditRec, nil)
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"errors"
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
)
var ImportCmd = &cobra.Command{
Use: "import",
Short: "Import data.",
}
var SlackImportCmd = &cobra.Command{
Use: "slack [team] [file]",
Short: "Import a team from Slack.",
Long: "Import a team from a Slack export zip file.",
Example: " import slack myteam slack_export.zip",
RunE: slackImportCmdF,
}
var BulkImportCmd = &cobra.Command{
Use: "bulk [file]",
Short: "Import bulk data.",
Long: "Import data from a Mattermost Bulk Import File.",
Example: " import bulk bulk_data.json",
RunE: bulkImportCmdF,
}
func init() {
BulkImportCmd.Flags().Bool("apply", false, "Save the import data to the database. Use with caution - this cannot be reverted.")
BulkImportCmd.Flags().Bool("validate", false, "Validate the import data without making any changes to the system.")
BulkImportCmd.Flags().Int("workers", 2, "How many workers to run whilst doing the import.")
BulkImportCmd.Flags().String("import-path", "", "A path to the data directory to import files from.")
ImportCmd.AddCommand(
BulkImportCmd,
SlackImportCmd,
)
RootCmd.AddCommand(ImportCmd)
}
func slackImportCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command)
if err != nil {
return err
}
defer a.Srv().Shutdown()
if len(args) != 2 {
return errors.New("Incorrect number of arguments.")
}
team := getTeamFromTeamArg(a, args[0])
if team == nil {
return errors.New("Unable to find team '" + args[0] + "'")
}
fileReader, err := os.Open(args[1])
if err != nil {
return err
}
defer fileReader.Close()
fileInfo, err := fileReader.Stat()
if err != nil {
return err
}
CommandPrettyPrintln("Running Slack Import. This may take a long time for large teams or teams with many messages.")
importErr, log := a.SlackImport(request.EmptyContext(a.Log()), fileReader, fileInfo.Size(), team.Id)
if importErr != nil {
return err
}
CommandPrettyPrintln("")
CommandPrintln(log.String())
CommandPrettyPrintln("")
CommandPrettyPrintln("Finished Slack Import.")
CommandPrettyPrintln("")
auditRec := a.MakeAuditRecord("slackImport", audit.Success)
auditRec.AddMeta("team", team)
auditRec.AddMeta("file", args[1])
a.LogAuditRec(auditRec, nil)
return nil
}
func bulkImportCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command)
if err != nil {
return err
}
defer a.Srv().Shutdown()
apply, err := command.Flags().GetBool("apply")
if err != nil {
return errors.New("Apply flag error")
}
validate, err := command.Flags().GetBool("validate")
if err != nil {
return errors.New("Validate flag error")
}
workers, err := command.Flags().GetInt("workers")
if err != nil {
return errors.New("Workers flag error")
}
importPath, err := command.Flags().GetString("import-path")
if err != nil {
return errors.New("import-path flag error")
}
if len(args) != 1 {
return errors.New("Incorrect number of arguments.")
}
fileReader, err := os.Open(args[0])
if err != nil {
return err
}
defer fileReader.Close()
if apply && validate {
CommandPrettyPrintln("Use only one of --apply or --validate.")
return nil
}
if apply && !validate {
CommandPrettyPrintln("Running Bulk Import. This may take a long time.")
} else {
CommandPrettyPrintln("Running Bulk Import Data Validation.")
CommandPrettyPrintln("** This checks the validity of the entities in the data file, but does not persist any changes **")
CommandPrettyPrintln("Use the --apply flag to perform the actual data import.")
}
CommandPrettyPrintln("")
if err, lineNumber := a.BulkImportWithPath(request.EmptyContext(a.Log()), fileReader, nil, !apply, workers, importPath); err != nil {
CommandPrintErrorln(err.Error())
if lineNumber != 0 {
CommandPrintErrorln(fmt.Sprintf("Error occurred on data file line %v", lineNumber))
}
return err
}
if apply {
CommandPrettyPrintln("Finished Bulk Import.")
auditRec := a.MakeAuditRecord("bulkImport", audit.Success)
auditRec.AddMeta("file", args[0])
a.LogAuditRec(auditRec, nil)
} else {
CommandPrettyPrintln("Validation complete. You can now perform the import by rerunning this command with the --apply flag.")
}
return nil
}
func getTeamFromTeamArg(a *app.App, teamArg string) *model.Team {
var team *model.Team
team, err := a.Srv().Store().Team().GetByName(teamArg)
if err != nil {
var t *model.Team
if t, err = a.Srv().Store().Team().Get(teamArg); err == nil {
team = t
}
}
return team
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
func initDBCommandContextCobra(command *cobra.Command, readOnlyConfigStore bool, options ...app.Option) (*app.App, error) {
a, err := initDBCommandContext(getConfigDSN(command, config.GetEnvironment()), readOnlyConfigStore, options...)
if err != nil {
// Returning an error just prints the usage message, so actually panic
panic(err)
}
a.InitPlugins(request.EmptyContext(a.Log()), *a.Config().PluginSettings.Directory, *a.Config().PluginSettings.ClientDirectory)
a.DoAppMigrations()
return a, nil
}
func InitDBCommandContextCobra(command *cobra.Command, options ...app.Option) (*app.App, error) {
return initDBCommandContextCobra(command, true, options...)
}
func initDBCommandContext(configDSN string, readOnlyConfigStore bool, options ...app.Option) (*app.App, error) {
if err := utils.TranslationsPreInit(); err != nil {
return nil, err
}
model.AppErrorInit(i18n.T)
// The option order is important as app.Config option reads app.StartMetrics option.
options = append(options, app.Config(configDSN, readOnlyConfigStore, nil))
s, err := app.NewServer(options...)
if err != nil {
return nil, err
}
a := app.New(app.ServerConnector(s.Channels()))
if model.BuildEnterpriseReady == "true" {
a.Srv().LoadLicense()
}
return a, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"os"
"os/signal"
"syscall"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var JobserverCmd = &cobra.Command{
Use: "jobserver",
Short: "Start the Mattermost job server",
RunE: jobserverCmdF,
}
func init() {
JobserverCmd.Flags().Bool("nojobs", false, "Do not run jobs on this jobserver.")
JobserverCmd.Flags().Bool("noschedule", false, "Do not schedule jobs from this jobserver.")
RootCmd.AddCommand(JobserverCmd)
}
func jobserverCmdF(command *cobra.Command, args []string) error {
// Options
noJobs, _ := command.Flags().GetBool("nojobs")
noSchedule, _ := command.Flags().GetBool("noschedule")
// Initialize
a, err := initDBCommandContext(getConfigDSN(command, config.GetEnvironment()), false, app.StartMetrics)
if err != nil {
return err
}
defer a.Srv().Shutdown()
a.Srv().LoadLicense()
// Run jobs
mlog.Info("Starting Mattermost job server")
defer mlog.Info("Stopped Mattermost job server")
if !noJobs {
a.Srv().Jobs.StartWorkers()
defer a.Srv().Jobs.StopWorkers()
}
if !noSchedule {
a.Srv().Jobs.StartSchedulers()
defer a.Srv().Jobs.StopSchedulers()
}
if !noJobs || !noSchedule {
auditRec := a.MakeAuditRecord("jobServer", audit.Success)
a.LogAuditRec(auditRec, nil)
}
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
<-signalChan
// Cleanup anything that isn't handled by a defer statement
mlog.Info("Stopping Mattermost job server")
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"fmt"
"os"
)
func CommandPrintln(a ...any) (int, error) {
return fmt.Println(a...)
}
func CommandPrintErrorln(a ...any) (int, error) {
return fmt.Fprintln(os.Stderr, a...)
}
func CommandPrettyPrintln(a ...any) (int, error) {
return fmt.Fprintln(os.Stdout, a...)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"github.com/spf13/cobra"
)
type Command = cobra.Command
func Run(args []string) error {
RootCmd.SetArgs(args)
return RootCmd.Execute()
}
var RootCmd = &cobra.Command{
Use: "mattermost",
Short: "Open source, self-hosted Slack-alternative",
Long: `Mattermost offers workplace messaging across web, PC and phones with archiving, search and integration with your existing systems. Documentation available at https://docs.mattermost.com`,
}
func init() {
RootCmd.PersistentFlags().StringP("config", "c", "", "Configuration file to use.")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"bytes"
"net"
"os"
"os/signal"
"runtime/debug"
"runtime/pprof"
"syscall"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/server/channels/api4"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/manualtesting"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
"github.com/mattermost/mattermost-server/v6/server/channels/wsapi"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var serverCmd = &cobra.Command{
Use: "server",
Short: "Run the Mattermost server",
RunE: serverCmdF,
SilenceUsage: true,
}
func init() {
RootCmd.AddCommand(serverCmd)
RootCmd.RunE = serverCmdF
}
func serverCmdF(command *cobra.Command, args []string) error {
interruptChan := make(chan os.Signal, 1)
if err := utils.TranslationsPreInit(); err != nil {
return errors.Wrap(err, "unable to load Mattermost translation files")
}
customDefaults, err := loadCustomDefaults()
if err != nil {
mlog.Warn("Error loading custom configuration defaults: " + err.Error())
}
configStore, err := config.NewStoreFromDSN(getConfigDSN(command, config.GetEnvironment()), false, customDefaults, true)
if err != nil {
return errors.Wrap(err, "failed to load configuration")
}
defer configStore.Close()
return runServer(configStore, interruptChan)
}
func runServer(configStore *config.Store, interruptChan chan os.Signal) error {
// Setting the highest traceback level from the code.
// This is done to print goroutines from all threads (see golang.org/issue/13161)
// and also preserve a crash dump for later investigation.
debug.SetTraceback("crash")
options := []app.Option{
// The option order is important as app.Config option reads app.StartMetrics option.
app.StartMetrics,
app.ConfigStore(configStore),
app.RunEssentialJobs,
app.JoinCluster,
}
server, err := app.NewServer(options...)
if err != nil {
mlog.Error(err.Error())
return err
}
defer server.Shutdown()
// We add this after shutdown so that it can be called
// before server shutdown happens as it can close
// the advanced logger and prevent the mlog call from working properly.
defer func() {
// A panic pass-through layer which just logs it
// and sends it upwards.
if x := recover(); x != nil {
var buf bytes.Buffer
pprof.Lookup("goroutine").WriteTo(&buf, 2)
mlog.Error("A panic occurred",
mlog.Any("error", x),
mlog.String("stack", buf.String()))
panic(x)
}
}()
api, err := api4.Init(server)
if err != nil {
mlog.Error(err.Error())
return err
}
wsapi.Init(server)
web.New(server)
err = server.Start()
if err != nil {
mlog.Error(err.Error())
return err
}
// If we allow testing then listen for manual testing URL hits
if *server.Config().ServiceSettings.EnableTesting {
manualtesting.Init(api)
}
notifyReady()
// wait for kill signal before attempting to gracefully shutdown
// the running service
signal.Notify(interruptChan, syscall.SIGINT, syscall.SIGTERM)
<-interruptChan
return nil
}
func notifyReady() {
// If the environment vars provide a systemd notification socket,
// notify systemd that the server is ready.
systemdSocket := os.Getenv("NOTIFY_SOCKET")
if systemdSocket != "" {
mlog.Info("Sending systemd READY notification.")
err := sendSystemdReadyNotification(systemdSocket)
if err != nil {
mlog.Error(err.Error())
}
}
}
func sendSystemdReadyNotification(socketPath string) error {
msg := "READY=1"
addr := &net.UnixAddr{
Name: socketPath,
Net: "unixgram",
}
conn, err := net.DialUnix(addr.Net, nil, addr)
if err != nil {
return err
}
defer conn.Close()
_, err = conn.Write([]byte(msg))
return err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"bufio"
"fmt"
"os"
"os/exec"
"os/signal"
"syscall"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/api4"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/wsapi"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
var TestCmd = &cobra.Command{
Use: "test",
Short: "Testing Commands",
Hidden: true,
}
var RunWebClientTestsCmd = &cobra.Command{
Use: "web_client_tests",
Short: "Run the web client tests",
RunE: webClientTestsCmdF,
}
var RunServerForWebClientTestsCmd = &cobra.Command{
Use: "web_client_tests_server",
Short: "Run the server configured for running the web client tests against it",
RunE: serverForWebClientTestsCmdF,
}
func init() {
TestCmd.AddCommand(
RunWebClientTestsCmd,
RunServerForWebClientTestsCmd,
)
RootCmd.AddCommand(TestCmd)
}
func webClientTestsCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command, app.StartMetrics)
if err != nil {
return err
}
defer a.Srv().Shutdown()
i18n.InitTranslations(*a.Config().LocalizationSettings.DefaultServerLocale, *a.Config().LocalizationSettings.DefaultClientLocale)
serverErr := a.Srv().Start()
if serverErr != nil {
return serverErr
}
_, err = api4.Init(a.Srv())
if err != nil {
return err
}
wsapi.Init(a.Srv())
a.UpdateConfig(setupClientTests)
runWebClientTests()
return nil
}
func serverForWebClientTestsCmdF(command *cobra.Command, args []string) error {
a, err := InitDBCommandContextCobra(command, app.StartMetrics)
if err != nil {
return err
}
defer a.Srv().Shutdown()
i18n.InitTranslations(*a.Config().LocalizationSettings.DefaultServerLocale, *a.Config().LocalizationSettings.DefaultClientLocale)
serverErr := a.Srv().Start()
if serverErr != nil {
return serverErr
}
_, err = api4.Init(a.Srv())
if err != nil {
return err
}
wsapi.Init(a.Srv())
a.UpdateConfig(setupClientTests)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
<-c
return nil
}
func setupClientTests(cfg *model.Config) {
*cfg.TeamSettings.EnableOpenServer = true
*cfg.ServiceSettings.EnableCommands = false
*cfg.ServiceSettings.EnableCustomEmoji = true
*cfg.ServiceSettings.EnableIncomingWebhooks = false
*cfg.ServiceSettings.EnableOutgoingWebhooks = false
}
func executeTestCommand(command *exec.Cmd) {
cmdOutPipe, err := command.StdoutPipe()
if err != nil {
CommandPrintErrorln("Failed to run tests")
os.Exit(1)
return
}
cmdErrOutPipe, err := command.StderrPipe()
if err != nil {
CommandPrintErrorln("Failed to run tests")
os.Exit(1)
return
}
cmdOutReader := bufio.NewScanner(cmdOutPipe)
cmdErrOutReader := bufio.NewScanner(cmdErrOutPipe)
go func() {
for cmdOutReader.Scan() {
fmt.Println(cmdOutReader.Text())
}
}()
go func() {
for cmdErrOutReader.Scan() {
fmt.Println(cmdErrOutReader.Text())
}
}()
if err := command.Run(); err != nil {
CommandPrintErrorln("Client Tests failed")
os.Exit(1)
return
}
}
func runWebClientTests() {
if webappDir := os.Getenv("WEBAPP_DIR"); webappDir != "" {
os.Chdir(webappDir)
} else {
os.Chdir("../mattermost-webapp")
}
cmd := exec.Command("npm", "test")
executeTestCommand(cmd)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"bytes"
"encoding/json"
"fmt"
"os"
"reflect"
"sort"
"strings"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/model"
)
const CustomDefaultsEnvVar = "MM_CUSTOM_DEFAULTS_PATH"
// printStringMap takes a reflect.Value and prints it out alphabetically based on key values, which must be strings.
// This is done recursively if it's a map, and uses the given tab settings.
func printStringMap(value reflect.Value, tabVal int) string {
out := &bytes.Buffer{}
var sortedKeys []string
stringToKeyMap := make(map[string]reflect.Value)
for _, k := range value.MapKeys() {
sortedKeys = append(sortedKeys, k.String())
stringToKeyMap[k.String()] = k
}
sort.Strings(sortedKeys)
for _, keyString := range sortedKeys {
key := stringToKeyMap[keyString]
val := value.MapIndex(key)
if newVal, ok := val.Interface().(map[string]any); !ok {
fmt.Fprintf(out, "%s", strings.Repeat("\t", tabVal))
fmt.Fprintf(out, "%v: \"%v\"\n", key.Interface(), val.Interface())
} else {
fmt.Fprintf(out, "%s", strings.Repeat("\t", tabVal))
fmt.Fprintf(out, "%v:\n", key.Interface())
// going one level in, increase the tab
tabVal++
fmt.Fprintf(out, "%s", printStringMap(reflect.ValueOf(newVal), tabVal))
// coming back one level, decrease the tab
tabVal--
}
}
return out.String()
}
func getConfigDSN(command *cobra.Command, env map[string]string) string {
configDSN, _ := command.Flags().GetString("config")
// Config not supplied in flag, check env
if configDSN == "" {
configDSN = env["MM_CONFIG"]
}
// Config not supplied in env or flag use default
if configDSN == "" {
configDSN = "config.json"
}
return configDSN
}
func loadCustomDefaults() (*model.Config, error) {
customDefaultsPath := os.Getenv(CustomDefaultsEnvVar)
if customDefaultsPath == "" {
return nil, nil
}
file, err := os.Open(customDefaultsPath)
if err != nil {
return nil, fmt.Errorf("unable to open custom defaults file at %q: %w", customDefaultsPath, err)
}
defer file.Close()
var customDefaults *model.Config
err = json.NewDecoder(file).Decode(&customDefaults)
if err != nil {
return nil, fmt.Errorf("unable to decode custom defaults configuration: %w", err)
}
return customDefaults, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"github.com/spf13/cobra"
"github.com/mattermost/mattermost-server/v6/model"
)
var VersionCmd = &cobra.Command{
Use: "version",
Short: "Display version information",
RunE: versionCmdF,
}
func init() {
VersionCmd.Flags().Bool("skip-server-start", false, "Skip the server initialization and return the Mattermost version without the DB version.")
VersionCmd.Flags().MarkDeprecated("skip-server-start", "This flag is not necessary anymore and the flag will be removed in the future releases. Consider removing it from your scripts.")
RootCmd.AddCommand(VersionCmd)
}
func versionCmdF(command *cobra.Command, args []string) error {
CommandPrintln("Version: " + model.CurrentVersion)
CommandPrintln("Build Number: " + model.BuildNumber)
CommandPrintln("Build Date: " + model.BuildDate)
CommandPrintln("Build Hash: " + model.BuildHash)
CommandPrintln("Build Enterprise Ready: " + model.BuildEnterpriseReady)
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"fmt"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
)
// GenerateClientConfig renders the given configuration for a client.
func GenerateClientConfig(c *model.Config, telemetryID string, license *model.License) map[string]string {
props := GenerateLimitedClientConfig(c, telemetryID, license)
props["EnableCustomUserStatuses"] = strconv.FormatBool(*c.TeamSettings.EnableCustomUserStatuses)
props["EnableLastActiveTime"] = strconv.FormatBool(*c.TeamSettings.EnableLastActiveTime)
props["EnableUserDeactivation"] = strconv.FormatBool(*c.TeamSettings.EnableUserDeactivation)
props["RestrictDirectMessage"] = *c.TeamSettings.RestrictDirectMessage
props["TeammateNameDisplay"] = *c.TeamSettings.TeammateNameDisplay
props["LockTeammateNameDisplay"] = strconv.FormatBool(*c.TeamSettings.LockTeammateNameDisplay)
props["ExperimentalPrimaryTeam"] = *c.TeamSettings.ExperimentalPrimaryTeam
props["ExperimentalViewArchivedChannels"] = strconv.FormatBool(*c.TeamSettings.ExperimentalViewArchivedChannels)
props["EnableBotAccountCreation"] = strconv.FormatBool(*c.ServiceSettings.EnableBotAccountCreation)
props["EnableOAuthServiceProvider"] = strconv.FormatBool(*c.ServiceSettings.EnableOAuthServiceProvider)
props["GoogleDeveloperKey"] = *c.ServiceSettings.GoogleDeveloperKey
props["EnableIncomingWebhooks"] = strconv.FormatBool(*c.ServiceSettings.EnableIncomingWebhooks)
props["EnableOutgoingWebhooks"] = strconv.FormatBool(*c.ServiceSettings.EnableOutgoingWebhooks)
props["EnableCommands"] = strconv.FormatBool(*c.ServiceSettings.EnableCommands)
props["EnablePostUsernameOverride"] = strconv.FormatBool(*c.ServiceSettings.EnablePostUsernameOverride)
props["EnablePostIconOverride"] = strconv.FormatBool(*c.ServiceSettings.EnablePostIconOverride)
props["EnableUserAccessTokens"] = strconv.FormatBool(*c.ServiceSettings.EnableUserAccessTokens)
props["EnableLinkPreviews"] = strconv.FormatBool(*c.ServiceSettings.EnableLinkPreviews)
props["EnablePermalinkPreviews"] = strconv.FormatBool(*c.ServiceSettings.EnablePermalinkPreviews)
props["EnableTesting"] = strconv.FormatBool(*c.ServiceSettings.EnableTesting)
props["EnableDeveloper"] = strconv.FormatBool(*c.ServiceSettings.EnableDeveloper)
props["EnableClientPerformanceDebugging"] = strconv.FormatBool(*c.ServiceSettings.EnableClientPerformanceDebugging)
props["PostEditTimeLimit"] = fmt.Sprintf("%v", *c.ServiceSettings.PostEditTimeLimit)
props["MinimumHashtagLength"] = fmt.Sprintf("%v", *c.ServiceSettings.MinimumHashtagLength)
props["EnablePreviewFeatures"] = strconv.FormatBool(*c.ServiceSettings.EnablePreviewFeatures)
props["EnableTutorial"] = strconv.FormatBool(*c.ServiceSettings.EnableTutorial)
props["EnableOnboardingFlow"] = strconv.FormatBool(*c.ServiceSettings.EnableOnboardingFlow)
props["ExperimentalEnableDefaultChannelLeaveJoinMessages"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages)
props["ExperimentalGroupUnreadChannels"] = *c.ServiceSettings.ExperimentalGroupUnreadChannels
props["EnableSVGs"] = strconv.FormatBool(*c.ServiceSettings.EnableSVGs)
props["EnableMarketplace"] = strconv.FormatBool(*c.PluginSettings.EnableMarketplace)
props["EnableLatex"] = strconv.FormatBool(*c.ServiceSettings.EnableLatex)
props["EnableInlineLatex"] = strconv.FormatBool(*c.ServiceSettings.EnableInlineLatex)
props["ExtendSessionLengthWithActivity"] = strconv.FormatBool(*c.ServiceSettings.ExtendSessionLengthWithActivity)
props["ManagedResourcePaths"] = *c.ServiceSettings.ManagedResourcePaths
// This setting is only temporary, so keep using the old setting name for the mobile and web apps
props["ExperimentalEnablePostMetadata"] = "true"
props["EnableAppBar"] = strconv.FormatBool(*c.ExperimentalSettings.EnableAppBar)
props["ExperimentalEnableAutomaticReplies"] = strconv.FormatBool(*c.TeamSettings.ExperimentalEnableAutomaticReplies)
props["ExperimentalTimezone"] = strconv.FormatBool(*c.DisplaySettings.ExperimentalTimezone)
props["SendEmailNotifications"] = strconv.FormatBool(*c.EmailSettings.SendEmailNotifications)
props["SendPushNotifications"] = strconv.FormatBool(*c.EmailSettings.SendPushNotifications)
props["RequireEmailVerification"] = strconv.FormatBool(*c.EmailSettings.RequireEmailVerification)
props["EnableEmailBatching"] = strconv.FormatBool(*c.EmailSettings.EnableEmailBatching)
props["EnablePreviewModeBanner"] = strconv.FormatBool(*c.EmailSettings.EnablePreviewModeBanner)
props["EmailNotificationContentsType"] = *c.EmailSettings.EmailNotificationContentsType
props["ShowEmailAddress"] = strconv.FormatBool(*c.PrivacySettings.ShowEmailAddress)
props["ShowFullName"] = strconv.FormatBool(*c.PrivacySettings.ShowFullName)
props["EnableFileAttachments"] = strconv.FormatBool(*c.FileSettings.EnableFileAttachments)
props["EnablePublicLink"] = strconv.FormatBool(*c.FileSettings.EnablePublicLink)
props["AvailableLocales"] = *c.LocalizationSettings.AvailableLocales
props["SQLDriverName"] = *c.SqlSettings.DriverName
props["EnableEmojiPicker"] = strconv.FormatBool(*c.ServiceSettings.EnableEmojiPicker)
props["EnableGifPicker"] = strconv.FormatBool(*c.ServiceSettings.EnableGifPicker)
props["GfycatApiKey"] = *c.ServiceSettings.GfycatAPIKey
props["GfycatApiSecret"] = *c.ServiceSettings.GfycatAPISecret
props["MaxFileSize"] = strconv.FormatInt(*c.FileSettings.MaxFileSize, 10)
props["MaxNotificationsPerChannel"] = strconv.FormatInt(*c.TeamSettings.MaxNotificationsPerChannel, 10)
props["EnableConfirmNotificationsToChannel"] = strconv.FormatBool(*c.TeamSettings.EnableConfirmNotificationsToChannel)
props["TimeBetweenUserTypingUpdatesMilliseconds"] = strconv.FormatInt(*c.ServiceSettings.TimeBetweenUserTypingUpdatesMilliseconds, 10)
props["EnableUserTypingMessages"] = strconv.FormatBool(*c.ServiceSettings.EnableUserTypingMessages)
props["EnableChannelViewedMessages"] = strconv.FormatBool(*c.ServiceSettings.EnableChannelViewedMessages)
props["RunJobs"] = strconv.FormatBool(*c.JobSettings.RunJobs)
props["EnableEmailInvitations"] = strconv.FormatBool(*c.ServiceSettings.EnableEmailInvitations)
props["CWSURL"] = *c.CloudSettings.CWSURL
// Set default values for all options that require a license.
props["ExperimentalEnableAuthenticationTransfer"] = "true"
props["LdapNicknameAttributeSet"] = "false"
props["LdapFirstNameAttributeSet"] = "false"
props["LdapLastNameAttributeSet"] = "false"
props["LdapPictureAttributeSet"] = "false"
props["LdapPositionAttributeSet"] = "false"
props["EnableCompliance"] = "false"
props["EnableMobileFileDownload"] = "true"
props["EnableMobileFileUpload"] = "true"
props["SamlFirstNameAttributeSet"] = "false"
props["SamlLastNameAttributeSet"] = "false"
props["SamlNicknameAttributeSet"] = "false"
props["SamlPositionAttributeSet"] = "false"
props["EnableCluster"] = "false"
props["EnableMetrics"] = "false"
props["EnableBanner"] = "false"
props["BannerText"] = ""
props["BannerColor"] = ""
props["BannerTextColor"] = ""
props["AllowBannerDismissal"] = "false"
props["EnableThemeSelection"] = "true"
props["DefaultTheme"] = ""
props["AllowCustomThemes"] = "true"
props["AllowedThemes"] = ""
props["DataRetentionEnableMessageDeletion"] = "false"
props["DataRetentionMessageRetentionDays"] = "0"
props["DataRetentionEnableFileDeletion"] = "false"
props["DataRetentionFileRetentionDays"] = "0"
props["DataRetentionEnableBoardsDeletion"] = "false"
props["DataRetentionBoardsRetentionDays"] = "0"
props["CustomUrlSchemes"] = strings.Join(c.DisplaySettings.CustomURLSchemes, ",")
props["IsDefaultMarketplace"] = strconv.FormatBool(*c.PluginSettings.MarketplaceURL == model.PluginSettingsDefaultMarketplaceURL)
props["ExperimentalSharedChannels"] = "false"
props["CollapsedThreads"] = *c.ServiceSettings.CollapsedThreads
props["EnableCustomGroups"] = "false"
props["InsightsEnabled"] = strconv.FormatBool(c.FeatureFlags.InsightsEnabled)
props["PostPriority"] = strconv.FormatBool(*c.ServiceSettings.PostPriority)
props["AllowSyncedDrafts"] = strconv.FormatBool(*c.ServiceSettings.AllowSyncedDrafts)
if license != nil {
props["ExperimentalEnableAuthenticationTransfer"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableAuthenticationTransfer)
if *license.Features.LDAP {
props["LdapNicknameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.NicknameAttribute != "")
props["LdapFirstNameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.FirstNameAttribute != "")
props["LdapLastNameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.LastNameAttribute != "")
props["LdapPictureAttributeSet"] = strconv.FormatBool(*c.LdapSettings.PictureAttribute != "")
props["LdapPositionAttributeSet"] = strconv.FormatBool(*c.LdapSettings.PositionAttribute != "")
}
if *license.Features.Compliance {
props["EnableCompliance"] = strconv.FormatBool(*c.ComplianceSettings.Enable)
props["EnableMobileFileDownload"] = strconv.FormatBool(*c.FileSettings.EnableMobileDownload)
props["EnableMobileFileUpload"] = strconv.FormatBool(*c.FileSettings.EnableMobileUpload)
}
if *license.Features.SAML {
props["SamlFirstNameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.FirstNameAttribute != "")
props["SamlLastNameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.LastNameAttribute != "")
props["SamlNicknameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.NicknameAttribute != "")
props["SamlPositionAttributeSet"] = strconv.FormatBool(*c.SamlSettings.PositionAttribute != "")
}
if *license.Features.FutureFeatures {
props["ExperimentalClientSideCertEnable"] = strconv.FormatBool(*c.ExperimentalSettings.ClientSideCertEnable)
props["ExperimentalClientSideCertCheck"] = *c.ExperimentalSettings.ClientSideCertCheck
}
if *license.Features.Cluster {
props["EnableCluster"] = strconv.FormatBool(*c.ClusterSettings.Enable)
}
if *license.Features.Cluster {
props["EnableMetrics"] = strconv.FormatBool(*c.MetricsSettings.Enable)
}
if *license.Features.Announcement {
props["EnableBanner"] = strconv.FormatBool(*c.AnnouncementSettings.EnableBanner)
props["BannerText"] = *c.AnnouncementSettings.BannerText
props["BannerColor"] = *c.AnnouncementSettings.BannerColor
props["BannerTextColor"] = *c.AnnouncementSettings.BannerTextColor
props["AllowBannerDismissal"] = strconv.FormatBool(*c.AnnouncementSettings.AllowBannerDismissal)
}
if *license.Features.ThemeManagement {
props["EnableThemeSelection"] = strconv.FormatBool(*c.ThemeSettings.EnableThemeSelection)
props["DefaultTheme"] = *c.ThemeSettings.DefaultTheme
props["AllowCustomThemes"] = strconv.FormatBool(*c.ThemeSettings.AllowCustomThemes)
props["AllowedThemes"] = strings.Join(c.ThemeSettings.AllowedThemes, ",")
}
if *license.Features.DataRetention {
props["DataRetentionEnableMessageDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableMessageDeletion)
props["DataRetentionMessageRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.MessageRetentionDays), 10)
props["DataRetentionEnableFileDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableFileDeletion)
props["DataRetentionFileRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.FileRetentionDays), 10)
props["DataRetentionEnableBoardsDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableBoardsDeletion)
props["DataRetentionBoardsRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.BoardsRetentionDays), 10)
}
if license.HasSharedChannels() {
props["ExperimentalSharedChannels"] = strconv.FormatBool(*c.ExperimentalSettings.EnableSharedChannels)
props["ExperimentalRemoteClusterService"] = strconv.FormatBool(c.FeatureFlags.EnableRemoteClusterService && *c.ExperimentalSettings.EnableRemoteClusterService)
}
if license.SkuShortName == model.LicenseShortSkuProfessional || license.SkuShortName == model.LicenseShortSkuEnterprise {
props["EnableCustomGroups"] = strconv.FormatBool(*c.ServiceSettings.EnableCustomGroups)
}
if (license.SkuShortName == model.LicenseShortSkuProfessional || license.SkuShortName == model.LicenseShortSkuEnterprise) && c.FeatureFlags.PostPriority {
props["PostAcknowledgements"] = "true"
}
}
return props
}
// GenerateLimitedClientConfig renders the given configuration for an untrusted client.
func GenerateLimitedClientConfig(c *model.Config, telemetryID string, license *model.License) map[string]string {
props := make(map[string]string)
props["Version"] = model.CurrentVersion
props["BuildNumber"] = model.BuildNumber
props["BuildDate"] = model.BuildDate
props["BuildHash"] = model.BuildHash
props["BuildHashEnterprise"] = model.BuildHashEnterprise
props["BuildEnterpriseReady"] = model.BuildEnterpriseReady
props["BuildHashBoards"] = model.BuildHashBoards
props["BuildBoards"] = model.BuildBoards
props["BuildHashPlaybooks"] = model.BuildHashPlaybooks
props["EnableBotAccountCreation"] = strconv.FormatBool(*c.ServiceSettings.EnableBotAccountCreation)
props["EnableFile"] = strconv.FormatBool(*c.LogSettings.EnableFile)
props["FileLevel"] = *c.LogSettings.FileLevel
props["SiteURL"] = strings.TrimRight(*c.ServiceSettings.SiteURL, "/")
props["SiteName"] = *c.TeamSettings.SiteName
props["WebsocketURL"] = strings.TrimRight(*c.ServiceSettings.WebsocketURL, "/")
props["WebsocketPort"] = fmt.Sprintf("%v", *c.ServiceSettings.WebsocketPort)
props["WebsocketSecurePort"] = fmt.Sprintf("%v", *c.ServiceSettings.WebsocketSecurePort)
props["EnableUserCreation"] = strconv.FormatBool(*c.TeamSettings.EnableUserCreation)
props["EnableOpenServer"] = strconv.FormatBool(*c.TeamSettings.EnableOpenServer)
props["AndroidLatestVersion"] = c.ClientRequirements.AndroidLatestVersion
props["AndroidMinVersion"] = c.ClientRequirements.AndroidMinVersion
props["IosLatestVersion"] = c.ClientRequirements.IosLatestVersion
props["IosMinVersion"] = c.ClientRequirements.IosMinVersion
props["EnableDiagnostics"] = strconv.FormatBool(*c.LogSettings.EnableDiagnostics)
props["EnableComplianceExport"] = strconv.FormatBool(*c.MessageExportSettings.EnableExport)
props["EnableSignUpWithEmail"] = strconv.FormatBool(*c.EmailSettings.EnableSignUpWithEmail)
props["EnableSignInWithEmail"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithEmail)
props["EnableSignInWithUsername"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithUsername)
props["EmailLoginButtonColor"] = *c.EmailSettings.LoginButtonColor
props["EmailLoginButtonBorderColor"] = *c.EmailSettings.LoginButtonBorderColor
props["EmailLoginButtonTextColor"] = *c.EmailSettings.LoginButtonTextColor
props["EnableSignUpWithGitLab"] = strconv.FormatBool(*c.GitLabSettings.Enable)
props["GitLabButtonColor"] = *c.GitLabSettings.ButtonColor
props["GitLabButtonText"] = *c.GitLabSettings.ButtonText
props["TermsOfServiceLink"] = *c.SupportSettings.TermsOfServiceLink
props["PrivacyPolicyLink"] = *c.SupportSettings.PrivacyPolicyLink
props["AboutLink"] = *c.SupportSettings.AboutLink
props["HelpLink"] = *c.SupportSettings.HelpLink
props["ReportAProblemLink"] = *c.SupportSettings.ReportAProblemLink
props["SupportEmail"] = *c.SupportSettings.SupportEmail
props["EnableAskCommunityLink"] = strconv.FormatBool(*c.SupportSettings.EnableAskCommunityLink)
props["DefaultClientLocale"] = *c.LocalizationSettings.DefaultClientLocale
props["EnableCustomEmoji"] = strconv.FormatBool(*c.ServiceSettings.EnableCustomEmoji)
props["AppDownloadLink"] = *c.NativeAppSettings.AppDownloadLink
props["AndroidAppDownloadLink"] = *c.NativeAppSettings.AndroidAppDownloadLink
props["IosAppDownloadLink"] = *c.NativeAppSettings.IosAppDownloadLink
props["DiagnosticId"] = telemetryID
props["TelemetryId"] = telemetryID
props["DiagnosticsEnabled"] = strconv.FormatBool(*c.LogSettings.EnableDiagnostics)
props["HasImageProxy"] = strconv.FormatBool(*c.ImageProxySettings.Enable)
props["PluginsEnabled"] = strconv.FormatBool(*c.PluginSettings.Enable)
props["PasswordMinimumLength"] = fmt.Sprintf("%v", *c.PasswordSettings.MinimumLength)
props["PasswordRequireLowercase"] = strconv.FormatBool(*c.PasswordSettings.Lowercase)
props["PasswordRequireUppercase"] = strconv.FormatBool(*c.PasswordSettings.Uppercase)
props["PasswordRequireNumber"] = strconv.FormatBool(*c.PasswordSettings.Number)
props["PasswordRequireSymbol"] = strconv.FormatBool(*c.PasswordSettings.Symbol)
// Set default values for all options that require a license.
props["EnableCustomBrand"] = "false"
props["CustomBrandText"] = ""
props["CustomDescriptionText"] = ""
props["EnableLdap"] = "false"
props["LdapLoginFieldName"] = ""
props["LdapLoginButtonColor"] = ""
props["LdapLoginButtonBorderColor"] = ""
props["LdapLoginButtonTextColor"] = ""
props["EnableSaml"] = "false"
props["SamlLoginButtonText"] = ""
props["SamlLoginButtonColor"] = ""
props["SamlLoginButtonBorderColor"] = ""
props["SamlLoginButtonTextColor"] = ""
props["EnableSignUpWithGoogle"] = "false"
props["EnableSignUpWithOffice365"] = "false"
props["EnableSignUpWithOpenId"] = "false"
props["OpenIdButtonText"] = ""
props["OpenIdButtonColor"] = ""
props["CWSURL"] = ""
props["EnableCustomBrand"] = strconv.FormatBool(*c.TeamSettings.EnableCustomBrand)
props["CustomBrandText"] = *c.TeamSettings.CustomBrandText
props["CustomDescriptionText"] = *c.TeamSettings.CustomDescriptionText
props["EnableMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnableMultifactorAuthentication)
props["EnforceMultifactorAuthentication"] = "false"
props["EnableGuestAccounts"] = strconv.FormatBool(*c.GuestAccountsSettings.Enable)
props["GuestAccountsEnforceMultifactorAuthentication"] = strconv.FormatBool(*c.GuestAccountsSettings.EnforceMultifactorAuthentication)
if license != nil {
if *license.Features.LDAP {
props["EnableLdap"] = strconv.FormatBool(*c.LdapSettings.Enable)
props["LdapLoginFieldName"] = *c.LdapSettings.LoginFieldName
props["LdapLoginButtonColor"] = *c.LdapSettings.LoginButtonColor
props["LdapLoginButtonBorderColor"] = *c.LdapSettings.LoginButtonBorderColor
props["LdapLoginButtonTextColor"] = *c.LdapSettings.LoginButtonTextColor
}
if *license.Features.SAML {
props["EnableSaml"] = strconv.FormatBool(*c.SamlSettings.Enable)
props["SamlLoginButtonText"] = *c.SamlSettings.LoginButtonText
props["SamlLoginButtonColor"] = *c.SamlSettings.LoginButtonColor
props["SamlLoginButtonBorderColor"] = *c.SamlSettings.LoginButtonBorderColor
props["SamlLoginButtonTextColor"] = *c.SamlSettings.LoginButtonTextColor
}
if *license.Features.CustomTermsOfService {
props["EnableCustomTermsOfService"] = strconv.FormatBool(*c.SupportSettings.CustomTermsOfServiceEnabled)
props["CustomTermsOfServiceReAcceptancePeriod"] = strconv.FormatInt(int64(*c.SupportSettings.CustomTermsOfServiceReAcceptancePeriod), 10)
}
if *license.Features.MFA {
props["EnforceMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnforceMultifactorAuthentication)
}
if license.IsCloud() {
// MM-48727: enable SSO options for free cloud - not in self hosted
*license.Features.GoogleOAuth = true
*license.Features.Office365OAuth = true
}
if *license.Features.GoogleOAuth {
props["EnableSignUpWithGoogle"] = strconv.FormatBool(*c.GoogleSettings.Enable)
}
if *license.Features.Office365OAuth {
props["EnableSignUpWithOffice365"] = strconv.FormatBool(*c.Office365Settings.Enable)
}
if *license.Features.OpenId {
props["EnableSignUpWithOpenId"] = strconv.FormatBool(*c.OpenIdSettings.Enable)
props["OpenIdButtonColor"] = *c.OpenIdSettings.ButtonColor
props["OpenIdButtonText"] = *c.OpenIdSettings.ButtonText
}
}
for key, value := range c.FeatureFlags.ToMap() {
props["FeatureFlag"+key] = value
}
return props
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"bytes"
"context"
"crypto/sha256"
"database/sql"
"embed"
"encoding/hex"
"encoding/json"
"fmt"
"path/filepath"
"strings"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
// Load the MySQL driver
_ "github.com/go-sql-driver/mysql"
// Load the Postgres driver
_ "github.com/lib/pq"
"github.com/mattermost/morph"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/morph/drivers"
ms "github.com/mattermost/morph/drivers/mysql"
ps "github.com/mattermost/morph/drivers/postgres"
mbindata "github.com/mattermost/morph/sources/embedded"
)
//go:embed migrations
var assets embed.FS
// MaxWriteLength defines the maximum length accepted for write to the Configurations or
// ConfigurationFiles table.
//
// It is imposed by MySQL's default max_allowed_packet value of 4Mb.
const MaxWriteLength = 4 * 1024 * 1024
// We use the something different from the default migration table name of morph
const migrationsTableName = "db_config_migrations"
// The timeout value for each migration file to run.
const migrationsTimeoutInSeconds = 100000
// DatabaseStore is a config store backed by a database.
// Not to be used directly. Only to be used as a backing store for config.Store
type DatabaseStore struct {
originalDsn string
driverName string
dataSourceName string
db *sqlx.DB
}
// NewDatabaseStore creates a new instance of a config store backed by the given database.
func NewDatabaseStore(dsn string) (ds *DatabaseStore, err error) {
driverName, dataSourceName, err := parseDSN(dsn)
if err != nil {
return nil, errors.Wrap(err, "invalid DSN")
}
db, err := sqlx.Open(driverName, dataSourceName)
if err != nil {
return nil, errors.Wrapf(err, "failed to connect to %s database", driverName)
}
// Set conservative connection configuration for configuration database.
db.SetMaxIdleConns(0)
db.SetMaxOpenConns(2)
defer func() {
if err != nil {
db.Close()
}
}()
ds = &DatabaseStore{
driverName: driverName,
originalDsn: dsn,
dataSourceName: dataSourceName,
db: db,
}
if err = ds.initializeConfigurationsTable(); err != nil {
err = errors.Wrap(err, "failed to initialize")
return nil, err
}
return ds, nil
}
// initializeConfigurationsTable ensures the requisite tables in place to form the backing store.
//
// Uses MEDIUMTEXT on MySQL, and TEXT on sane databases.
func (ds *DatabaseStore) initializeConfigurationsTable() error {
assetsList, err := assets.ReadDir(filepath.Join("migrations", ds.driverName))
if err != nil {
return err
}
assetNamesForDriver := make([]string, len(assetsList))
for i, entry := range assetsList {
assetNamesForDriver[i] = entry.Name()
}
src, err := mbindata.WithInstance(&mbindata.AssetSource{
Names: assetNamesForDriver,
AssetFunc: func(name string) ([]byte, error) {
return assets.ReadFile(filepath.Join("migrations", ds.driverName, name))
},
})
if err != nil {
return err
}
var driver drivers.Driver
switch ds.driverName {
case model.DatabaseDriverMysql:
dataSource, rErr := sqlstore.ResetReadTimeout(ds.dataSourceName)
if rErr != nil {
return fmt.Errorf("failed to reset read timeout from datasource: %w", rErr)
}
dataSource, err = sqlstore.AppendMultipleStatementsFlag(dataSource)
if err != nil {
return err
}
var db *sqlx.DB
db, err = sqlx.Open(ds.driverName, dataSource)
if err != nil {
return errors.Wrapf(err, "failed to connect to %s database", ds.driverName)
}
driver, err = ms.WithInstance(db.DB)
defer db.Close()
case model.DatabaseDriverPostgres:
driver, err = ps.WithInstance(ds.db.DB)
default:
err = fmt.Errorf("unsupported database type %s for migration", ds.driverName)
}
if err != nil {
return err
}
opts := []morph.EngineOption{
morph.WithLock("mm-config-lock-key"),
morph.SetMigrationTableName(migrationsTableName),
morph.SetStatementTimeoutInSeconds(migrationsTimeoutInSeconds),
}
engine, err := morph.New(context.Background(), driver, src, opts...)
if err != nil {
return err
}
defer engine.Close()
return engine.ApplyAll()
}
// parseDSN splits up a connection string into a driver name and data source name.
//
// For example:
//
// mysql://mmuser:mostest@localhost:5432/mattermost_test
//
// returns
//
// driverName = mysql
// dataSourceName = mmuser:mostest@localhost:5432/mattermost_test
//
// By contrast, a Postgres DSN is returned unmodified.
func parseDSN(dsn string) (string, string, error) {
// Treat the DSN as the URL that it is.
s := strings.SplitN(dsn, "://", 2)
if len(s) != 2 {
return "", "", errors.New("failed to parse DSN as URL")
}
scheme := s[0]
switch scheme {
case "mysql":
// Strip off the mysql:// for the dsn with which to connect.
dsn = s[1]
case "postgres", "postgresql":
// No changes required
default:
return "", "", errors.Errorf("unsupported scheme %s", scheme)
}
return scheme, dsn, nil
}
// Set replaces the current configuration in its entirety and updates the backing store.
func (ds *DatabaseStore) Set(newCfg *model.Config) error {
return ds.persist(newCfg)
}
// maxLength identifies the maximum length of a configuration or configuration file
func (ds *DatabaseStore) checkLength(length int) error {
if ds.db.DriverName() == "mysql" && length > MaxWriteLength {
return errors.Errorf("value is too long: %d > %d bytes", length, MaxWriteLength)
}
return nil
}
// persist writes the configuration to the configured database.
func (ds *DatabaseStore) persist(cfg *model.Config) error {
b, err := marshalConfig(cfg)
if err != nil {
return errors.Wrap(err, "failed to serialize")
}
value := string(b)
err = ds.checkLength(len(value))
if err != nil {
return errors.Wrap(err, "marshalled configuration failed length check")
}
sum := sha256.Sum256(b)
// Skip the persist altogether if we're effectively writing the same configuration.
var oldValue string
var row *sql.Row
if ds.driverName == model.DatabaseDriverMysql {
// We use a sub-query to get the Id first because selecting the Id column using
// active uses the index, but selecting SHA column using active does not use the index.
// The sub-query uses the active index, and then the top-level query uses the primary key.
// This takes 2 queries, but it is actually faster than one slow query for MySQL
row = ds.db.QueryRow("SELECT SHA FROM Configurations WHERE Id = (select Id from Configurations Where Active)")
} else {
row = ds.db.QueryRow("SELECT SHA FROM Configurations WHERE Active")
}
if err = row.Scan(&oldValue); err != nil && err != sql.ErrNoRows {
return errors.Wrap(err, "failed to query active configuration")
}
// postgres retruns blank-padded therefore we trim the space
oldSum, err := hex.DecodeString(strings.TrimSpace(oldValue))
if err != nil {
return errors.Wrap(err, "could not encode value")
}
// compare checksums, it's more efficient rather than comparing entire config itself
if bytes.Equal(oldSum, sum[0:]) {
return nil
}
tx, err := ds.db.Beginx()
if err != nil {
return errors.Wrap(err, "failed to begin transaction")
}
defer func() {
// Rollback after Commit just returns sql.ErrTxDone.
if err = tx.Rollback(); err != nil && err != sql.ErrTxDone {
mlog.Error("Failed to rollback configuration transaction", mlog.Err(err))
}
}()
var oldId string
if ds.driverName == model.DatabaseDriverMysql {
// the query doesn't use active index if we query for value (mysql, no surprise)
// we select Id column which triggers using index hence we do quicker reads
// that's the reason we select id first then query against id to get the value.
row = tx.QueryRow("SELECT Id FROM Configurations WHERE Active")
if err = row.Scan(&oldId); err != nil && err != sql.ErrNoRows {
return errors.Wrap(err, "failed to query active configuration")
}
if oldId != "" {
if _, err := tx.NamedExec("UPDATE Configurations SET Active = NULL WHERE Id = :id", map[string]any{"id": oldId}); err != nil {
return errors.Wrap(err, "failed to deactivate current configuration")
}
}
} else {
if _, err := tx.Exec("UPDATE Configurations SET Active = NULL WHERE Active"); err != nil {
return errors.Wrap(err, "failed to deactivate current configuration")
}
}
params := map[string]any{
"id": model.NewId(),
"value": value,
"create_at": model.GetMillis(),
"key": "ConfigurationId",
"sha": hex.EncodeToString(sum[0:]),
}
if _, err := tx.NamedExec("INSERT INTO Configurations (Id, Value, CreateAt, Active, SHA) VALUES (:id, :value, :create_at, TRUE, :sha)", params); err != nil {
return errors.Wrap(err, "failed to record new configuration")
}
if err := tx.Commit(); err != nil {
return errors.Wrap(err, "failed to commit transaction")
}
return nil
}
// Load updates the current configuration from the backing store.
func (ds *DatabaseStore) Load() ([]byte, error) {
var configurationData []byte
row := ds.db.QueryRow("SELECT Value FROM Configurations WHERE Active")
if err := row.Scan(&configurationData); err != nil && err != sql.ErrNoRows {
return nil, errors.Wrap(err, "failed to query active configuration")
}
// Initialize from the default config if no active configuration could be found.
if len(configurationData) == 0 {
configWithDB := model.Config{}
configWithDB.SqlSettings.DriverName = model.NewString(ds.driverName)
configWithDB.SqlSettings.DataSource = model.NewString(ds.dataSourceName)
return json.Marshal(configWithDB)
}
return configurationData, nil
}
// GetFile fetches the contents of a previously persisted configuration file.
func (ds *DatabaseStore) GetFile(name string) ([]byte, error) {
query, args, err := sqlx.Named("SELECT Data FROM ConfigurationFiles WHERE Name = :name", map[string]any{
"name": name,
})
if err != nil {
return nil, err
}
var data []byte
row := ds.db.QueryRowx(ds.db.Rebind(query), args...)
if err = row.Scan(&data); err != nil {
return nil, errors.Wrapf(err, "failed to scan data from row for %s", name)
}
return data, nil
}
// SetFile sets or replaces the contents of a configuration file.
func (ds *DatabaseStore) SetFile(name string, data []byte) error {
err := ds.checkLength(len(data))
if err != nil {
return errors.Wrap(err, "file data failed length check")
}
params := map[string]any{
"name": name,
"data": data,
"create_at": model.GetMillis(),
"update_at": model.GetMillis(),
}
result, err := ds.db.NamedExec("UPDATE ConfigurationFiles SET Data = :data, UpdateAt = :update_at WHERE Name = :name", params)
if err != nil {
return errors.Wrapf(err, "failed to update row for %s", name)
}
count, err := result.RowsAffected()
if err != nil {
return errors.Wrapf(err, "failed to count rows affected for %s", name)
} else if count > 0 {
return nil
}
_, err = ds.db.NamedExec("INSERT INTO ConfigurationFiles (Name, Data, CreateAt, UpdateAt) VALUES (:name, :data, :create_at, :update_at)", params)
if err != nil {
return errors.Wrapf(err, "failed to insert row for %s", name)
}
return nil
}
// HasFile returns true if the given file was previously persisted.
func (ds *DatabaseStore) HasFile(name string) (bool, error) {
query, args, err := sqlx.Named("SELECT COUNT(*) FROM ConfigurationFiles WHERE Name = :name", map[string]any{
"name": name,
})
if err != nil {
return false, err
}
var count int64
row := ds.db.QueryRowx(ds.db.Rebind(query), args...)
if err = row.Scan(&count); err != nil {
return false, errors.Wrapf(err, "failed to scan count of rows for %s", name)
}
return count != 0, nil
}
// RemoveFile remoevs a previously persisted configuration file.
func (ds *DatabaseStore) RemoveFile(name string) error {
_, err := ds.db.NamedExec("DELETE FROM ConfigurationFiles WHERE Name = :name", map[string]any{
"name": name,
})
if err != nil {
return errors.Wrapf(err, "failed to remove row for %s", name)
}
return nil
}
// String returns the path to the database backing the config, masking the password.
func (ds *DatabaseStore) String() string {
return stripPassword(ds.originalDsn, ds.driverName)
}
// Close cleans up resources associated with the store.
func (ds *DatabaseStore) Close() error {
return ds.db.Close()
}
// removes configurations from database if they are older than threshold.
func (ds *DatabaseStore) cleanUp(thresholdCreatAt int) error {
if _, err := ds.db.NamedExec("DELETE FROM Configurations Where CreateAt < :timestamp", map[string]any{"timestamp": thresholdCreatAt}); err != nil {
return errors.Wrap(err, "unable to clean Configurations table")
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"fmt"
"reflect"
"github.com/mattermost/mattermost-server/v6/model"
)
type ConfigDiffs []ConfigDiff
type ConfigDiff struct {
Path string `json:"path"`
BaseVal any `json:"base_val"`
ActualVal any `json:"actual_val"`
}
func (c *ConfigDiff) Auditable() map[string]interface{} {
return map[string]interface{}{
"path": c.Path,
"base_val": c.BaseVal,
"actual_val": c.ActualVal,
}
}
func (cd *ConfigDiffs) Auditable() map[string]interface{} {
var s []interface{}
for _, d := range cd.Sanitize() {
s = append(s, d.Auditable())
}
return map[string]interface{}{
"config_diffs": s,
}
}
var configSensitivePaths = map[string]bool{
"LdapSettings.BindPassword": true,
"FileSettings.PublicLinkSalt": true,
"FileSettings.AmazonS3SecretAccessKey": true,
"SqlSettings.DataSource": true,
"SqlSettings.AtRestEncryptKey": true,
"SqlSettings.DataSourceReplicas": true,
"SqlSettings.DataSourceSearchReplicas": true,
"EmailSettings.SMTPPassword": true,
"GitLabSettings.Secret": true,
"GoogleSettings.Secret": true,
"Office365Settings.Secret": true,
"OpenIdSettings.Secret": true,
"ElasticsearchSettings.Password": true,
"MessageExportSettings.GlobalRelaySettings.SMTPUsername": true,
"MessageExportSettings.GlobalRelaySettings.SMTPPassword": true,
"MessageExportSettings.GlobalRelaySettings.EmailAddress": true,
"ServiceSettings.GfycatAPISecret": true,
"ServiceSettings.SplitKey": true,
"PluginSettings.Plugins": true,
}
// Sanitize replaces sensitive config values in the diff with asterisks filled strings.
func (cd ConfigDiffs) Sanitize() ConfigDiffs {
if len(cd) == 1 {
cfgPtr, ok := cd[0].BaseVal.(*model.Config)
if ok {
cfgPtr.Sanitize()
}
cfgPtr, ok = cd[0].ActualVal.(*model.Config)
if ok {
cfgPtr.Sanitize()
}
cfgVal, ok := cd[0].BaseVal.(model.Config)
if ok {
cfgVal.Sanitize()
}
cfgVal, ok = cd[0].ActualVal.(model.Config)
if ok {
cfgVal.Sanitize()
}
}
for i := range cd {
if configSensitivePaths[cd[i].Path] {
cd[i].BaseVal = model.FakeSetting
cd[i].ActualVal = model.FakeSetting
}
}
return cd
}
func diff(base, actual reflect.Value, label string) ([]ConfigDiff, error) {
var diffs []ConfigDiff
if base.IsZero() && actual.IsZero() {
return diffs, nil
}
if base.IsZero() || actual.IsZero() {
return append(diffs, ConfigDiff{
Path: label,
BaseVal: base.Interface(),
ActualVal: actual.Interface(),
}), nil
}
baseType := base.Type()
actualType := actual.Type()
if baseType.Kind() == reflect.Ptr {
base = reflect.Indirect(base)
actual = reflect.Indirect(actual)
baseType = base.Type()
actualType = actual.Type()
}
if baseType != actualType {
return nil, fmt.Errorf("not same type %s %s", baseType, actualType)
}
switch baseType.Kind() {
case reflect.Struct:
if base.NumField() != actual.NumField() {
return nil, fmt.Errorf("not same number of fields in struct")
}
for i := 0; i < base.NumField(); i++ {
fieldLabel := baseType.Field(i).Name
if label != "" {
fieldLabel = label + "." + fieldLabel
}
d, err := diff(base.Field(i), actual.Field(i), fieldLabel)
if err != nil {
return nil, err
}
diffs = append(diffs, d...)
}
default:
if !reflect.DeepEqual(base.Interface(), actual.Interface()) {
diffs = append(diffs, ConfigDiff{
Path: label,
BaseVal: base.Interface(),
ActualVal: actual.Interface(),
})
}
}
return diffs, nil
}
func Diff(base, actual *model.Config) (ConfigDiffs, error) {
if base == nil || actual == nil {
return nil, fmt.Errorf("input configs should not be nil")
}
baseVal := reflect.Indirect(reflect.ValueOf(base))
actualVal := reflect.Indirect(reflect.ValueOf(actual))
return diff(baseVal, actualVal, "")
}
func (cd ConfigDiffs) String() string {
return fmt.Sprintf("%+v", []ConfigDiff(cd))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"sync"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// Listener is a callback function invoked when the configuration changes.
type Listener func(oldCfg, newCfg *model.Config)
// emitter enables threadsafe registration and broadcasting to configuration listeners
type emitter struct {
listeners sync.Map
}
// AddListener adds a callback function to invoke when the configuration is modified.
func (e *emitter) AddListener(listener Listener) string {
id := model.NewId()
e.listeners.Store(id, listener)
return id
}
// RemoveListener removes a callback function using an id returned from AddListener.
func (e *emitter) RemoveListener(id string) {
e.listeners.Delete(id)
}
// invokeConfigListeners synchronously notifies all listeners about the configuration change.
func (e *emitter) invokeConfigListeners(oldCfg, newCfg *model.Config) {
e.listeners.Range(func(key, value any) bool {
listener := value.(Listener)
listener(oldCfg, newCfg)
return true
})
}
// srcEmitter enables threadsafe registration and broadcasting to configuration listeners
type logSrcEmitter struct {
listeners sync.Map
}
// AddListener adds a callback function to invoke when the configuration is modified.
func (e *logSrcEmitter) AddListener(listener LogSrcListener) string {
id := model.NewId()
e.listeners.Store(id, listener)
return id
}
// RemoveListener removes a callback function using an id returned from AddListener.
func (e *logSrcEmitter) RemoveListener(id string) {
e.listeners.Delete(id)
}
// invokeConfigListeners synchronously notifies all listeners about the configuration change.
func (e *logSrcEmitter) invokeConfigListeners(oldCfg, newCfg mlog.LoggerConfiguration) {
e.listeners.Range(func(key, value any) bool {
listener := value.(LogSrcListener)
listener(oldCfg, newCfg)
return true
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"encoding/json"
"os"
"reflect"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
)
func GetEnvironment() map[string]string {
mmenv := make(map[string]string)
for _, env := range os.Environ() {
kv := strings.SplitN(env, "=", 2)
key := strings.ToUpper(kv[0])
if strings.HasPrefix(key, "MM") {
mmenv[key] = kv[1]
}
}
return mmenv
}
func applyEnvKey(key, value string, rValueSubject reflect.Value) {
keyParts := strings.SplitN(key, "_", 2)
if len(keyParts) < 1 {
return
}
rFieldValue := rValueSubject.FieldByNameFunc(func(candidate string) bool {
candidateUpper := strings.ToUpper(candidate)
return candidateUpper == keyParts[0]
})
if !rFieldValue.IsValid() {
return
}
if rFieldValue.Kind() == reflect.Ptr {
rFieldValue = rFieldValue.Elem()
if !rFieldValue.IsValid() {
return
}
}
switch rFieldValue.Kind() {
case reflect.Struct:
// If we have only one part left, we can't deal with a struct
// the env var is incomplete so give up.
if len(keyParts) < 2 {
return
}
applyEnvKey(keyParts[1], value, rFieldValue)
case reflect.String:
rFieldValue.Set(reflect.ValueOf(value))
case reflect.Bool:
boolVal, err := strconv.ParseBool(value)
if err == nil {
rFieldValue.Set(reflect.ValueOf(boolVal))
}
case reflect.Int:
intVal, err := strconv.ParseInt(value, 10, 0)
if err == nil {
rFieldValue.Set(reflect.ValueOf(int(intVal)))
}
case reflect.Int64:
intVal, err := strconv.ParseInt(value, 10, 0)
if err == nil {
rFieldValue.Set(reflect.ValueOf(intVal))
}
case reflect.SliceOf(reflect.TypeOf("")).Kind():
rFieldValue.Set(reflect.ValueOf(strings.Split(value, " ")))
case reflect.Map:
target := reflect.New(rFieldValue.Type()).Interface()
if err := json.Unmarshal([]byte(value), target); err == nil {
rFieldValue.Set(reflect.ValueOf(target).Elem())
}
}
}
func applyEnvironmentMap(inputConfig *model.Config, env map[string]string) *model.Config {
appliedConfig := inputConfig.Clone()
rvalConfig := reflect.ValueOf(appliedConfig).Elem()
for envKey, envValue := range env {
applyEnvKey(strings.TrimPrefix(envKey, "MM_"), envValue, rvalConfig)
}
return appliedConfig
}
// generateEnvironmentMap creates a map[string]any containing true at the leaves mirroring the
// configuration structure so the client can know which env variables are overridden
func generateEnvironmentMap(env map[string]string, filter func(reflect.StructField) bool) map[string]any {
rType := reflect.TypeOf(model.Config{})
return generateEnvironmentMapWithBaseKey(env, rType, "MM", filter)
}
func generateEnvironmentMapWithBaseKey(env map[string]string, rType reflect.Type, base string, filter func(reflect.StructField) bool) map[string]any {
if rType.Kind() != reflect.Struct {
return nil
}
mapRepresentation := make(map[string]any)
for i := 0; i < rType.NumField(); i++ {
rField := rType.Field(i)
if filter != nil && !filter(rField) {
continue
}
if rField.Type.Kind() == reflect.Struct {
if val := generateEnvironmentMapWithBaseKey(env, rField.Type, base+"_"+rField.Name, filter); val != nil {
mapRepresentation[rField.Name] = val
}
} else {
if _, ok := env[strings.ToUpper(base+"_"+rField.Name)]; ok {
mapRepresentation[rField.Name] = true
}
}
}
if len(mapRepresentation) == 0 {
return nil
}
return mapRepresentation
}
// removeEnvOverrides returns a new config without the given environment overrides.
// If a config variable has an environment override, that variable is set to the value that was
// read from the store.
func removeEnvOverrides(cfg, cfgWithoutEnv *model.Config, envOverrides map[string]any) *model.Config {
paths := getPaths(envOverrides)
newCfg := cfg.Clone()
for _, path := range paths {
originalVal := getVal(cfgWithoutEnv, path)
newVal := getVal(newCfg, path)
if newVal.CanSet() {
newVal.Set(originalVal)
}
}
return newCfg
}
// getPaths turns a nested map into a slice of paths describing the keys of the map. Eg:
// map[string]map[string]map[string]bool{"this":{"is first":{"path":true}, "is second":{"path":true}))) is turned into:
// [][]string{{"this", "is first", "path"}, {"this", "is second", "path"}}
func getPaths(m map[string]any) [][]string {
return getPathsRec(m, nil)
}
// getPathsRec assembles the paths (see `getPaths` above)
func getPathsRec(src any, curPath []string) [][]string {
if srcMap, ok := src.(map[string]any); ok {
paths := [][]string{}
for k, v := range srcMap {
paths = append(paths, getPathsRec(v, append(curPath, k))...)
}
return paths
}
return [][]string{curPath}
}
// getVal walks `src` (here it starts with a model.Config, then recurses into its leaves)
// and returns the reflect.Value of the leaf at the end `path`
func getVal(src any, path []string) reflect.Value {
var val reflect.Value
// If we recursed on a Value, we already have it. If we're calling on an any, get the Value.
switch v := src.(type) {
case reflect.Value:
val = v
default:
val = reflect.ValueOf(src)
}
// Move into the struct
if val.Kind() == reflect.Ptr {
val = val.Elem().FieldByName(path[0])
} else {
val = val.FieldByName(path[0])
}
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() == reflect.Struct {
return getVal(val, path[1:])
}
return val
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var (
// ErrReadOnlyConfiguration is returned when an attempt to modify a read-only configuration is made.
ErrReadOnlyConfiguration = errors.New("configuration is read-only")
)
// FileStore is a config store backed by a file such as config/config.json.
//
// It also uses the folder containing the configuration file for storing other configuration files.
// Not to be used directly. Only to be used as a backing store for config.Store
type FileStore struct {
path string
}
// NewFileStore creates a new instance of a config store backed by the given file path.
func NewFileStore(path string, createFileIfNotExists bool) (fs *FileStore, err error) {
resolvedPath, err := resolveConfigFilePath(path)
if err != nil {
return nil, err
}
f, err := os.Open(resolvedPath)
if err != nil && errors.Is(err, os.ErrNotExist) && createFileIfNotExists {
file, err2 := os.Create(resolvedPath)
if err2 != nil {
return nil, fmt.Errorf("could not create config file: %w", err2)
}
defer file.Close()
} else if err != nil {
return nil, err
} else {
defer f.Close()
}
return &FileStore{
path: resolvedPath,
}, nil
}
// resolveConfigFilePath attempts to resolve the given configuration file path to an absolute path.
//
// Consideration is given to maintaining backwards compatibility when resolving the path to the
// configuration file.
func resolveConfigFilePath(path string) (string, error) {
// Absolute paths are explicit and require no resolution.
if filepath.IsAbs(path) {
return path, nil
}
// Search for the relative path to the file in the channels/config folder, taking into account
// various common starting points.
if configFile := fileutils.FindFile(filepath.Join("channels/config", path)); configFile != "" {
return configFile, nil
}
// Search for the relative path to the file in the config folder, taking into account
// various common starting points.
if configFile := fileutils.FindFile(filepath.Join("config", path)); configFile != "" {
return configFile, nil
}
// Search for the relative path in the current working directory, also taking into account
// various common starting points.
if configFile := fileutils.FindPath(path, []string{"."}, nil); configFile != "" {
return configFile, nil
}
if configFolder, found := fileutils.FindDir("config"); found {
return filepath.Join(configFolder, path), nil
}
// Fail altogether if we can't even find the config/ folder. This should only happen if
// the executable is relocated away from the supporting files.
return "", fmt.Errorf("failed to find config file %s", path)
}
// resolveFilePath uses the name if name is absolute path.
// otherwise returns the combined path/name
func (fs *FileStore) resolveFilePath(name string) string {
// Absolute paths are explicit and require no resolution.
if filepath.IsAbs(name) {
return name
}
return filepath.Join(filepath.Dir(fs.path), name)
}
// Set replaces the current configuration in its entirety and updates the backing store.
func (fs *FileStore) Set(newCfg *model.Config) error {
if *newCfg.ClusterSettings.Enable && *newCfg.ClusterSettings.ReadOnlyConfig {
return ErrReadOnlyConfiguration
}
return fs.persist(newCfg)
}
// persist writes the configuration to the configured file.
func (fs *FileStore) persist(cfg *model.Config) error {
b, err := marshalConfig(cfg)
if err != nil {
return errors.Wrap(err, "failed to serialize")
}
err = os.WriteFile(fs.path, b, 0600)
if err != nil {
return errors.Wrap(err, "failed to write file")
}
return nil
}
// Load updates the current configuration from the backing store.
func (fs *FileStore) Load() ([]byte, error) {
f, err := os.Open(fs.path)
if os.IsNotExist(err) {
return nil, nil
} else if err != nil {
return nil, errors.Wrapf(err, "failed to open %s for reading", fs.path)
}
defer f.Close()
fileBytes, err := io.ReadAll(f)
if err != nil {
return nil, err
}
return fileBytes, nil
}
// GetFile fetches the contents of a previously persisted configuration file.
func (fs *FileStore) GetFile(name string) ([]byte, error) {
resolvedPath := fs.resolveFilePath(name)
data, err := os.ReadFile(resolvedPath)
if err != nil {
return nil, errors.Wrapf(err, "failed to read file from %s", resolvedPath)
}
return data, nil
}
// GetFilePath returns the resolved path of a configuration file.
// The file may not necessarily exist.
func (fs *FileStore) GetFilePath(name string) string {
return fs.resolveFilePath(name)
}
// SetFile sets or replaces the contents of a configuration file.
func (fs *FileStore) SetFile(name string, data []byte) error {
resolvedPath := fs.resolveFilePath(name)
err := os.WriteFile(resolvedPath, data, 0600)
if err != nil {
return errors.Wrapf(err, "failed to write file to %s", resolvedPath)
}
return nil
}
// HasFile returns true if the given file was previously persisted.
func (fs *FileStore) HasFile(name string) (bool, error) {
if name == "" {
return false, nil
}
resolvedPath := fs.resolveFilePath(name)
_, err := os.Stat(resolvedPath)
if err != nil && os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, errors.Wrap(err, "failed to check if file exists")
}
return true, nil
}
// RemoveFile removes a previously persisted configuration file.
func (fs *FileStore) RemoveFile(name string) error {
if filepath.IsAbs(name) {
// Don't delete absolute filenames, as may be mounted drive, etc.
mlog.Debug("Skipping removal of configuration file with absolute path", mlog.String("filename", name))
return nil
}
resolvedPath := filepath.Join(filepath.Dir(fs.path), name)
err := os.Remove(resolvedPath)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return errors.Wrap(err, "failed to remove file")
}
return nil
}
// String returns the path to the file backing the config.
func (fs *FileStore) String() string {
return "file://" + fs.path
}
// Close cleans up resources associated with the store.
func (fs *FileStore) Close() error {
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"encoding/json"
"errors"
"path/filepath"
"strings"
"sync"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type LogSrcListener func(old, new mlog.LoggerConfiguration)
// LogConfigSrc abstracts the Advanced Logging configuration so that implementations can
// fetch from file, database, etc.
type LogConfigSrc interface {
// Get fetches the current, cached configuration.
Get() mlog.LoggerConfiguration
// Set updates the dsn specifying the source and reloads
Set(dsn string, configStore *Store) (err error)
// Close cleans up resources.
Close() error
}
// NewLogConfigSrc creates an advanced logging configuration source, backed by a
// file, JSON string, or database.
func NewLogConfigSrc(dsn string, configStore *Store) (LogConfigSrc, error) {
if dsn == "" {
return nil, errors.New("dsn should not be empty")
}
if configStore == nil {
return nil, errors.New("configStore should not be nil")
}
dsn = strings.TrimSpace(dsn)
if isJSONMap(dsn) {
return newJSONSrc(dsn)
}
path := dsn
// If this is a file based config we need the full path so it can be watched.
if strings.HasPrefix(configStore.String(), "file://") && !filepath.IsAbs(dsn) {
configPath := strings.TrimPrefix(configStore.String(), "file://")
path = filepath.Join(filepath.Dir(configPath), dsn)
}
return newFileSrc(path, configStore)
}
// jsonSrc
type jsonSrc struct {
logSrcEmitter
mutex sync.RWMutex
cfg mlog.LoggerConfiguration
}
func newJSONSrc(data string) (*jsonSrc, error) {
src := &jsonSrc{}
return src, src.Set(data, nil)
}
// Get fetches the current, cached configuration
func (src *jsonSrc) Get() mlog.LoggerConfiguration {
src.mutex.RLock()
defer src.mutex.RUnlock()
return src.cfg
}
// Set updates the JSON specifying the source and reloads
func (src *jsonSrc) Set(data string, _ *Store) error {
cfg, err := logTargetCfgFromJSON([]byte(data))
if err != nil {
return err
}
src.set(cfg)
return nil
}
func (src *jsonSrc) set(cfg mlog.LoggerConfiguration) {
src.mutex.Lock()
defer src.mutex.Unlock()
old := src.cfg
src.cfg = cfg
src.invokeConfigListeners(old, cfg)
}
// Close cleans up resources.
func (src *jsonSrc) Close() error {
return nil
}
// fileSrc
type fileSrc struct {
mutex sync.RWMutex
cfg mlog.LoggerConfiguration
path string
}
func newFileSrc(path string, configStore *Store) (*fileSrc, error) {
src := &fileSrc{
path: path,
}
if err := src.Set(path, configStore); err != nil {
return nil, err
}
return src, nil
}
// Get fetches the current, cached configuration
func (src *fileSrc) Get() mlog.LoggerConfiguration {
src.mutex.RLock()
defer src.mutex.RUnlock()
return src.cfg
}
// Set updates the dsn specifying the file source and reloads.
// The file will be watched for changes and reloaded as needed,
// and all listeners notified.
func (src *fileSrc) Set(path string, configStore *Store) error {
data, err := configStore.GetFile(path)
if err != nil {
return err
}
cfg, err := logTargetCfgFromJSON(data)
if err != nil {
return err
}
src.set(cfg)
return nil
}
func (src *fileSrc) set(cfg mlog.LoggerConfiguration) {
src.mutex.Lock()
defer src.mutex.Unlock()
src.cfg = cfg
}
// Close cleans up resources.
func (src *fileSrc) Close() error {
return nil
}
func logTargetCfgFromJSON(data []byte) (mlog.LoggerConfiguration, error) {
cfg := make(mlog.LoggerConfiguration)
err := json.Unmarshal(data, &cfg)
if err != nil {
return nil, err
}
return cfg, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"encoding/json"
"fmt"
"path/filepath"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
LogRotateSizeMB = 100
LogCompress = true
LogRotateMaxAge = 0
LogRotateMaxBackups = 0
LogFilename = "mattermost.log"
LogNotificationFilename = "notifications.log"
LogMinLevelLen = 5
LogMinMsgLen = 45
LogDelim = " "
LogEnableCaller = true
)
type fileLocationFunc func(string) string
func MloggerConfigFromLoggerConfig(s *model.LogSettings, configSrc LogConfigSrc, getFileFunc fileLocationFunc) (mlog.LoggerConfiguration, error) {
cfg := make(mlog.LoggerConfiguration)
var targetCfg mlog.TargetCfg
var err error
// add the simple logging config
if *s.EnableConsole {
targetCfg, err = makeSimpleConsoleTarget(*s.ConsoleLevel, *s.ConsoleJson, *s.EnableColor)
if err != nil {
return cfg, err
}
cfg["_defConsole"] = targetCfg
}
if *s.EnableFile {
targetCfg, err = makeSimpleFileTarget(getFileFunc(*s.FileLocation), *s.FileLevel, *s.FileJson)
if err != nil {
return cfg, err
}
cfg["_defFile"] = targetCfg
}
if configSrc == nil {
return cfg, nil
}
// add advanced logging config
cfgAdv := configSrc.Get()
cfg.Append(cfgAdv)
return cfg, nil
}
func MloggerConfigFromAuditConfig(auditSettings model.ExperimentalAuditSettings, configSrc LogConfigSrc) (mlog.LoggerConfiguration, error) {
cfg := make(mlog.LoggerConfiguration)
var targetCfg mlog.TargetCfg
var err error
// add the simple audit config
if *auditSettings.FileEnabled {
targetCfg, err = makeSimpleFileTarget(*auditSettings.FileName, "error", true)
if err != nil {
return nil, err
}
// apply audit specific levels
targetCfg.Levels = []mlog.Level{mlog.LvlAuditAPI, mlog.LvlAuditContent, mlog.LvlAuditPerms, mlog.LvlAuditCLI}
// apply audit specific formatting
targetCfg.FormatOptions = json.RawMessage(`{"disable_timestamp": false, "disable_msg": true, "disable_stacktrace": true, "disable_level": true}`)
cfg["_defAudit"] = targetCfg
}
if configSrc == nil {
return cfg, nil
}
// add advanced audit config
cfgAdv := configSrc.Get()
cfg.Append(cfgAdv)
return cfg, nil
}
func GetLogFileLocation(fileLocation string) string {
if fileLocation == "" {
fileLocation, _ = fileutils.FindDir("logs")
}
return filepath.Join(fileLocation, LogFilename)
}
func GetNotificationsLogFileLocation(fileLocation string) string {
if fileLocation == "" {
fileLocation, _ = fileutils.FindDir("logs")
}
return filepath.Join(fileLocation, LogNotificationFilename)
}
func GetLogSettingsFromNotificationsLogSettings(notificationLogSettings *model.NotificationLogSettings) *model.LogSettings {
settings := &model.LogSettings{}
settings.SetDefaults()
settings.ConsoleJson = notificationLogSettings.ConsoleJson
settings.ConsoleLevel = notificationLogSettings.ConsoleLevel
settings.EnableConsole = notificationLogSettings.EnableConsole
settings.EnableFile = notificationLogSettings.EnableFile
settings.FileJson = notificationLogSettings.FileJson
settings.FileLevel = notificationLogSettings.FileLevel
settings.FileLocation = notificationLogSettings.FileLocation
settings.AdvancedLoggingConfig = notificationLogSettings.AdvancedLoggingConfig
settings.EnableColor = notificationLogSettings.EnableColor
return settings
}
func makeSimpleConsoleTarget(level string, outputJSON bool, color bool) (mlog.TargetCfg, error) {
levels, err := stdLevels(level)
if err != nil {
return mlog.TargetCfg{}, err
}
target := mlog.TargetCfg{
Type: "console",
Levels: levels,
Options: json.RawMessage(`{"out": "stdout"}`),
MaxQueueSize: 1000,
}
if outputJSON {
target.Format = "json"
target.FormatOptions = makeJSONFormatOptions()
} else {
target.Format = "plain"
target.FormatOptions = makePlainFormatOptions(color)
}
return target, nil
}
func makeSimpleFileTarget(filename string, level string, json bool) (mlog.TargetCfg, error) {
levels, err := stdLevels(level)
if err != nil {
return mlog.TargetCfg{}, err
}
fileOpts, err := makeFileOptions(filename)
if err != nil {
return mlog.TargetCfg{}, fmt.Errorf("cannot encode file options: %w", err)
}
target := mlog.TargetCfg{
Type: "file",
Levels: levels,
Options: fileOpts,
MaxQueueSize: 1000,
}
if json {
target.Format = "json"
target.FormatOptions = makeJSONFormatOptions()
} else {
target.Format = "plain"
target.FormatOptions = makePlainFormatOptions(false)
}
return target, nil
}
func stdLevels(level string) ([]mlog.Level, error) {
stdLevel, err := stringToStdLevel(level)
if err != nil {
return nil, err
}
var levels []mlog.Level
for _, l := range mlog.StdAll {
if l.ID <= stdLevel.ID {
levels = append(levels, l)
}
}
return levels, nil
}
func stringToStdLevel(level string) (mlog.Level, error) {
level = strings.ToLower(level)
for _, l := range mlog.StdAll {
if l.Name == level {
return l, nil
}
}
return mlog.Level{}, fmt.Errorf("%s is not a standard level", level)
}
func makeJSONFormatOptions() json.RawMessage {
str := fmt.Sprintf(`{"enable_caller": %t}`, LogEnableCaller)
return json.RawMessage(str)
}
func makePlainFormatOptions(enableColor bool) json.RawMessage {
str := fmt.Sprintf(`{"delim": "%s", "min_level_len": %d, "min_msg_len": %d, "enable_color": %t, "enable_caller": %t}`,
LogDelim, LogMinLevelLen, LogMinMsgLen, enableColor, LogEnableCaller)
return json.RawMessage(str)
}
func makeFileOptions(filename string) (json.RawMessage, error) {
opts := struct {
Filename string `json:"filename"`
Max_size int `json:"max_size"`
Max_age int `json:"max_age"`
Max_backups int `json:"max_backups"`
Compress bool `json:"compress"`
}{
Filename: filename,
Max_size: LogRotateSizeMB,
Max_age: LogRotateMaxAge,
Max_backups: LogRotateMaxBackups,
Compress: LogCompress,
}
b, err := json.Marshal(opts)
if err != nil {
return nil, err
}
return json.RawMessage(b), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"fmt"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
)
// MemoryStore implements the Store interface. It is meant primarily for testing.
// Not to be used directly. Only to be used as a backing store for config.Store
type MemoryStore struct {
allowEnvironmentOverrides bool
validate bool
files map[string][]byte
savedConfig *model.Config
}
// MemoryStoreOptions makes configuration of the memory store explicit.
type MemoryStoreOptions struct {
IgnoreEnvironmentOverrides bool
SkipValidation bool
InitialConfig *model.Config
InitialFiles map[string][]byte
}
// NewMemoryStore creates a new MemoryStore instance with default options.
func NewMemoryStore() (*MemoryStore, error) {
return NewMemoryStoreWithOptions(&MemoryStoreOptions{})
}
// NewMemoryStoreWithOptions creates a new MemoryStore instance.
func NewMemoryStoreWithOptions(options *MemoryStoreOptions) (*MemoryStore, error) {
savedConfig := options.InitialConfig
if savedConfig == nil {
savedConfig = &model.Config{}
savedConfig.SetDefaults()
}
initialFiles := options.InitialFiles
if initialFiles == nil {
initialFiles = make(map[string][]byte)
}
ms := &MemoryStore{
allowEnvironmentOverrides: !options.IgnoreEnvironmentOverrides,
validate: !options.SkipValidation,
files: initialFiles,
savedConfig: savedConfig,
}
return ms, nil
}
// Set replaces the current configuration in its entirety.
func (ms *MemoryStore) Set(newCfg *model.Config) error {
return ms.persist(newCfg)
}
// persist copies the active config to the saved config.
func (ms *MemoryStore) persist(cfg *model.Config) error {
ms.savedConfig = cfg.Clone()
return nil
}
// Load applies environment overrides to the default config as if a re-load had occurred.
func (ms *MemoryStore) Load() ([]byte, error) {
cfgBytes, err := marshalConfig(ms.savedConfig)
if err != nil {
return nil, errors.Wrap(err, "failed to serialize config")
}
return cfgBytes, nil
}
// GetFile fetches the contents of a previously persisted configuration file.
func (ms *MemoryStore) GetFile(name string) ([]byte, error) {
data, ok := ms.files[name]
if !ok {
return nil, fmt.Errorf("file %s not stored", name)
}
return data, nil
}
// SetFile sets or replaces the contents of a configuration file.
func (ms *MemoryStore) SetFile(name string, data []byte) error {
ms.files[name] = data
return nil
}
// HasFile returns true if the given file was previously persisted.
func (ms *MemoryStore) HasFile(name string) (bool, error) {
_, ok := ms.files[name]
return ok, nil
}
// RemoveFile removes a previously persisted configuration file.
func (ms *MemoryStore) RemoveFile(name string) error {
delete(ms.files, name)
return nil
}
// String returns a hard-coded description, as there is no backing store.
func (ms *MemoryStore) String() string {
return "memory://"
}
// Close does nothing for a memory store.
func (ms *MemoryStore) Close() error {
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"github.com/pkg/errors"
)
// Migrate migrates SAML keys, certificates, and other config files from one store to another given their data source names.
func Migrate(from, to string) error {
source, err := NewStoreFromDSN(from, false, nil, false)
if err != nil {
return errors.Wrapf(err, "failed to access source config %s", from)
}
defer source.Close()
destination, err := NewStoreFromDSN(to, false, nil, true)
if err != nil {
return errors.Wrapf(err, "failed to access destination config %s", to)
}
defer destination.Close()
sourceConfig := source.Get()
if _, _, err = destination.Set(sourceConfig); err != nil {
return errors.Wrapf(err, "failed to set config")
}
files := []string{
*sourceConfig.SamlSettings.IdpCertificateFile,
*sourceConfig.SamlSettings.PublicCertificateFile,
*sourceConfig.SamlSettings.PrivateKeyFile,
}
// Only migrate advanced logging config if it is not embedded JSON.
if !isJSONMap(*sourceConfig.LogSettings.AdvancedLoggingConfig) {
files = append(files, *sourceConfig.LogSettings.AdvancedLoggingConfig)
}
files = append(files, sourceConfig.PluginSettings.SignaturePublicKeyFiles...)
for _, file := range files {
if err := migrateFile(file, source, destination); err != nil {
return err
}
}
return nil
}
func migrateFile(name string, source *Store, destination *Store) error {
fileExists, err := source.HasFile(name)
if err != nil {
return errors.Wrapf(err, "failed to check existence of %s", name)
}
if fileExists {
file, err := source.GetFile(name)
if err != nil {
return errors.Wrapf(err, "failed to migrate %s", name)
}
err = destination.SetFile(name, file)
if err != nil {
return errors.Wrapf(err, "failed to migrate %s", name)
}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"encoding/json"
"reflect"
"sync"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/jsonutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
var (
// ErrReadOnlyStore is returned when an attempt to modify a read-only
// configuration store is made.
ErrReadOnlyStore = errors.New("configuration store is read-only")
)
// Store is the higher level object that handles storing and retrieval of config data.
// To do so it relies on a variety of backing stores (e.g. file, database, memory).
type Store struct {
emitter
backingStore BackingStore
configLock sync.RWMutex
config *model.Config
configNoEnv *model.Config
configCustomDefaults *model.Config
readOnly bool
readOnlyFF bool
}
// BackingStore defines the behaviour exposed by the underlying store
// implementation (e.g. file, database).
type BackingStore interface {
// Set replaces the current configuration in its entirety and updates the backing store.
Set(*model.Config) error
// Load retrieves the configuration stored. If there is no configuration stored
// the io.ReadCloser will be nil
Load() ([]byte, error)
// GetFile fetches the contents of a previously persisted configuration file.
// If no such file exists, an empty byte array will be returned without error.
GetFile(name string) ([]byte, error)
// SetFile sets or replaces the contents of a configuration file.
SetFile(name string, data []byte) error
// HasFile returns true if the given file was previously persisted.
HasFile(name string) (bool, error)
// RemoveFile removes a previously persisted configuration file.
RemoveFile(name string) error
// String describes the backing store for the config.
String() string
// Close cleans up resources associated with the store.
Close() error
}
// NewStoreFromBacking creates and returns a new config store given a backing store.
func NewStoreFromBacking(backingStore BackingStore, customDefaults *model.Config, readOnly bool) (*Store, error) {
store := &Store{
backingStore: backingStore,
configCustomDefaults: customDefaults,
readOnly: readOnly,
readOnlyFF: true,
}
if err := store.Load(); err != nil {
return nil, errors.Wrap(err, "unable to load on store creation")
}
return store, nil
}
// NewStoreFromDSN creates and returns a new config store backed by either a database or file store
// depending on the value of the given data source name string.
func NewStoreFromDSN(dsn string, readOnly bool, customDefaults *model.Config, createFileIfNotExist bool) (*Store, error) {
var err error
var backingStore BackingStore
if IsDatabaseDSN(dsn) {
backingStore, err = NewDatabaseStore(dsn)
} else {
backingStore, err = NewFileStore(dsn, createFileIfNotExist)
}
if err != nil {
return nil, err
}
store, err := NewStoreFromBacking(backingStore, customDefaults, readOnly)
if err != nil {
backingStore.Close()
return nil, errors.Wrap(err, "failed to create store")
}
return store, nil
}
// NewTestMemoryStore returns a new config store backed by a memory store
// to be used for testing purposes.
func NewTestMemoryStore() *Store {
memoryStore, err := NewMemoryStore()
if err != nil {
panic("failed to initialize memory store: " + err.Error())
}
configStore, err := NewStoreFromBacking(memoryStore, nil, false)
if err != nil {
panic("failed to initialize config store: " + err.Error())
}
return configStore
}
// Get fetches the current, cached configuration.
func (s *Store) Get() *model.Config {
s.configLock.RLock()
defer s.configLock.RUnlock()
return s.config
}
// GetNoEnv fetches the current cached configuration without environment variable overrides.
func (s *Store) GetNoEnv() *model.Config {
s.configLock.RLock()
defer s.configLock.RUnlock()
return s.configNoEnv
}
// GetEnvironmentOverrides fetches the configuration fields overridden by environment variables.
func (s *Store) GetEnvironmentOverrides() map[string]any {
return generateEnvironmentMap(GetEnvironment(), nil)
}
// GetEnvironmentOverridesWithFilter fetches the configuration fields overridden by environment variables.
// If filter is not nil and returns false for a struct field, that field will be omitted.
func (s *Store) GetEnvironmentOverridesWithFilter(filter func(reflect.StructField) bool) map[string]any {
return generateEnvironmentMap(GetEnvironment(), filter)
}
// RemoveEnvironmentOverrides returns a new config without the environment
// overrides.
func (s *Store) RemoveEnvironmentOverrides(cfg *model.Config) *model.Config {
s.configLock.RLock()
defer s.configLock.RUnlock()
return removeEnvOverrides(cfg, s.configNoEnv, s.GetEnvironmentOverrides())
}
// SetReadOnlyFF sets whether feature flags should be written out to
// config or treated as read-only.
func (s *Store) SetReadOnlyFF(readOnly bool) {
s.configLock.Lock()
defer s.configLock.Unlock()
s.readOnlyFF = readOnly
}
// Set replaces the current configuration in its entirety and updates the backing store.
// It returns both old and new versions of the config.
func (s *Store) Set(newCfg *model.Config) (*model.Config, *model.Config, error) {
s.configLock.Lock()
defer s.configLock.Unlock()
if s.readOnly {
return nil, nil, ErrReadOnlyStore
}
newCfg = newCfg.Clone()
oldCfg := s.config.Clone()
oldCfgNoEnv := s.configNoEnv
// Setting defaults allows us to accept partial config objects.
newCfg.SetDefaults()
// Sometimes the config is received with "fake" data in sensitive fields. Apply the real
// data from the existing config as necessary.
desanitize(oldCfg, newCfg)
// We apply back environment overrides since the input config may or
// may not have them applied.
newCfg = applyEnvironmentMap(newCfg, GetEnvironment())
fixConfig(newCfg)
if err := newCfg.IsValid(); err != nil {
return nil, nil, errors.Wrap(err, "new configuration is invalid")
}
// We attempt to remove any environment override that may be present in the input config.
newCfgNoEnv := removeEnvOverrides(newCfg, oldCfgNoEnv, s.GetEnvironmentOverrides())
// Don't store feature flags unless we are on MM cloud
// MM cloud uses config in the DB as a cache of the feature flag
// settings in case the management system is down when a pod starts.
// Backing up feature flags section in case we need to restore them later on.
oldCfgFF := oldCfg.FeatureFlags
oldCfgNoEnvFF := oldCfgNoEnv.FeatureFlags
// Clearing FF sections to avoid both comparing and persisting them.
if s.readOnlyFF {
oldCfg.FeatureFlags = nil
newCfg.FeatureFlags = nil
newCfgNoEnv.FeatureFlags = nil
}
if err := s.backingStore.Set(newCfgNoEnv); err != nil {
return nil, nil, errors.Wrap(err, "failed to persist")
}
hasChanged, err := equal(oldCfg, newCfg)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to compare configs")
}
// We restore the previously cleared feature flags sections back.
if s.readOnlyFF {
oldCfg.FeatureFlags = oldCfgFF
newCfg.FeatureFlags = oldCfgFF
newCfgNoEnv.FeatureFlags = oldCfgNoEnvFF
}
s.configNoEnv = newCfgNoEnv
s.config = newCfg
newCfgCopy := newCfg.Clone()
if hasChanged {
s.configLock.Unlock()
s.invokeConfigListeners(oldCfg, newCfgCopy.Clone())
s.configLock.Lock()
}
return oldCfg, newCfgCopy, nil
}
// Load updates the current configuration from the backing store, possibly initializing.
func (s *Store) Load() error {
s.configLock.Lock()
defer s.configLock.Unlock()
oldCfg := &model.Config{}
if s.config != nil {
oldCfg = s.config.Clone()
}
configBytes, err := s.backingStore.Load()
if err != nil {
return err
}
loadedCfg := &model.Config{}
if len(configBytes) != 0 {
if err = json.Unmarshal(configBytes, &loadedCfg); err != nil {
return jsonutils.HumanizeJSONError(err, configBytes)
}
}
// If we have custom defaults set, the initial config is merged on
// top of them and we delete them not to be used again in the
// configuration reloads
if s.configCustomDefaults != nil {
var mErr error
loadedCfg, mErr = Merge(s.configCustomDefaults, loadedCfg, nil)
if mErr != nil {
return errors.Wrap(mErr, "failed to merge custom config defaults")
}
s.configCustomDefaults = nil
}
// We set the SiteURL to empty (if nil) so that the following call to
// SetDefaults() will generate missing data. This avoids an additional write
// to the backing store.
if loadedCfg.ServiceSettings.SiteURL == nil {
loadedCfg.ServiceSettings.SiteURL = model.NewString("")
}
// Setting defaults allows us to accept partial config objects.
loadedCfg.SetDefaults()
// No need to clone here since the below call to applyEnvironmentMap
// already does that internally.
loadedCfgNoEnv := loadedCfg
fixConfig(loadedCfgNoEnv)
loadedCfg = applyEnvironmentMap(loadedCfg, GetEnvironment())
fixConfig(loadedCfg)
if appErr := loadedCfg.IsValid(); appErr != nil {
// Translating the error before displaying it in the console.
// Defaulting to english for server side language.
appErr.Translate(i18n.GetUserTranslations("en"))
return errors.Wrap(appErr, "invalid config")
}
// Backing up feature flags section in case we need to restore them later on.
oldCfgFF := oldCfg.FeatureFlags
loadedCfgFF := loadedCfg.FeatureFlags
loadedCfgNoEnvFF := loadedCfgNoEnv.FeatureFlags
// Clearing FF sections to avoid both comparing and persisting them.
if s.readOnlyFF {
oldCfg.FeatureFlags = nil
loadedCfg.FeatureFlags = nil
loadedCfgNoEnv.FeatureFlags = nil
}
// Check for changes that may have happened on load to the backing store.
hasChanged, err := equal(oldCfg, loadedCfg)
if err != nil {
return errors.Wrap(err, "failed to compare configs")
}
// We write back to the backing store only if the store is not read-only
// and the config has either changed or is missing.
if !s.readOnly && (hasChanged || len(configBytes) == 0) {
err := s.backingStore.Set(loadedCfgNoEnv)
if err != nil && !errors.Is(err, ErrReadOnlyConfiguration) {
return errors.Wrap(err, "failed to persist")
}
}
// We restore the previously cleared feature flags sections back.
if s.readOnlyFF {
oldCfg.FeatureFlags = oldCfgFF
loadedCfg.FeatureFlags = loadedCfgFF
loadedCfgNoEnv.FeatureFlags = loadedCfgNoEnvFF
}
s.config = loadedCfg
s.configNoEnv = loadedCfgNoEnv
loadedCfgCopy := loadedCfg.Clone()
if hasChanged {
s.configLock.Unlock()
s.invokeConfigListeners(oldCfg, loadedCfgCopy)
s.configLock.Lock()
}
return nil
}
// GetFile fetches the contents of a previously persisted configuration file.
// If no such file exists, an empty byte array will be returned without error.
func (s *Store) GetFile(name string) ([]byte, error) {
s.configLock.RLock()
defer s.configLock.RUnlock()
return s.backingStore.GetFile(name)
}
// SetFile sets or replaces the contents of a configuration file.
func (s *Store) SetFile(name string, data []byte) error {
s.configLock.Lock()
defer s.configLock.Unlock()
if s.readOnly {
return ErrReadOnlyStore
}
return s.backingStore.SetFile(name, data)
}
// HasFile returns true if the given file was previously persisted.
func (s *Store) HasFile(name string) (bool, error) {
s.configLock.RLock()
defer s.configLock.RUnlock()
return s.backingStore.HasFile(name)
}
// RemoveFile removes a previously persisted configuration file.
func (s *Store) RemoveFile(name string) error {
s.configLock.Lock()
defer s.configLock.Unlock()
if s.readOnly {
return ErrReadOnlyStore
}
return s.backingStore.RemoveFile(name)
}
// String describes the backing store for the config.
func (s *Store) String() string {
return s.backingStore.String()
}
// Close cleans up resources associated with the store.
func (s *Store) Close() error {
s.configLock.Lock()
defer s.configLock.Unlock()
return s.backingStore.Close()
}
// IsReadOnly returns whether or not the store is read-only.
func (s *Store) IsReadOnly() bool {
s.configLock.RLock()
defer s.configLock.RUnlock()
return s.readOnly
}
// Cleanup removes outdated configurations from the database.
// this is a no-op function for FileStore type backing store.
func (s *Store) CleanUp() error {
switch bs := s.backingStore.(type) {
case *DatabaseStore:
dur := time.Duration(*s.config.JobSettings.CleanupConfigThresholdDays) * time.Hour * 24
expiry := model.GetMillisForTime(time.Now().Add(-dur))
return bs.cleanUp(int(expiry))
default:
return nil
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// marshalConfig converts the given configuration into JSON bytes for persistence.
func marshalConfig(cfg *model.Config) ([]byte, error) {
return json.MarshalIndent(cfg, "", " ")
}
// desanitize replaces fake settings with their actual values.
func desanitize(actual, target *model.Config) {
if target.LdapSettings.BindPassword != nil && *target.LdapSettings.BindPassword == model.FakeSetting {
*target.LdapSettings.BindPassword = *actual.LdapSettings.BindPassword
}
if *target.FileSettings.PublicLinkSalt == model.FakeSetting {
*target.FileSettings.PublicLinkSalt = *actual.FileSettings.PublicLinkSalt
}
if *target.FileSettings.AmazonS3SecretAccessKey == model.FakeSetting {
target.FileSettings.AmazonS3SecretAccessKey = actual.FileSettings.AmazonS3SecretAccessKey
}
if *target.EmailSettings.SMTPPassword == model.FakeSetting {
target.EmailSettings.SMTPPassword = actual.EmailSettings.SMTPPassword
}
if *target.GitLabSettings.Secret == model.FakeSetting {
target.GitLabSettings.Secret = actual.GitLabSettings.Secret
}
if target.GoogleSettings.Secret != nil && *target.GoogleSettings.Secret == model.FakeSetting {
target.GoogleSettings.Secret = actual.GoogleSettings.Secret
}
if target.Office365Settings.Secret != nil && *target.Office365Settings.Secret == model.FakeSetting {
target.Office365Settings.Secret = actual.Office365Settings.Secret
}
if target.OpenIdSettings.Secret != nil && *target.OpenIdSettings.Secret == model.FakeSetting {
target.OpenIdSettings.Secret = actual.OpenIdSettings.Secret
}
if *target.SqlSettings.DataSource == model.FakeSetting {
*target.SqlSettings.DataSource = *actual.SqlSettings.DataSource
}
if *target.SqlSettings.AtRestEncryptKey == model.FakeSetting {
target.SqlSettings.AtRestEncryptKey = actual.SqlSettings.AtRestEncryptKey
}
if *target.ElasticsearchSettings.Password == model.FakeSetting {
*target.ElasticsearchSettings.Password = *actual.ElasticsearchSettings.Password
}
if len(target.SqlSettings.DataSourceReplicas) == len(actual.SqlSettings.DataSourceReplicas) {
for i, value := range target.SqlSettings.DataSourceReplicas {
if value == model.FakeSetting {
target.SqlSettings.DataSourceReplicas[i] = actual.SqlSettings.DataSourceReplicas[i]
}
}
}
if len(target.SqlSettings.DataSourceSearchReplicas) == len(actual.SqlSettings.DataSourceSearchReplicas) {
for i, value := range target.SqlSettings.DataSourceSearchReplicas {
if value == model.FakeSetting {
target.SqlSettings.DataSourceSearchReplicas[i] = actual.SqlSettings.DataSourceSearchReplicas[i]
}
}
}
if *target.MessageExportSettings.GlobalRelaySettings.SMTPPassword == model.FakeSetting {
*target.MessageExportSettings.GlobalRelaySettings.SMTPPassword = *actual.MessageExportSettings.GlobalRelaySettings.SMTPPassword
}
if target.ServiceSettings.GfycatAPISecret != nil && *target.ServiceSettings.GfycatAPISecret == model.FakeSetting {
*target.ServiceSettings.GfycatAPISecret = *actual.ServiceSettings.GfycatAPISecret
}
if *target.ServiceSettings.SplitKey == model.FakeSetting {
*target.ServiceSettings.SplitKey = *actual.ServiceSettings.SplitKey
}
}
// fixConfig patches invalid or missing data in the configuration.
func fixConfig(cfg *model.Config) {
// Ensure SiteURL has no trailing slash.
if strings.HasSuffix(*cfg.ServiceSettings.SiteURL, "/") {
*cfg.ServiceSettings.SiteURL = strings.TrimRight(*cfg.ServiceSettings.SiteURL, "/")
}
// Ensure the directory for a local file store has a trailing slash.
if *cfg.FileSettings.DriverName == model.ImageDriverLocal {
if *cfg.FileSettings.Directory != "" && !strings.HasSuffix(*cfg.FileSettings.Directory, "/") {
*cfg.FileSettings.Directory += "/"
}
}
FixInvalidLocales(cfg)
}
// FixInvalidLocales checks and corrects the given config for invalid locale-related settings.
//
// Ideally, this function would be completely internal, but it's currently exposed to allow the cli
// to test the config change before allowing the save.
func FixInvalidLocales(cfg *model.Config) bool {
var changed bool
locales := i18n.GetSupportedLocales()
if _, ok := locales[*cfg.LocalizationSettings.DefaultServerLocale]; !ok {
*cfg.LocalizationSettings.DefaultServerLocale = model.DefaultLocale
mlog.Warn("DefaultServerLocale must be one of the supported locales. Setting DefaultServerLocale to en as default value.")
changed = true
}
if _, ok := locales[*cfg.LocalizationSettings.DefaultClientLocale]; !ok {
*cfg.LocalizationSettings.DefaultClientLocale = model.DefaultLocale
mlog.Warn("DefaultClientLocale must be one of the supported locales. Setting DefaultClientLocale to en as default value.")
changed = true
}
if *cfg.LocalizationSettings.AvailableLocales != "" {
isDefaultClientLocaleInAvailableLocales := false
for _, word := range strings.Split(*cfg.LocalizationSettings.AvailableLocales, ",") {
if _, ok := locales[word]; !ok {
*cfg.LocalizationSettings.AvailableLocales = ""
isDefaultClientLocaleInAvailableLocales = true
mlog.Warn("AvailableLocales must include DefaultClientLocale. Setting AvailableLocales to all locales as default value.")
changed = true
break
}
if word == *cfg.LocalizationSettings.DefaultClientLocale {
isDefaultClientLocaleInAvailableLocales = true
}
}
availableLocales := *cfg.LocalizationSettings.AvailableLocales
if !isDefaultClientLocaleInAvailableLocales {
availableLocales += "," + *cfg.LocalizationSettings.DefaultClientLocale
mlog.Warn("Adding DefaultClientLocale to AvailableLocales.")
changed = true
}
*cfg.LocalizationSettings.AvailableLocales = strings.Join(utils.RemoveDuplicatesFromStringArray(strings.Split(availableLocales, ",")), ",")
}
return changed
}
// Merge merges two configs together. The receiver's values are overwritten with the patch's
// values except when the patch's values are nil.
func Merge(cfg *model.Config, patch *model.Config, mergeConfig *utils.MergeConfig) (*model.Config, error) {
ret, err := utils.Merge(cfg, patch, mergeConfig)
if err != nil {
return nil, err
}
retCfg := ret.(model.Config)
return &retCfg, nil
}
func IsDatabaseDSN(dsn string) bool {
return strings.HasPrefix(dsn, "mysql://") ||
strings.HasPrefix(dsn, "postgres://") ||
strings.HasPrefix(dsn, "postgresql://")
}
// stripPassword remove the password from a given DSN
func stripPassword(dsn, schema string) string {
prefix := schema + "://"
dsn = strings.TrimPrefix(dsn, prefix)
i := strings.Index(dsn, ":")
j := strings.LastIndex(dsn, "@")
// Return error if no @ sign is found
if j < 0 {
return "(omitted due to error parsing the DSN)"
}
// Return back the input if no password is found
if i < 0 || i > j {
return prefix + dsn
}
return prefix + dsn[:i+1] + dsn[j:]
}
func isJSONMap(data string) bool {
var m map[string]any
return json.Unmarshal([]byte(data), &m) == nil
}
func GetValueByPath(path []string, obj any) (any, bool) {
r := reflect.ValueOf(obj)
var val reflect.Value
if r.Kind() == reflect.Map {
val = r.MapIndex(reflect.ValueOf(path[0]))
if val.IsValid() {
val = val.Elem()
}
} else {
val = r.FieldByName(path[0])
}
if !val.IsValid() {
return nil, false
}
switch {
case len(path) == 1:
return val.Interface(), true
case val.Kind() == reflect.Struct:
return GetValueByPath(path[1:], val.Interface())
case val.Kind() == reflect.Map:
remainingPath := strings.Join(path[1:], ".")
mapIter := val.MapRange()
for mapIter.Next() {
key := mapIter.Key().String()
if strings.HasPrefix(remainingPath, key) {
i := strings.Count(key, ".") + 2 // number of dots + a dot on each side
mapVal := mapIter.Value()
// if no sub field path specified, return the object
if len(path[i:]) == 0 {
return mapVal.Interface(), true
}
data := mapVal.Interface()
if mapVal.Kind() == reflect.Ptr {
data = mapVal.Elem().Interface() // if value is a pointer, dereference it
}
// pass subpath
return GetValueByPath(path[i:], data)
}
}
}
return nil, false
}
func equal(oldCfg, newCfg *model.Config) (bool, error) {
oldCfgBytes, err := json.Marshal(oldCfg)
if err != nil {
return false, fmt.Errorf("failed to marshal old config: %w", err)
}
newCfgBytes, err := json.Marshal(newCfg)
if err != nil {
return false, fmt.Errorf("failed to marshal new config: %w", err)
}
return !bytes.Equal(oldCfgBytes, newCfgBytes), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package awsmeter
import (
"encoding/json"
"os"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/marketplacemetering"
"github.com/aws/aws-sdk-go/service/marketplacemetering/marketplacemeteringiface"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type AwsMeter struct {
store store.Store
service *AWSMeterService
config *model.Config
}
type AWSMeterService struct {
AwsDryRun bool
AwsProductCode string
AwsMeteringSvc marketplacemeteringiface.MarketplaceMeteringAPI
}
type AWSMeterReport struct {
Dimension string `json:"dimension"`
Value int64 `json:"value"`
Timestamp time.Time `json:"timestamp"`
}
func (o *AWSMeterReport) ToJSON() string {
b, _ := json.Marshal(o)
return string(b)
}
func New(store store.Store, config *model.Config) *AwsMeter {
svc := &AWSMeterService{
AwsDryRun: false,
AwsProductCode: "12345", //TODO
}
service, err := newAWSMarketplaceMeteringService()
if err != nil {
mlog.Debug("Could not create AWS metering service", mlog.String("error", err.Error()))
return nil
}
svc.AwsMeteringSvc = service
return &AwsMeter{
store: store,
service: svc,
config: config,
}
}
func newAWSMarketplaceMeteringService() (*marketplacemetering.MarketplaceMetering, error) {
region := os.Getenv("AWS_REGION")
s, err := session.NewSession(&aws.Config{Region: ®ion})
if err != nil {
return nil, err
}
creds := credentials.NewChainCredentials(
[]credentials.Provider{
&ec2rolecreds.EC2RoleProvider{
Client: ec2metadata.New(s),
},
})
_, err = creds.Get()
if err != nil {
return nil, errors.Wrap(err, "cannot obtain credentials")
}
return marketplacemetering.New(session.Must(session.NewSession(&aws.Config{
Credentials: creds,
}))), nil
}
// a report entry is for all metrics
func (awsm *AwsMeter) GetUserCategoryUsage(dimensions []string, startTime time.Time, endTime time.Time) []*AWSMeterReport {
reports := make([]*AWSMeterReport, 0)
for _, dimension := range dimensions {
var userCount int64
var err error
switch dimension {
case model.AwsMeteringDimensionUsageHrs:
userCount, err = awsm.store.User().AnalyticsActiveCountForPeriod(model.GetMillisForTime(startTime), model.GetMillisForTime(endTime), model.UserCountOptions{})
if err != nil {
mlog.Warn("Failed to obtain usage data", mlog.String("dimension", dimension), mlog.String("start", startTime.String()), mlog.Int64("count", userCount), mlog.Err(err))
continue
}
default:
mlog.Debug("Dimension does not exist!", mlog.String("dimension", dimension))
continue
}
report := &AWSMeterReport{
Dimension: dimension,
Value: userCount,
Timestamp: startTime,
}
reports = append(reports, report)
}
return reports
}
func (awsm *AwsMeter) ReportUserCategoryUsage(reports []*AWSMeterReport) error {
for _, report := range reports {
err := sendReportToMeteringService(awsm.service, report)
if err != nil {
return err
}
}
return nil
}
func sendReportToMeteringService(ams *AWSMeterService, report *AWSMeterReport) error {
params := &marketplacemetering.MeterUsageInput{
DryRun: aws.Bool(ams.AwsDryRun),
ProductCode: aws.String(ams.AwsProductCode),
UsageDimension: aws.String(report.Dimension),
UsageQuantity: aws.Int64(report.Value),
Timestamp: aws.Time(report.Timestamp),
}
resp, err := ams.AwsMeteringSvc.MeterUsage(params)
if err != nil {
return errors.Wrap(err, "Invalid metering service id.")
}
if resp.MeteringRecordId == nil {
return errors.Wrap(err, "Invalid metering service id.")
}
mlog.Debug("Sent record to AWS metering service", mlog.String("dimension", report.Dimension), mlog.Int64("value", report.Value), mlog.String("timestamp", report.Timestamp.String()))
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package cache
import (
"container/list"
"sync"
"time"
"github.com/tinylib/msgp/msgp"
"github.com/vmihailenco/msgpack/v5"
"github.com/mattermost/mattermost-server/v6/model"
)
// LRU is a thread-safe fixed size LRU cache.
type LRU struct {
lock sync.RWMutex
size int
len int
currentGeneration int64
evictList *list.List
items map[string]*list.Element
defaultExpiry time.Duration
name string
invalidateClusterEvent model.ClusterEvent
}
// LRUOptions contains options for initializing LRU cache
type LRUOptions struct {
Name string
Size int
DefaultExpiry time.Duration
InvalidateClusterEvent model.ClusterEvent
// StripedBuckets is used only by LRUStriped and shouldn't be greater than the number
// of CPUs available on the machine running this cache.
StripedBuckets int
}
// entry is used to hold a value in the evictList.
type entry struct {
key string
value []byte
expires time.Time
generation int64
}
// NewLRU creates an LRU of the given size.
func NewLRU(opts LRUOptions) Cache {
return &LRU{
name: opts.Name,
size: opts.Size,
evictList: list.New(),
items: make(map[string]*list.Element, opts.Size),
defaultExpiry: opts.DefaultExpiry,
invalidateClusterEvent: opts.InvalidateClusterEvent,
}
}
// Purge is used to completely clear the cache.
func (l *LRU) Purge() error {
l.lock.Lock()
defer l.lock.Unlock()
l.len = 0
l.currentGeneration++
return nil
}
// Set adds the given key and value to the store without an expiry. If the key already exists,
// it will overwrite the previous value.
func (l *LRU) Set(key string, value any) error {
return l.SetWithExpiry(key, value, 0)
}
// SetWithDefaultExpiry adds the given key and value to the store with the default expiry. If
// the key already exists, it will overwrite the previous value
func (l *LRU) SetWithDefaultExpiry(key string, value any) error {
return l.SetWithExpiry(key, value, l.defaultExpiry)
}
// SetWithExpiry adds the given key and value to the cache with the given expiry. If the key
// already exists, it will overwrite the previous value
func (l *LRU) SetWithExpiry(key string, value any, ttl time.Duration) error {
return l.set(key, value, ttl)
}
// Get the content stored in the cache for the given key, and decode it into the value interface.
// return ErrKeyNotFound if the key is missing from the cache
func (l *LRU) Get(key string, value any) error {
return l.get(key, value)
}
// Remove deletes the value for a key.
func (l *LRU) Remove(key string) error {
l.lock.Lock()
defer l.lock.Unlock()
if ent, ok := l.items[key]; ok {
l.removeElement(ent)
}
return nil
}
// Keys returns a slice of the keys in the cache.
func (l *LRU) Keys() ([]string, error) {
l.lock.RLock()
defer l.lock.RUnlock()
keys := make([]string, l.len)
i := 0
for ent := l.evictList.Back(); ent != nil; ent = ent.Prev() {
e := ent.Value.(*entry)
if e.generation == l.currentGeneration {
keys[i] = e.key
i++
}
}
return keys, nil
}
// Len returns the number of items in the cache.
func (l *LRU) Len() (int, error) {
l.lock.RLock()
defer l.lock.RUnlock()
return l.len, nil
}
// GetInvalidateClusterEvent returns the cluster event configured when this cache was created.
func (l *LRU) GetInvalidateClusterEvent() model.ClusterEvent {
return l.invalidateClusterEvent
}
// Name returns the name of the cache
func (l *LRU) Name() string {
return l.name
}
func (l *LRU) set(key string, value any, ttl time.Duration) error {
var expires time.Time
if ttl > 0 {
expires = time.Now().Add(ttl)
}
var buf []byte
var err error
// We use a fast path for hot structs.
if msgpVal, ok := value.(msgp.Marshaler); ok {
buf, err = msgpVal.MarshalMsg(nil)
} else {
// Slow path for other structs.
buf, err = msgpack.Marshal(value)
}
if err != nil {
return err
}
l.lock.Lock()
defer l.lock.Unlock()
// Check for existing item, ignoring expiry since we'd update anyway.
if ent, ok := l.items[key]; ok {
l.evictList.MoveToFront(ent)
e := ent.Value.(*entry)
e.value = buf
e.expires = expires
if e.generation != l.currentGeneration {
e.generation = l.currentGeneration
l.len++
}
return nil
}
// Add new item
ent := &entry{key, buf, expires, l.currentGeneration}
entry := l.evictList.PushFront(ent)
l.items[key] = entry
l.len++
if l.evictList.Len() > l.size {
l.removeElement(l.evictList.Back())
}
return nil
}
func (l *LRU) get(key string, value any) error {
val, err := l.getItem(key)
if err != nil {
return err
}
// We use a fast path for hot structs.
if msgpVal, ok := value.(msgp.Unmarshaler); ok {
_, err := msgpVal.UnmarshalMsg(val)
return err
}
// This is ugly and makes the cache package aware of the model package.
// But this is due to 2 things.
// 1. The msgp package works on methods on structs rather than functions.
// 2. Our cache interface passes pointers to empty pointers, and not pointers
// to values. This is mainly how all our model structs are passed around.
// It might be technically possible to use values _just_ for hot structs
// like these and then return a pointer while returning from the cache function,
// but it will make the codebase inconsistent, and has some edge-cases to take care of.
switch v := value.(type) {
case **model.User:
var u model.User
_, err := u.UnmarshalMsg(val)
*v = &u
return err
case *map[string]*model.User:
var u model.UserMap
_, err := u.UnmarshalMsg(val)
*v = u
return err
}
// Slow path for other structs.
return msgpack.Unmarshal(val, value)
}
func (l *LRU) getItem(key string) ([]byte, error) {
l.lock.Lock()
defer l.lock.Unlock()
ent, ok := l.items[key]
if !ok {
return nil, ErrKeyNotFound
}
e := ent.Value.(*entry)
if e.generation != l.currentGeneration || (!e.expires.IsZero() && time.Now().After(e.expires)) {
l.removeElement(ent)
return nil, ErrKeyNotFound
}
l.evictList.MoveToFront(ent)
return e.value, nil
}
func (l *LRU) removeElement(e *list.Element) {
l.evictList.Remove(e)
kv := e.Value.(*entry)
if kv.generation == l.currentGeneration {
l.len--
}
delete(l.items, kv.key)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package cache
import (
"fmt"
"math"
"time"
"github.com/cespare/xxhash/v2"
"github.com/mattermost/mattermost-server/v6/model"
)
// LRUStriped keeps LRU caches in buckets in order to lower mutex contention.
// This is achieved by hashing the input key to map it to a dedicated bucket.
// Each bucket (an LRU cache) has its own lock that helps distributing the lock
// contention on multiple threads/cores, leading to less wait times.
//
// LRUStriped implements the Cache interface with the same behavior as LRU.
//
// Note that, because of it's distributed nature, the fixed size cannot be strictly respected
// and you may have a tiny bit more space for keys than you defined through LRUOptions.
// Bucket size is computed as follows: (size / nbuckets) + (size % nbuckets)
//
// Because of this size limit per bucket, and because of the nature of the data, you
// may have buckets filled unevenly, and because of this, keys will be evicted from the entire
// cache where a simple LRU wouldn't have. Example:
//
// Two buckets B1 and B2, of max size 2 each, meaning, theoretically, a max size of 4:
// - Say you have a set of 3 keys, they could fill an entire LRU cache.
// - But if all those keys are assigned to a single bucket B1, the first key will be evicted from B1
// - B2 will remain empty, even though there was enough memory allocated
//
// With 4 buckets and random UUIDs as keys, the amount of false evictions is around 5%.
//
// By default, the number of buckets equals the number of cpus returned from runtime.NumCPU.
//
// This struct is lock-free and intended to be used without lock.
type LRUStriped struct {
buckets []*LRU
name string
invalidateClusterEvent model.ClusterEvent
}
func (L LRUStriped) hashkeyMapHash(key string) uint64 {
return xxhash.Sum64String(key)
}
func (L LRUStriped) keyBucket(key string) *LRU {
return L.buckets[L.hashkeyMapHash(key)%uint64(len(L.buckets))]
}
// Purge loops through each LRU cache for purging. Since LRUStriped doesn't use any lock,
// each LRU bucket is purged after another one, which means that keys could still
// be present after a call to Purge.
func (L LRUStriped) Purge() error {
for _, lru := range L.buckets {
lru.Purge() // errors from purging LRU can be ignored as they always return nil
}
return nil
}
// Set does the same as LRU.Set
func (L LRUStriped) Set(key string, value any) error {
return L.keyBucket(key).Set(key, value)
}
// SetWithDefaultExpiry does the same as LRU.SetWithDefaultExpiry
func (L LRUStriped) SetWithDefaultExpiry(key string, value any) error {
return L.keyBucket(key).SetWithDefaultExpiry(key, value)
}
// SetWithExpiry does the same as LRU.SetWithExpiry
func (L LRUStriped) SetWithExpiry(key string, value any, ttl time.Duration) error {
return L.keyBucket(key).SetWithExpiry(key, value, ttl)
}
// Get does the same as LRU.Get
func (L LRUStriped) Get(key string, value any) error {
return L.keyBucket(key).Get(key, value)
}
// Remove does the same as LRU.Remove
func (L LRUStriped) Remove(key string) error {
return L.keyBucket(key).Remove(key)
}
// Keys does the same as LRU.Keys. However, because this is lock-free, keys might be
// inserted or removed from a previously scanned LRU cache.
// This is not as precise as using a single LRU instance.
func (L LRUStriped) Keys() ([]string, error) {
var keys []string
for _, lru := range L.buckets {
k, _ := lru.Keys() // Keys never returns any error
keys = append(keys, k...)
}
return keys, nil
}
// Len does the same as LRU.Len. As for LRUStriped.Keys, this call cannot be precise.
func (L LRUStriped) Len() (int, error) {
var size int
for _, lru := range L.buckets {
s, _ := lru.Len() // Len never returns any error
size += s
}
return size, nil
}
// GetInvalidateClusterEvent does the same as LRU.GetInvalidateClusterEvent
func (L LRUStriped) GetInvalidateClusterEvent() model.ClusterEvent {
return L.invalidateClusterEvent
}
// Name does the same as LRU.Name
func (L LRUStriped) Name() string {
return L.name
}
// NewLRUStriped creates a striped LRU cache using the special LRUOptions.StripedBuckets value.
// See LRUStriped and LRUOptions for more details.
//
// Not that in order to prevent false eviction, this LRU cache adds 10% (computation is rounded up) of the
// requested size to the total cache size.
func NewLRUStriped(opts LRUOptions) (Cache, error) {
if opts.StripedBuckets == 0 {
return nil, fmt.Errorf("number of buckets is mandatory")
}
if opts.Size < opts.StripedBuckets {
return nil, fmt.Errorf("cache size must at least be equal to the number of buckets")
}
// add 10% to the total size, before splitting
opts.Size += int(math.Ceil(float64(opts.Size) * 10.0 / 100.0))
// now this is the size for each bucket
opts.Size = (opts.Size / opts.StripedBuckets) + (opts.Size % opts.StripedBuckets)
buckets := make([]*LRU, opts.StripedBuckets)
for i := 0; i < opts.StripedBuckets; i++ {
buckets[i] = NewLRU(opts).(*LRU)
}
return LRUStriped{
buckets: buckets,
invalidateClusterEvent: opts.InvalidateClusterEvent,
name: opts.Name,
}, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package cache
import (
"time"
"github.com/mattermost/mattermost-server/v6/model"
)
// CacheOptions contains options for initializing a cache
type CacheOptions struct {
Size int
DefaultExpiry time.Duration
Name string
InvalidateClusterEvent model.ClusterEvent
Striped bool
StripedBuckets int
}
// Provider is a provider for Cache
type Provider interface {
// NewCache creates a new cache with given options.
NewCache(opts *CacheOptions) (Cache, error)
// Connect opens a new connection to the cache using specific provider parameters.
Connect() error
// Close releases any resources used by the cache provider.
Close() error
}
type cacheProvider struct {
}
// NewProvider creates a new CacheProvider
func NewProvider() Provider {
return &cacheProvider{}
}
// NewCache creates a new cache with given opts
func (c *cacheProvider) NewCache(opts *CacheOptions) (Cache, error) {
if opts.Striped {
return NewLRUStriped(LRUOptions{
Name: opts.Name,
Size: opts.Size,
DefaultExpiry: opts.DefaultExpiry,
InvalidateClusterEvent: opts.InvalidateClusterEvent,
StripedBuckets: opts.StripedBuckets,
})
}
return NewLRU(LRUOptions{
Name: opts.Name,
Size: opts.Size,
DefaultExpiry: opts.DefaultExpiry,
InvalidateClusterEvent: opts.InvalidateClusterEvent,
}), nil
}
// Connect opens a new connection to the cache using specific provider parameters.
func (c *cacheProvider) Connect() error {
return nil
}
// Close releases any resources used by the cache provider.
func (c *cacheProvider) Close() error {
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package docextractor
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/mholt/archiver/v3"
)
type archiveExtractor struct {
SubExtractor Extractor
}
func (ae *archiveExtractor) Match(filename string) bool {
_, err := archiver.ByExtension(filename)
return err == nil
}
func (ae *archiveExtractor) Extract(name string, r io.ReadSeeker) (string, error) {
dir, err := os.MkdirTemp(os.TempDir(), "archiver")
if err != nil {
return "", fmt.Errorf("error creating temporary file: %v", err)
}
defer os.RemoveAll(dir)
f, err := os.Create(filepath.Join(dir, name))
if err != nil {
return "", fmt.Errorf("error copying data into temporary file: %v", err)
}
_, err = io.Copy(f, r)
f.Close()
if err != nil {
return "", fmt.Errorf("error copying data into temporary file: %v", err)
}
var text strings.Builder
err = archiver.Walk(f.Name(), func(file archiver.File) error {
text.WriteString(file.Name() + " ")
if ae.SubExtractor != nil {
filename := filepath.Base(file.Name())
filename = strings.ReplaceAll(filename, "-", " ")
filename = strings.ReplaceAll(filename, ".", " ")
filename = strings.ReplaceAll(filename, ",", " ")
data, err2 := io.ReadAll(file)
if err2 != nil {
return err2
}
subtext, extractErr := ae.SubExtractor.Extract(filename, bytes.NewReader(data))
if extractErr == nil {
text.WriteString(subtext + " ")
}
}
return nil
})
if err != nil {
return "", err
}
return text.String(), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package docextractor
import (
"io"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type combineExtractor struct {
SubExtractors []Extractor
}
func (ce *combineExtractor) Add(extractor Extractor) {
ce.SubExtractors = append(ce.SubExtractors, extractor)
}
func (ce *combineExtractor) Match(filename string) bool {
for _, extractor := range ce.SubExtractors {
if extractor.Match(filename) {
return true
}
}
return false
}
func (ce *combineExtractor) Extract(filename string, r io.ReadSeeker) (string, error) {
for _, extractor := range ce.SubExtractors {
if extractor.Match(filename) {
r.Seek(0, io.SeekStart)
text, err := extractor.Extract(filename, r)
if err != nil {
mlog.Warn("unable to extract file content", mlog.Err(err))
continue
}
return text, nil
}
}
return "", nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package docextractor
import (
"io"
)
// ExtractSettings defines the features enabled/disable during the document text extraction.
type ExtractSettings struct {
ArchiveRecursion bool
MMPreviewURL string
MMPreviewSecret string
}
// Extract extract the text from a document using the system default extractors
func Extract(filename string, r io.ReadSeeker, settings ExtractSettings) (string, error) {
return ExtractWithExtraExtractors(filename, r, settings, []Extractor{})
}
// ExtractWithExtraExtractors extract the text from a document using the provided extractors beside the system default extractors.
func ExtractWithExtraExtractors(filename string, r io.ReadSeeker, settings ExtractSettings, extraExtractors []Extractor) (string, error) {
enabledExtractors := &combineExtractor{}
for _, extraExtractor := range extraExtractors {
enabledExtractors.Add(extraExtractor)
}
enabledExtractors.Add(&documentExtractor{})
enabledExtractors.Add(&pdfExtractor{})
if settings.ArchiveRecursion {
enabledExtractors.Add(&archiveExtractor{SubExtractor: enabledExtractors})
} else {
enabledExtractors.Add(&archiveExtractor{})
}
if settings.MMPreviewURL != "" {
enabledExtractors.Add(newMMPreviewExtractor(settings.MMPreviewURL, settings.MMPreviewSecret, pdfExtractor{}))
}
enabledExtractors.Add(&plainExtractor{})
if enabledExtractors.Match(filename) {
return enabledExtractors.Extract(filename, r)
}
return "", nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package docextractor
import (
"errors"
"io"
"path"
"strings"
"code.sajari.com/docconv"
)
type documentExtractor struct{}
var doconvConverterByExtensions = map[string]func(io.Reader) (string, map[string]string, error){
"doc": docconv.ConvertDoc,
"docx": docconv.ConvertDocx,
"pptx": docconv.ConvertPptx,
"odt": docconv.ConvertODT,
"html": func(r io.Reader) (string, map[string]string, error) { return docconv.ConvertHTML(r, true) },
// Temporarily disabled to avoid crashes on malicious .pages files
// "pages": docconv.ConvertPages,
"rtf": docconv.ConvertRTF,
"pdf": docconv.ConvertPDF,
}
func (de *documentExtractor) Match(filename string) bool {
extension := strings.TrimPrefix(path.Ext(filename), ".")
_, ok := doconvConverterByExtensions[extension]
return ok
}
func (de *documentExtractor) Extract(filename string, r io.ReadSeeker) (out string, outErr error) {
defer func() {
if r := recover(); r != nil {
out = ""
outErr = errors.New("error extracting document text")
}
}()
extension := strings.TrimPrefix(path.Ext(filename), ".")
converter, ok := doconvConverterByExtensions[extension]
if !ok {
return "", errors.New("unknown converter")
}
text, _, err := converter(r)
if err != nil {
return "", err
}
return text, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package docextractor
// MMPreview is a micro-service to convert from any libreoffice supported
// format into a PDF file, and then we use the regular pdf extractor to convert
// it into plain text.
import (
"bytes"
"io"
"mime/multipart"
"net/http"
"path"
"strings"
"github.com/pkg/errors"
)
type mmPreviewExtractor struct {
url string
secret string
pdfExtractor pdfExtractor
}
var mmpreviewSupportedExtensions = map[string]bool{
"ppt": true,
"odp": true,
"xls": true,
"xlsx": true,
"ods": true,
}
func newMMPreviewExtractor(url string, secret string, pdfExtractor pdfExtractor) *mmPreviewExtractor {
return &mmPreviewExtractor{url: url, secret: secret, pdfExtractor: pdfExtractor}
}
func (mpe *mmPreviewExtractor) Match(filename string) bool {
extension := strings.TrimPrefix(path.Ext(filename), ".")
return mmpreviewSupportedExtensions[extension]
}
func (mpe *mmPreviewExtractor) Extract(filename string, file io.ReadSeeker) (string, error) {
b, w, err := createMultipartFormData("file", filename, file)
if err != nil {
return "", errors.Wrap(err, "Unable to generate file preview using mmpreview.")
}
req, err := http.NewRequest("POST", mpe.url+"/toPDF", &b)
if err != nil {
return "", errors.Wrap(err, "Unable to generate file preview using mmpreview.")
}
req.Header.Set("Content-Type", w.FormDataContentType())
if mpe.secret != "" {
req.Header.Add("Authentication", mpe.secret)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", errors.Wrap(err, "Unable to generate file preview using mmpreview.")
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return "", errors.New("Unable to generate file preview using mmpreview (The server has replied with an error)")
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", errors.Wrap(err, "unable to read the response from mmpreview")
}
return mpe.pdfExtractor.Extract(filename, bytes.NewReader(data))
}
func createMultipartFormData(fieldName, fileName string, fileData io.ReadSeeker) (bytes.Buffer, *multipart.Writer, error) {
var b bytes.Buffer
var err error
w := multipart.NewWriter(&b)
var fw io.Writer
if fw, err = w.CreateFormFile(fieldName, fileName); err != nil {
return b, nil, err
}
if _, err = io.Copy(fw, fileData); err != nil {
return b, nil, err
}
w.Close()
return b, w, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package docextractor
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path"
"strings"
"github.com/ledongthuc/pdf"
)
type pdfExtractor struct{}
func (pe *pdfExtractor) Match(filename string) bool {
supportedExtensions := map[string]bool{
"pdf": true,
}
extension := strings.TrimPrefix(path.Ext(filename), ".")
return supportedExtensions[extension]
}
func (pe *pdfExtractor) Extract(filename string, r io.ReadSeeker) (out string, outErr error) {
defer func() {
if r := recover(); r != nil {
out = ""
outErr = errors.New("error extracting pdf text")
}
}()
f, err := os.CreateTemp(os.TempDir(), "pdflib")
if err != nil {
return "", fmt.Errorf("error creating temporary file: %v", err)
}
defer f.Close()
defer os.Remove(f.Name())
size, err := io.Copy(f, r)
if err != nil {
return "", fmt.Errorf("error copying data into temporary file: %v", err)
}
reader, err := pdf.NewReader(f, size)
if err != nil {
return "", err
}
var buf bytes.Buffer
b, err := reader.GetPlainText()
if err != nil {
return "", err
}
buf.ReadFrom(b)
return buf.String(), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package docextractor
import (
"io"
"unicode"
"unicode/utf8"
)
type plainExtractor struct{}
func (pe *plainExtractor) Match(filename string) bool {
return true
}
func (pe *plainExtractor) Extract(filename string, r io.ReadSeeker) (string, error) {
// This detects any visible character plus any whitespace
validRanges := append(unicode.GraphicRanges, unicode.White_Space)
runes := make([]byte, 1024)
total, err := r.Read(runes)
if err != nil && err != io.EOF {
return "", err
}
if total == 0 {
return "", nil
}
count := 0
for {
c, size := utf8.DecodeRune(runes[count:])
if !unicode.In(c, validRanges...) {
return "", nil
}
if size == 0 {
break
}
count += size
// subtract the max rune size to prevent accidentally splitted runes at the end of first 1024 bytes
if count > total-utf8.UTFMax {
break
}
}
text, _ := io.ReadAll(r)
return string(runes[0:total]) + string(text), nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package httpservice
import (
"context"
"crypto/tls"
"errors"
"net"
"net/http"
"time"
)
const (
ConnectTimeout = 3 * time.Second
RequestTimeout = 30 * time.Second
)
var reservedIPRanges []*net.IPNet
// IsReservedIP checks whether the target IP belongs to reserved IP address ranges to avoid SSRF attacks to the internal
// network of the Mattermost server
func IsReservedIP(ip net.IP) bool {
for _, ipRange := range reservedIPRanges {
if ipRange.Contains(ip) {
return true
}
}
return false
}
// IsOwnIP handles the special case that a request might be made to the public IP of the host which on Linux is routed
// directly via the loopback IP to any listening sockets, effectively bypassing host-based firewalls such as firewalld
func IsOwnIP(ip net.IP) (bool, error) {
interfaces, err := net.Interfaces()
if err != nil {
return false, err
}
for _, interf := range interfaces {
addresses, err := interf.Addrs()
if err != nil {
return false, err
}
for _, addr := range addresses {
var selfIP net.IP
switch v := addr.(type) {
case *net.IPNet:
selfIP = v.IP
case *net.IPAddr:
selfIP = v.IP
}
if ip.Equal(selfIP) {
return true, nil
}
}
}
return false, nil
}
var defaultUserAgent string
func init() {
for _, cidr := range []string{
// See https://tools.ietf.org/html/rfc6890
"0.0.0.0/8", // This host on this network
"10.0.0.0/8", // Private-Use
"127.0.0.0/8", // Loopback
"169.254.0.0/16", // Link Local
"172.16.0.0/12", // Private-Use Networks
"192.168.0.0/16", // Private-Use Networks
"::/128", // Unspecified Address
"::1/128", // Loopback Address
"fc00::/7", // Unique-Local
"fe80::/10", // Linked-Scoped Unicast
} {
_, parsed, err := net.ParseCIDR(cidr)
if err != nil {
panic(err)
}
reservedIPRanges = append(reservedIPRanges, parsed)
}
defaultUserAgent = "Mattermost-Bot/1.1"
}
type DialContextFunction func(ctx context.Context, network, addr string) (net.Conn, error)
var AddressForbidden error = errors.New("address forbidden, you may need to set AllowedUntrustedInternalConnections to allow an integration access to your internal network")
func dialContextFilter(dial DialContextFunction, allowHost func(host string) bool, allowIP func(ip net.IP) bool) DialContextFunction {
return func(ctx context.Context, network, addr string) (net.Conn, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
if allowHost != nil && allowHost(host) {
return dial(ctx, network, addr)
}
ips, err := net.LookupIP(host)
if err != nil {
return nil, err
}
var firstErr error
for _, ip := range ips {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
if allowIP == nil || !allowIP(ip) {
continue
}
conn, err := dial(ctx, network, net.JoinHostPort(ip.String(), port))
if err == nil {
return conn, nil
}
if firstErr == nil {
firstErr = err
}
}
if firstErr == nil {
return nil, AddressForbidden
}
return nil, firstErr
}
}
func NewTransport(enableInsecureConnections bool, allowHost func(host string) bool, allowIP func(ip net.IP) bool) *MattermostTransport {
dialContext := (&net.Dialer{
Timeout: ConnectTimeout,
KeepAlive: 30 * time.Second,
}).DialContext
if allowHost != nil || allowIP != nil {
dialContext = dialContextFilter(dialContext, allowHost, allowIP)
}
return &MattermostTransport{
&http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: dialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: ConnectTimeout,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: enableInsecureConnections,
},
},
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package httpservice
import (
"net"
"net/http"
"strings"
"time"
"unicode"
"github.com/mattermost/mattermost-server/v6/server/platform/services/configservice"
)
// HTTPService wraps the functionality for making http requests to provide some improvements to the default client
// behaviour.
type HTTPService interface {
// MakeClient returns an http client constructed with a RoundTripper as returned by MakeTransport.
MakeClient(trustURLs bool) *http.Client
// MakeTransport returns a RoundTripper that is suitable for making requests to external resources. The default
// implementation provides:
// - A shorter timeout for dial and TLS handshake (defined as constant "ConnectTimeout")
// - A timeout for end-to-end requests
// - A Mattermost-specific user agent header
// - Additional security for untrusted and insecure connections
MakeTransport(trustURLs bool) *MattermostTransport
}
type HTTPServiceImpl struct {
configService configservice.ConfigService
RequestTimeout time.Duration
}
func splitFields(c rune) bool {
return unicode.IsSpace(c) || c == ','
}
func MakeHTTPService(configService configservice.ConfigService) HTTPService {
return &HTTPServiceImpl{
configService,
RequestTimeout,
}
}
func (h *HTTPServiceImpl) MakeClient(trustURLs bool) *http.Client {
return &http.Client{
Transport: h.MakeTransport(trustURLs),
Timeout: h.RequestTimeout,
}
}
func (h *HTTPServiceImpl) MakeTransport(trustURLs bool) *MattermostTransport {
insecure := h.configService.Config().ServiceSettings.EnableInsecureOutgoingConnections != nil && *h.configService.Config().ServiceSettings.EnableInsecureOutgoingConnections
if trustURLs {
return NewTransport(insecure, nil, nil)
}
allowHost := func(host string) bool {
if h.configService.Config().ServiceSettings.AllowedUntrustedInternalConnections == nil {
return false
}
for _, allowed := range strings.FieldsFunc(*h.configService.Config().ServiceSettings.AllowedUntrustedInternalConnections, splitFields) {
if host == allowed {
return true
}
}
return false
}
allowIP := func(ip net.IP) bool {
reservedIP := IsReservedIP(ip)
ownIP, err := IsOwnIP(ip)
// If there is an error getting the self-assigned IPs, default to the secure option
if err != nil {
return false
}
// If it's not a reserved IP and it's not self-assigned IP, accept the IP
if !reservedIP && !ownIP {
return true
}
if h.configService.Config().ServiceSettings.AllowedUntrustedInternalConnections == nil {
return false
}
// In the case it's the self-assigned IP, enforce that it needs to be explicitly added to the AllowedUntrustedInternalConnections
for _, allowed := range strings.FieldsFunc(*h.configService.Config().ServiceSettings.AllowedUntrustedInternalConnections, splitFields) {
if _, ipRange, err := net.ParseCIDR(allowed); err == nil && ipRange.Contains(ip) {
return true
}
}
return false
}
return NewTransport(insecure, allowHost, allowIP)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package httpservice
import (
"net/http"
)
// MattermostTransport is an implementation of http.RoundTripper that ensures each request contains a custom user agent
// string to indicate that the request is coming from a Mattermost instance.
type MattermostTransport struct {
// Transport is the underlying http.RoundTripper that is actually used to make the request
Transport http.RoundTripper
}
func (t *MattermostTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("User-Agent", defaultUserAgent)
return t.Transport.RoundTrip(req)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imageproxy
import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"io"
"net/http"
"net/url"
)
type AtmosCamoBackend struct {
proxy *ImageProxy
siteURL *url.URL
remoteURL *url.URL
client *http.Client
}
func makeAtmosCamoBackend(proxy *ImageProxy) *AtmosCamoBackend {
// We deliberately ignore the error because it's from config.json.
// The function returns a nil pointer in case of error, and we handle it when it's used.
siteURL, _ := url.Parse(*proxy.ConfigService.Config().ServiceSettings.SiteURL)
remoteURL, _ := url.Parse(*proxy.ConfigService.Config().ImageProxySettings.RemoteImageProxyURL)
return &AtmosCamoBackend{
proxy: proxy,
siteURL: siteURL,
remoteURL: remoteURL,
client: proxy.HTTPService.MakeClient(false),
}
}
func (backend *AtmosCamoBackend) GetImage(w http.ResponseWriter, r *http.Request, imageURL string) {
http.Redirect(w, r, backend.getAtmosCamoImageURL(imageURL), http.StatusFound)
}
func (backend *AtmosCamoBackend) GetImageDirect(imageURL string) (io.ReadCloser, string, error) {
req, err := http.NewRequest("GET", backend.getAtmosCamoImageURL(imageURL), nil)
if err != nil {
return nil, "", Error{err}
}
resp, err := backend.client.Do(req)
if err != nil {
return nil, "", Error{err}
}
// Note that we don't do any additional validation of the received data since we expect the image proxy to do that
return resp.Body, resp.Header.Get("Content-Type"), nil
}
func (backend *AtmosCamoBackend) getAtmosCamoImageURL(imageURL string) string {
cfg := *backend.proxy.ConfigService.Config()
options := *cfg.ImageProxySettings.RemoteImageProxyOptions
if imageURL == "" || backend.siteURL == nil {
return imageURL
}
// Parse url, return siteURL in case of failure.
// Also if the URL is opaque.
parsedURL, err := url.Parse(imageURL)
if err != nil || parsedURL.Opaque != "" {
return backend.siteURL.String()
}
// If host is same as siteURL host/ remoteURL host, return.
if parsedURL.Host == backend.siteURL.Host || parsedURL.Host == backend.remoteURL.Host {
return parsedURL.String()
}
// Handle protocol-relative URLs.
if parsedURL.Scheme == "" {
parsedURL.Scheme = backend.siteURL.Scheme
}
// If it's a relative URL, fill up the hostname and scheme and return.
if parsedURL.Host == "" {
parsedURL.Host = backend.siteURL.Host
return parsedURL.String()
}
urlBytes := []byte(parsedURL.String())
mac := hmac.New(sha1.New, []byte(options))
mac.Write(urlBytes)
digest := hex.EncodeToString(mac.Sum(nil))
return backend.remoteURL.String() + "/" + digest + "/" + hex.EncodeToString(urlBytes)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imageproxy
import (
"errors"
"io"
"net/http"
"net/url"
"strings"
"sync"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/configservice"
"github.com/mattermost/mattermost-server/v6/server/platform/services/httpservice"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var ErrNotEnabled = Error{errors.New("imageproxy.ImageProxy: image proxy not enabled")}
// An ImageProxy is the public interface for Mattermost's image proxy. An instance of ImageProxy should be created
// using MakeImageProxy which requires a configService and an HTTPService provided by the server.
type ImageProxy struct {
ConfigService configservice.ConfigService
configListenerID string
HTTPService httpservice.HTTPService
Logger *mlog.Logger
siteURL *url.URL
lock sync.RWMutex
backend ImageProxyBackend
}
// An ImageProxyBackend provides the functionality for different types of image proxies. An ImageProxy will construct
// the required backend depending on the ImageProxySettings provided by the ConfigService.
type ImageProxyBackend interface {
// GetImage provides a proxied image in response to an HTTP request.
GetImage(w http.ResponseWriter, r *http.Request, imageURL string)
// GetImageDirect returns a proxied image along with its content type.
GetImageDirect(imageURL string) (io.ReadCloser, string, error)
}
func MakeImageProxy(configService configservice.ConfigService, httpService httpservice.HTTPService, logger *mlog.Logger) *ImageProxy {
proxy := &ImageProxy{
ConfigService: configService,
HTTPService: httpService,
Logger: logger,
}
// We deliberately ignore the error because it's from config.json.
// The function returns a nil pointer in case of error, and we handle it when it's used.
siteURL, _ := url.Parse(*configService.Config().ServiceSettings.SiteURL)
proxy.siteURL = siteURL
proxy.configListenerID = proxy.ConfigService.AddConfigListener(proxy.OnConfigChange)
config := proxy.ConfigService.Config()
proxy.backend = proxy.makeBackend(*config.ImageProxySettings.Enable, *config.ImageProxySettings.ImageProxyType)
return proxy
}
func (proxy *ImageProxy) makeBackend(enable bool, proxyType string) ImageProxyBackend {
if !enable {
return nil
}
switch proxyType {
case model.ImageProxyTypeLocal:
return makeLocalBackend(proxy)
case model.ImageProxyTypeAtmosCamo:
return makeAtmosCamoBackend(proxy)
default:
return nil
}
}
func (proxy *ImageProxy) Close() {
proxy.lock.Lock()
defer proxy.lock.Unlock()
proxy.ConfigService.RemoveConfigListener(proxy.configListenerID)
}
func (proxy *ImageProxy) OnConfigChange(oldConfig, newConfig *model.Config) {
if *oldConfig.ImageProxySettings.Enable != *newConfig.ImageProxySettings.Enable ||
*oldConfig.ImageProxySettings.ImageProxyType != *newConfig.ImageProxySettings.ImageProxyType {
proxy.lock.Lock()
defer proxy.lock.Unlock()
proxy.backend = proxy.makeBackend(*newConfig.ImageProxySettings.Enable, *newConfig.ImageProxySettings.ImageProxyType)
}
}
// GetImage takes an HTTP request for an image and requests that image using the image proxy.
func (proxy *ImageProxy) GetImage(w http.ResponseWriter, r *http.Request, imageURL string) {
proxy.lock.RLock()
defer proxy.lock.RUnlock()
if proxy.backend == nil {
w.WriteHeader(http.StatusNotImplemented)
return
}
proxy.backend.GetImage(w, r, imageURL)
}
// GetImageDirect takes the URL of an image and returns the image along with its content type.
func (proxy *ImageProxy) GetImageDirect(imageURL string) (io.ReadCloser, string, error) {
proxy.lock.RLock()
defer proxy.lock.RUnlock()
if proxy.backend == nil {
return nil, "", ErrNotEnabled
}
return proxy.backend.GetImageDirect(imageURL)
}
// GetProxiedImageURL takes the URL of an image and returns a URL that can be used to view that image through the
// image proxy.
func (proxy *ImageProxy) GetProxiedImageURL(imageURL string) string {
if imageURL == "" || proxy.siteURL == nil {
return imageURL
}
// Parse url, return siteURL in case of failure.
// Also if the URL is opaque.
parsedURL, err := url.Parse(imageURL)
if err != nil || parsedURL.Opaque != "" {
return proxy.siteURL.String()
}
// If host is same as siteURL host, return.
if parsedURL.Host == proxy.siteURL.Host {
return parsedURL.String()
}
// Handle protocol-relative URLs.
if parsedURL.Scheme == "" {
parsedURL.Scheme = proxy.siteURL.Scheme
}
// If it's a relative URL, fill up the hostname and return.
if parsedURL.Host == "" {
parsedURL.Host = proxy.siteURL.Host
return parsedURL.String()
}
return proxy.siteURL.String() + "/api/v4/image?url=" + url.QueryEscape(parsedURL.String())
}
// GetUnproxiedImageURL takes the URL of an image on the image proxy and returns the original URL of the image.
func (proxy *ImageProxy) GetUnproxiedImageURL(proxiedURL string) string {
return getUnproxiedImageURL(proxiedURL, *proxy.ConfigService.Config().ServiceSettings.SiteURL)
}
func getUnproxiedImageURL(proxiedURL, siteURL string) string {
if !strings.HasPrefix(proxiedURL, siteURL+"/api/v4/image?url=") {
return proxiedURL
}
parsed, err := url.Parse(proxiedURL)
if err != nil {
return proxiedURL
}
u := parsed.Query()["url"]
if len(u) == 0 {
return proxiedURL
}
return u[0]
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imageproxy
import (
"bufio"
"errors"
"fmt"
"io"
"mime"
"net"
"net/http"
"net/http/httptest"
"net/url"
"path"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var imageContentTypes = []string{
"image/bmp", "image/cgm", "image/g3fax", "image/gif", "image/ief", "image/jp2",
"image/jpeg", "image/jpg", "image/pict", "image/png", "image/prs.btif", "image/svg+xml",
"image/tiff", "image/vnd.adobe.photoshop", "image/vnd.djvu", "image/vnd.dwg",
"image/vnd.dxf", "image/vnd.fastbidsheet", "image/vnd.fpx", "image/vnd.fst",
"image/vnd.fujixerox.edmics-mmr", "image/vnd.fujixerox.edmics-rlc",
"image/vnd.microsoft.icon", "image/vnd.ms-modi", "image/vnd.net-fpx", "image/vnd.wap.wbmp",
"image/vnd.xiff", "image/webp", "image/x-cmu-raster", "image/x-cmx", "image/x-icon",
"image/x-macpaint", "image/x-pcx", "image/x-pict", "image/x-portable-anymap",
"image/x-portable-bitmap", "image/x-portable-graymap", "image/x-portable-pixmap",
"image/x-quicktime", "image/x-rgb", "image/x-xbitmap", "image/x-xpixmap", "image/x-xwindowdump",
}
var msgNotAllowed = "requested URL is not allowed"
var ErrLocalRequestFailed = Error{errors.New("imageproxy.LocalBackend: failed to request proxied image")}
type LocalBackend struct {
proxy *ImageProxy
client *http.Client
baseURL *url.URL
}
// URLError reports a malformed URL error.
type URLError struct {
Message string
URL *url.URL
}
func (e URLError) Error() string {
return fmt.Sprintf("malformed URL %q: %s", e.URL, e.Message)
}
func makeLocalBackend(proxy *ImageProxy) *LocalBackend {
baseURL, err := url.Parse(*proxy.ConfigService.Config().ServiceSettings.SiteURL)
if err != nil {
mlog.Warn("Failed to set base URL for image proxy. Relative image links may not work.", mlog.Err(err))
}
client := proxy.HTTPService.MakeClient(false)
return &LocalBackend{
proxy: proxy,
client: client,
baseURL: baseURL,
}
}
type contentTypeRecorder struct {
http.ResponseWriter
filename string
}
func (rec *contentTypeRecorder) WriteHeader(code int) {
hdr := rec.ResponseWriter.Header()
contentType := hdr.Get("Content-Type")
mediaType, _, err := mime.ParseMediaType(contentType)
// The error is caused by a malformed input and there's not much use logging it.
// Therefore, even in the error case we set it to attachment mode to be safe.
if err != nil || mediaType == "image/svg+xml" {
hdr.Set("Content-Disposition", fmt.Sprintf("attachment;filename=%q", rec.filename))
}
rec.ResponseWriter.WriteHeader(code)
}
func (backend *LocalBackend) GetImage(w http.ResponseWriter, r *http.Request, imageURL string) {
// The interface to the proxy only exposes a ServeHTTP method, so fake a request to it
req, err := http.NewRequest(http.MethodGet, "/"+imageURL, nil)
if err != nil {
// http.NewRequest should only return an error on an invalid URL
mlog.Debug("Failed to create request for proxied image", mlog.String("url", imageURL), mlog.Err(err))
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte{})
return
}
u, err := url.Parse(imageURL)
if err != nil {
mlog.Debug("Failed to parse URL for proxied image", mlog.String("url", imageURL), mlog.Err(err))
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte{})
return
}
w.Header().Set("X-Frame-Options", "deny")
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Content-Security-Policy", "default-src 'none'; img-src data:; style-src 'unsafe-inline'")
rec := contentTypeRecorder{w, filepath.Base(u.Path)}
backend.ServeImage(&rec, req)
}
func (backend *LocalBackend) GetImageDirect(imageURL string) (io.ReadCloser, string, error) {
// The interface to the proxy only exposes a ServeHTTP method, so fake a request to it
req, err := http.NewRequest(http.MethodGet, "/"+imageURL, nil)
if err != nil {
return nil, "", Error{err}
}
recorder := httptest.NewRecorder()
backend.ServeImage(recorder, req)
if recorder.Code != http.StatusOK {
return nil, "", ErrLocalRequestFailed
}
return io.NopCloser(recorder.Body), recorder.Header().Get("Content-Type"), nil
}
func (backend *LocalBackend) ServeImage(w http.ResponseWriter, req *http.Request) {
proxyReq, err := newProxyRequest(req, backend.baseURL)
if err != nil {
http.Error(w, fmt.Sprintf("invalid request URL: %v", err), http.StatusBadRequest)
return
}
actualReq, err := http.NewRequest("GET", proxyReq.String(), nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
actualReq.Header.Set("Accept", strings.Join(imageContentTypes, ", "))
resp, err := backend.client.Do(actualReq)
if err != nil {
msg := fmt.Sprintf("error fetching remote image: %v", err)
mlog.Warn(msg)
statusCode := http.StatusInternalServerError
if e, ok := err.(net.Error); ok && e.Timeout() {
statusCode = http.StatusGatewayTimeout
}
http.Error(w, msg, statusCode)
return
}
// close the original resp.Body, even if we wrap it in a NopCloser below
defer resp.Body.Close()
copyHeader(w.Header(), resp.Header, "Cache-Control", "Last-Modified", "Expires", "Etag", "Link")
if should304(req, resp) {
w.WriteHeader(http.StatusNotModified)
return
}
contentType, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if contentType == "" || contentType == "application/octet-stream" || contentType == "binary/octet-stream" {
// try to detect content type
b := bufio.NewReader(resp.Body)
resp.Body = io.NopCloser(b)
contentType = peekContentType(b)
}
if resp.ContentLength != 0 && !contentTypeMatches(imageContentTypes, contentType) {
http.Error(w, msgNotAllowed, http.StatusForbidden)
return
}
w.Header().Set("Content-Type", contentType)
copyHeader(w.Header(), resp.Header, "Content-Length")
// Enable CORS for 3rd party applications
w.Header().Set("Access-Control-Allow-Origin", "*")
// Add a Content-Security-Policy to prevent stored-XSS attacks via SVG files
w.Header().Set("Content-Security-Policy", "script-src 'none'")
// Disable Content-Type sniffing
w.Header().Set("X-Content-Type-Options", "nosniff")
// Block potential XSS attacks especially in legacy browsers which do not support CSP
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.WriteHeader(resp.StatusCode)
if _, err := io.Copy(w, resp.Body); err != nil {
mlog.Warn("error copying response", mlog.Err(err))
}
}
// copyHeader copies header values from src to dst, adding to any existing
// values with the same header name. If keys is not empty, only those header
// keys will be copied.
func copyHeader(dst, src http.Header, keys ...string) {
if len(keys) == 0 {
for k := range src {
keys = append(keys, k)
}
}
for _, key := range keys {
k := http.CanonicalHeaderKey(key)
for _, v := range src[k] {
dst.Add(k, v)
}
}
}
func should304(req *http.Request, resp *http.Response) bool {
etag := resp.Header.Get("Etag")
if etag != "" && etag == req.Header.Get("If-None-Match") {
return true
}
lastModified, err := time.Parse(time.RFC1123, resp.Header.Get("Last-Modified"))
if err != nil {
return false
}
ifModSince, err := time.Parse(time.RFC1123, req.Header.Get("If-Modified-Since"))
if err != nil {
return false
}
if lastModified.Before(ifModSince) || lastModified.Equal(ifModSince) {
return true
}
return false
}
// peekContentType peeks at the first 512 bytes of p, and attempts to detect
// the content type. Returns empty string if error occurs.
func peekContentType(p *bufio.Reader) string {
byt, err := p.Peek(512)
if err != nil && err != bufio.ErrBufferFull && err != io.EOF {
return ""
}
return http.DetectContentType(byt)
}
// contentTypeMatches returns whether contentType matches one of the allowed patterns.
func contentTypeMatches(patterns []string, contentType string) bool {
if len(patterns) == 0 {
return true
}
for _, pattern := range patterns {
if ok, err := path.Match(pattern, contentType); ok && err == nil {
return true
}
}
return false
}
// proxyRequest is an imageproxy request which includes a remote URL of an image to
// proxy.
type proxyRequest struct {
URL *url.URL // URL of the image to proxy
Original *http.Request // The original HTTP request
}
// String returns the request URL as a string, with r.Options encoded in the
// URL fragment.
func (r proxyRequest) String() string {
return r.URL.String()
}
func newProxyRequest(r *http.Request, baseURL *url.URL) (*proxyRequest, error) {
var err error
req := &proxyRequest{Original: r}
path := r.URL.EscapedPath()[1:] // strip leading slash
req.URL, err = parseURL(path)
if err != nil || !req.URL.IsAbs() {
// first segment should be options
parts := strings.SplitN(path, "/", 2)
if len(parts) != 2 {
return nil, URLError{"too few path segments", r.URL}
}
var err error
req.URL, err = parseURL(parts[1])
if err != nil {
return nil, URLError{fmt.Sprintf("unable to parse remote URL: %v", err), r.URL}
}
}
if baseURL != nil {
req.URL = baseURL.ResolveReference(req.URL)
}
if !req.URL.IsAbs() {
return nil, URLError{"must provide absolute remote URL", r.URL}
}
if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
return nil, URLError{"remote URL must have http or https scheme", r.URL}
}
// query string is always part of the remote URL
req.URL.RawQuery = r.URL.RawQuery
return req, nil
}
var reCleanedURL = regexp.MustCompile(`^(https?):/+([^/])`)
// parseURL parses s as a URL, handling URLs that have been munged by
// path.Clean or a webserver that collapses multiple slashes.
func parseURL(s string) (*url.URL, error) {
s = reCleanedURL.ReplaceAllString(s, "$1://$2")
return url.Parse(s)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package marketplace
import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/httpservice"
)
// Client is the programmatic interface to the marketplace server API.
type Client struct {
address string
httpClient *http.Client
}
// NewClient creates a client to the marketplace server at the given address.
func NewClient(address string, httpService httpservice.HTTPService) (*Client, error) {
var httpClient *http.Client
addressURL, err := url.Parse(address)
if err != nil {
return nil, errors.Wrap(err, "failed to parse marketplace address")
}
if addressURL.Hostname() == "localhost" || addressURL.Hostname() == "127.0.0.1" {
httpClient = httpService.MakeClient(true)
} else {
httpClient = httpService.MakeClient(false)
}
return &Client{
address: address,
httpClient: httpClient,
}, nil
}
// GetPlugins fetches the list of plugins from the configured server.
func (c *Client) GetPlugins(request *model.MarketplacePluginFilter) ([]*model.BaseMarketplacePlugin, error) {
u, err := url.Parse(c.buildURL("/api/v1/plugins"))
if err != nil {
return nil, err
}
request.ApplyToURL(u)
resp, err := c.doGet(u.String())
if err != nil {
return nil, err
}
defer closeBody(resp)
switch resp.StatusCode {
case http.StatusOK:
return model.BaseMarketplacePluginsFromReader(resp.Body)
default:
return nil, errors.Errorf("failed with status code %d", resp.StatusCode)
}
}
func (c *Client) GetPlugin(filter *model.MarketplacePluginFilter, pluginVersion string) (*model.BaseMarketplacePlugin, error) {
filter.ReturnAllVersions = true
if filter.PluginId == "" {
return nil, errors.New("missing pluginID")
}
if pluginVersion == "" {
return nil, errors.New("missing pluginVersion")
}
plugins, err := c.GetPlugins(filter)
if err != nil {
return nil, err
}
for _, plugin := range plugins {
if plugin.Manifest.Version == pluginVersion {
return plugin, nil
}
}
return nil, errors.New("plugin not found")
}
func (c *Client) GetLatestPlugin(filter *model.MarketplacePluginFilter) (*model.BaseMarketplacePlugin, error) {
filter.ReturnAllVersions = false
if filter.PluginId == "" {
return nil, errors.New("no pluginID provided")
}
plugins, err := c.GetPlugins(filter)
if err != nil {
return nil, err
}
if len(plugins) == 0 {
return nil, errors.New("plugin not found")
}
if len(plugins) > 1 {
return nil, errors.Errorf("unexpectedly more then one plugin was returned from the marketplace")
}
return plugins[0], nil
}
// closeBody ensures the Body of an http.Response is properly closed.
func closeBody(r *http.Response) {
if r.Body != nil {
_, _ = io.Copy(io.Discard, r.Body)
_ = r.Body.Close()
}
}
func (c *Client) buildURL(urlPath string, args ...any) string {
return fmt.Sprintf("%s/%s", strings.TrimRight(c.address, "/"), strings.TrimLeft(fmt.Sprintf(urlPath, args...), "/"))
}
func (c *Client) doGet(u string) (*http.Response, error) {
return c.httpClient.Get(u)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package remotecluster
import "fmt"
type BufferFullError struct {
capacity int
}
func NewBufferFullError(capacity int) BufferFullError {
return BufferFullError{
capacity: capacity,
}
}
func (e BufferFullError) Capacity() int {
return e.capacity
}
func (e BufferFullError) Error() string {
return fmt.Sprintf("buffer capacity (%d) exceeded", e.capacity)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package remotecluster
import (
"encoding/json"
"errors"
"fmt"
"github.com/mattermost/mattermost-server/v6/model"
)
// AcceptInvitation is called when accepting an invitation to connect with a remote cluster.
func (rcs *Service) AcceptInvitation(invite *model.RemoteClusterInvite, name string, displayName, creatorId string, teamId string, siteURL string) (*model.RemoteCluster, error) {
rc := &model.RemoteCluster{
RemoteId: invite.RemoteId,
RemoteTeamId: invite.RemoteTeamId,
Name: name,
DisplayName: displayName,
Token: model.NewId(),
RemoteToken: invite.Token,
SiteURL: invite.SiteURL,
CreatorId: creatorId,
}
rcSaved, err := rcs.server.GetStore().RemoteCluster().Save(rc)
if err != nil {
return nil, err
}
// confirm the invitation with the originating site
frame, err := makeConfirmFrame(rcSaved, teamId, siteURL)
if err != nil {
return nil, err
}
url := fmt.Sprintf("%s/%s", rcSaved.SiteURL, ConfirmInviteURL)
resp, err := rcs.sendFrameToRemote(PingTimeout, rc, frame, url)
if err != nil {
rcs.server.GetStore().RemoteCluster().Delete(rcSaved.RemoteId)
return nil, err
}
var response Response
err = json.Unmarshal(resp, &response)
if err != nil {
rcs.server.GetStore().RemoteCluster().Delete(rcSaved.RemoteId)
return nil, fmt.Errorf("invalid response from remote server: %w", err)
}
if !response.IsSuccess() {
rcs.server.GetStore().RemoteCluster().Delete(rcSaved.RemoteId)
return nil, errors.New(response.Err)
}
// issue the first ping right away. The goroutine will exit when ping completes or PingTimeout exceeded.
go rcs.pingRemote(rcSaved)
return rcSaved, nil
}
func makeConfirmFrame(rc *model.RemoteCluster, teamId string, siteURL string) (*model.RemoteClusterFrame, error) {
confirm := model.RemoteClusterInvite{
RemoteId: rc.RemoteId,
RemoteTeamId: teamId,
SiteURL: siteURL,
Token: rc.Token,
}
confirmRaw, err := json.Marshal(confirm)
if err != nil {
return nil, err
}
msg := model.NewRemoteClusterMsg(InvitationTopic, confirmRaw)
frame := &model.RemoteClusterFrame{
RemoteId: rc.RemoteId,
Msg: msg,
}
return frame, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package remotecluster
import (
"encoding/json"
"fmt"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// pingLoop periodically sends a ping to all remote clusters.
func (rcs *Service) pingLoop(done <-chan struct{}) {
pingChan := make(chan *model.RemoteCluster, MaxConcurrentSends*2)
// create a thread pool to send pings concurrently to remotes.
for i := 0; i < MaxConcurrentSends; i++ {
go rcs.pingEmitter(pingChan, done)
}
go rcs.pingGenerator(pingChan, done)
}
func (rcs *Service) pingGenerator(pingChan chan *model.RemoteCluster, done <-chan struct{}) {
defer close(pingChan)
for {
start := time.Now()
// get all remotes, including any previously offline.
remotes, err := rcs.server.GetStore().RemoteCluster().GetAll(model.RemoteClusterQueryFilter{})
if err != nil {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceError, "Ping remote cluster failed (could not get list of remotes)", mlog.Err(err))
select {
case <-time.After(PingFreq):
continue
case <-done:
return
}
}
for _, rc := range remotes {
if rc.SiteURL != "" { // filter out unconfirmed invites
pingChan <- rc
}
}
// try to maintain frequency
elapsed := time.Since(start)
if elapsed < PingFreq {
sleep := time.Until(start.Add(PingFreq))
select {
case <-time.After(sleep):
case <-done:
return
}
}
}
}
// pingEmitter pulls Remotes from the ping queue (pingChan) and pings them.
// Pinging a remote cannot take longer than PingTimeoutMillis.
func (rcs *Service) pingEmitter(pingChan <-chan *model.RemoteCluster, done <-chan struct{}) {
for {
select {
case rc := <-pingChan:
if rc == nil {
return
}
online := rc.IsOnline()
if err := rcs.pingRemote(rc); err != nil {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceWarn, "Remote cluster ping failed",
mlog.String("remote", rc.DisplayName),
mlog.String("remoteId", rc.RemoteId),
mlog.Err(err),
)
}
if online != rc.IsOnline() {
if metrics := rcs.server.GetMetrics(); metrics != nil {
metrics.IncrementRemoteClusterConnStateChangeCounter(rc.RemoteId, rc.IsOnline())
}
rcs.fireConnectionStateChgEvent(rc)
}
case <-done:
return
}
}
}
// pingRemote make a synchronous ping to a remote cluster. Return is error if ping is
// unsuccessful and nil on success.
func (rcs *Service) pingRemote(rc *model.RemoteCluster) error {
frame, err := makePingFrame(rc)
if err != nil {
return err
}
url := fmt.Sprintf("%s/%s", rc.SiteURL, PingURL)
resp, err := rcs.sendFrameToRemote(PingTimeout, rc, frame, url)
if err != nil {
return err
}
ping := model.RemoteClusterPing{}
err = json.Unmarshal(resp, &ping)
if err != nil {
return err
}
if err := rcs.server.GetStore().RemoteCluster().SetLastPingAt(rc.RemoteId); err != nil {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceError, "Failed to update LastPingAt for remote cluster",
mlog.String("remote", rc.DisplayName),
mlog.String("remoteId", rc.RemoteId),
mlog.Err(err),
)
}
rc.LastPingAt = model.GetMillis()
if metrics := rcs.server.GetMetrics(); metrics != nil {
sentAt := time.Unix(0, ping.SentAt*int64(time.Millisecond))
elapsed := time.Since(sentAt).Seconds()
metrics.ObserveRemoteClusterPingDuration(rc.RemoteId, elapsed)
// we approximate clock skew between remotes.
skew := elapsed/2 - float64(ping.RecvAt-ping.SentAt)/1000
metrics.ObserveRemoteClusterClockSkew(rc.RemoteId, skew)
}
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceDebug, "Remote cluster ping",
mlog.String("remote", rc.DisplayName),
mlog.String("remoteId", rc.RemoteId),
mlog.Int64("SentAt", ping.SentAt),
mlog.Int64("RecvAt", ping.RecvAt),
mlog.Int64("Diff", ping.RecvAt-ping.SentAt),
)
return nil
}
func makePingFrame(rc *model.RemoteCluster) (*model.RemoteClusterFrame, error) {
ping := model.RemoteClusterPing{
SentAt: model.GetMillis(),
}
pingRaw, err := json.Marshal(ping)
if err != nil {
return nil, err
}
msg := model.NewRemoteClusterMsg(PingTopic, pingRaw)
frame := &model.RemoteClusterFrame{
RemoteId: rc.RemoteId,
Msg: msg,
}
return frame, nil
}
func (rcs *Service) fireConnectionStateChgEvent(rc *model.RemoteCluster) {
rcs.mux.RLock()
listeners := make([]ConnectionStateListener, 0, len(rcs.connectionStateListeners))
for _, l := range rcs.connectionStateListeners {
listeners = append(listeners, l)
}
rcs.mux.RUnlock()
for _, l := range listeners {
l(rc, rc.IsOnline())
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package remotecluster
import (
"fmt"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// ReceiveIncomingMsg is called by the Rest API layer, or websocket layer (future), when a Remote Cluster
// message is received. Here we route the message to any topic listeners.
// `rc` and `msg` cannot be nil.
func (rcs *Service) ReceiveIncomingMsg(rc *model.RemoteCluster, msg model.RemoteClusterMsg) Response {
rcs.mux.RLock()
defer rcs.mux.RUnlock()
if metrics := rcs.server.GetMetrics(); metrics != nil {
metrics.IncrementRemoteClusterMsgReceivedCounter(rc.RemoteId)
}
rcSanitized := *rc
rcSanitized.Token = ""
rcSanitized.RemoteToken = ""
var response Response
response.Status = ResponseStatusOK
listeners := rcs.getTopicListeners(msg.Topic)
for _, l := range listeners {
if err := callback(l, msg, &rcSanitized, &response); err != nil {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceError, "Error from remote cluster message listener",
mlog.String("msgId", msg.Id), mlog.String("topic", msg.Topic), mlog.String("remote", rc.DisplayName), mlog.Err(err))
response.Status = ResponseStatusFail
response.Err = err.Error()
}
}
return response
}
func callback(listener TopicListener, msg model.RemoteClusterMsg, rc *model.RemoteCluster, resp *Response) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%v", r)
}
}()
err = listener(msg, rc, resp)
return
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package remotecluster
import (
"encoding/json"
)
// Response represents the bytes replied from a remote server when a message is sent.
type Response struct {
Status string `json:"status"`
Err string `json:"err"`
Payload json.RawMessage `json:"payload"`
}
// IsSuccess returns true if the response status indicates success.
func (r *Response) IsSuccess() bool {
return r.Status == ResponseStatusOK
}
// SetPayload serializes an arbitrary struct as a RawMessage.
func (r *Response) SetPayload(v any) error {
raw, err := json.Marshal(v)
if err != nil {
return err
}
r.Payload = raw
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package remotecluster
import (
"context"
"hash/fnv"
)
// enqueueTask adds a task to one of the send channels based on remoteId.
//
// There are a number of send channels (`MaxConcurrentSends`) to allow for sending to multiple
// remotes concurrently, while preserving message order for each remote.
func (rcs *Service) enqueueTask(ctx context.Context, remoteId string, task any) error {
if ctx == nil {
ctx = context.Background()
}
h := hash(remoteId)
idx := h % uint32(len(rcs.send))
select {
case rcs.send[idx] <- task:
return nil
case <-ctx.Done():
return NewBufferFullError(cap(rcs.send))
}
}
func hash(s string) uint32 {
h := fnv.New32a()
h.Write([]byte(s))
return h.Sum32()
}
// sendLoop is called by each goroutine created for the send pool and waits for sendTask's until the
// done channel is signalled.
//
// Each goroutine in the pool is assigned a specific channel, and tasks are placed in the
// channel corresponding to the remoteId.
func (rcs *Service) sendLoop(idx int, done chan struct{}) {
for {
select {
case t := <-rcs.send[idx]:
switch task := t.(type) {
case sendMsgTask:
rcs.sendMsg(task)
case sendFileTask:
rcs.sendFile(task)
case sendProfileImageTask:
rcs.sendProfileImage(task)
}
case <-done:
return
}
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package remotecluster
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SendFileResultFunc func(us *model.UploadSession, rc *model.RemoteCluster, resp *Response, err error)
type sendFileTask struct {
rc *model.RemoteCluster
us *model.UploadSession
fi *model.FileInfo
rp ReaderProvider
f SendFileResultFunc
}
type ReaderProvider interface {
FileReader(path string) (filestore.ReadCloseSeeker, *model.AppError)
}
// SendFile asynchronously sends a file to a remote cluster.
//
// `ctx` determines behaviour when the outbound queue is full. A timeout or deadline context will return a
// BufferFullError if the task cannot be enqueued before the timeout. A background context will block indefinitely.
//
// Nil or error return indicates success or failure of task enqueue only.
//
// An optional callback can be provided that receives the response from the remote cluster. The `err` provided to the
// callback is regarding file delivery only. The `resp` contains the decoded bytes returned from the remote.
// If a callback is provided it should return quickly.
func (rcs *Service) SendFile(ctx context.Context, us *model.UploadSession, fi *model.FileInfo, rc *model.RemoteCluster, rp ReaderProvider, f SendFileResultFunc) error {
task := sendFileTask{
rc: rc,
us: us,
fi: fi,
rp: rp,
f: f,
}
return rcs.enqueueTask(ctx, rc.RemoteId, task)
}
// sendFile is called when a sendFileTask is popped from the send channel.
func (rcs *Service) sendFile(task sendFileTask) {
fi, err := rcs.sendFileToRemote(SendTimeout, task)
var response Response
if err != nil {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceError, "Remote Cluster send file failed",
mlog.String("remote", task.rc.DisplayName),
mlog.String("uploadId", task.us.Id),
mlog.Err(err),
)
response.Status = ResponseStatusFail
response.Err = err.Error()
} else {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceDebug, "Remote Cluster file sent successfully",
mlog.String("remote", task.rc.DisplayName),
mlog.String("uploadId", task.us.Id),
)
response.Status = ResponseStatusOK
response.SetPayload(fi)
}
// If callback provided then call it with the results.
if task.f != nil {
task.f(task.us, task.rc, &response, err)
}
}
func (rcs *Service) sendFileToRemote(timeout time.Duration, task sendFileTask) (*model.FileInfo, error) {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceDebug, "sending file to remote...",
mlog.String("remote", task.rc.DisplayName),
mlog.String("uploadId", task.us.Id),
mlog.String("file_path", task.us.Path),
)
r, appErr := task.rp.FileReader(task.fi.Path) // get Reader for the file
if appErr != nil {
return nil, fmt.Errorf("error opening file while sending file to remote %s: %w", task.rc.RemoteId, appErr)
}
defer r.Close()
u, err := url.Parse(task.rc.SiteURL)
if err != nil {
return nil, fmt.Errorf("invalid siteURL while sending file to remote %s: %w", task.rc.RemoteId, err)
}
u.Path = path.Join(u.Path, model.APIURLSuffix, "remotecluster", "upload", task.us.Id)
req, err := http.NewRequest("POST", u.String(), r)
if err != nil {
return nil, err
}
req.Header.Set(model.HeaderRemoteclusterId, task.rc.RemoteId)
req.Header.Set(model.HeaderRemoteclusterToken, task.rc.RemoteToken)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
resp, err := rcs.httpClient.Do(req.WithContext(ctx))
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected response: %d - %s", resp.StatusCode, resp.Status)
}
// body should be a FileInfo
var fi model.FileInfo
if err := json.Unmarshal(body, &fi); err != nil {
return nil, fmt.Errorf("unexpected response body: %w", err)
}
return &fi, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package remotecluster
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"time"
"github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SendMsgResultFunc func(msg model.RemoteClusterMsg, rc *model.RemoteCluster, resp *Response, err error)
type sendMsgTask struct {
rc *model.RemoteCluster
msg model.RemoteClusterMsg
f SendMsgResultFunc
}
// BroadcastMsg asynchronously sends a message to all remote clusters interested in the message's topic.
//
// `ctx` determines behaviour when the outbound queue is full. A timeout or deadline context will return a
// BufferFullError if the message cannot be enqueued before the timeout. A background context will block indefinitely.
//
// An optional callback can be provided that receives the success or fail result of sending to each remote cluster.
// Success or fail is regarding message delivery only. If a callback is provided it should return quickly.
func (rcs *Service) BroadcastMsg(ctx context.Context, msg model.RemoteClusterMsg, f SendMsgResultFunc) error {
// get list of interested remotes.
filter := model.RemoteClusterQueryFilter{
Topic: msg.Topic,
}
list, err := rcs.server.GetStore().RemoteCluster().GetAll(filter)
if err != nil {
return err
}
errs := merror.New()
for _, rc := range list {
if err := rcs.SendMsg(ctx, msg, rc, f); err != nil {
errs.Append(err)
}
}
return errs.ErrorOrNil()
}
// SendMsg asynchronously sends a message to a remote cluster.
//
// `ctx` determines behaviour when the outbound queue is full. A timeout or deadline context will return a
// BufferFullError if the message cannot be enqueued before the timeout. A background context will block indefinitely.
//
// Nil or error return indicates success or failure of message enqueue only.
//
// An optional callback can be provided that receives the response from the remote cluster. The `err` provided to the
// callback is regarding response decoding only. The `resp` contains the decoded bytes returned from the remote.
// If a callback is provided it should return quickly.
func (rcs *Service) SendMsg(ctx context.Context, msg model.RemoteClusterMsg, rc *model.RemoteCluster, f SendMsgResultFunc) error {
task := sendMsgTask{
rc: rc,
msg: msg,
f: f,
}
return rcs.enqueueTask(ctx, rc.RemoteId, task)
}
// sendMsg is called when a sendMsgTask is popped from the send channel.
func (rcs *Service) sendMsg(task sendMsgTask) {
var errResp error
var response Response
// Ensure a panic from the callback does not exit the pool goroutine.
defer func() {
if errResp != nil {
response.Err = errResp.Error()
}
// If callback provided then call it with the results.
if task.f != nil {
task.f(task.msg, task.rc, &response, errResp)
}
}()
frame := &model.RemoteClusterFrame{
RemoteId: task.rc.RemoteId,
Msg: task.msg,
}
u, err := url.Parse(task.rc.SiteURL)
if err != nil {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceError, "Invalid siteURL while sending message to remote",
mlog.String("remote", task.rc.DisplayName),
mlog.String("msgId", task.msg.Id),
mlog.Err(err),
)
errResp = err
return
}
u.Path = path.Join(u.Path, SendMsgURL)
respJSON, err := rcs.sendFrameToRemote(SendTimeout, task.rc, frame, u.String())
if err != nil {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceError, "Remote Cluster send message failed",
mlog.String("remote", task.rc.DisplayName),
mlog.String("msgId", task.msg.Id),
mlog.Err(err),
)
errResp = err
} else {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceDebug, "Remote Cluster message sent successfully",
mlog.String("remote", task.rc.DisplayName),
mlog.String("msgId", task.msg.Id),
)
if err = json.Unmarshal(respJSON, &response); err != nil {
rcs.server.Log().Error("Invalid response sending message to remote cluster",
mlog.String("remote", task.rc.DisplayName),
mlog.Err(err),
)
errResp = err
}
}
}
func (rcs *Service) sendFrameToRemote(timeout time.Duration, rc *model.RemoteCluster, frame *model.RemoteClusterFrame, url string) ([]byte, error) {
body, err := json.Marshal(frame)
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set(model.HeaderRemoteclusterId, rc.RemoteId)
req.Header.Set(model.HeaderRemoteclusterToken, rc.RemoteToken)
resp, err := rcs.httpClient.Do(req.WithContext(ctx))
if metrics := rcs.server.GetMetrics(); metrics != nil {
if err != nil || resp.StatusCode != http.StatusOK {
metrics.IncrementRemoteClusterMsgErrorsCounter(frame.RemoteId, os.IsTimeout(err))
} else {
metrics.IncrementRemoteClusterMsgSentCounter(frame.RemoteId)
}
}
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err = io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return body, fmt.Errorf("unexpected response: %d - %s", resp.StatusCode, resp.Status)
}
return body, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package remotecluster
import (
"bytes"
"context"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"path"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type SendProfileImageResultFunc func(userId string, rc *model.RemoteCluster, resp *Response, err error)
type sendProfileImageTask struct {
rc *model.RemoteCluster
userID string
provider ProfileImageProvider
f SendProfileImageResultFunc
}
type ProfileImageProvider interface {
GetProfileImage(user *model.User) ([]byte, bool, *model.AppError)
}
// SendProfileImage asynchronously sends a user's profile image to a remote cluster.
//
// `ctx` determines behaviour when the outbound queue is full. A timeout or deadline context will return a
// BufferFullError if the task cannot be enqueued before the timeout. A background context will block indefinitely.
//
// Nil or error return indicates success or failure of task enqueue only.
//
// An optional callback can be provided that receives the response from the remote cluster. The `err` provided to the
// callback is regarding image delivery only. The `resp` contains the decoded bytes returned from the remote.
// If a callback is provided it should return quickly.
func (rcs *Service) SendProfileImage(ctx context.Context, userID string, rc *model.RemoteCluster, provider ProfileImageProvider, f SendProfileImageResultFunc) error {
task := sendProfileImageTask{
rc: rc,
userID: userID,
provider: provider,
f: f,
}
return rcs.enqueueTask(ctx, rc.RemoteId, task)
}
// sendProfileImage is called when a sendProfileImageTask is popped from the send channel.
func (rcs *Service) sendProfileImage(task sendProfileImageTask) {
err := rcs.sendProfileImageToRemote(SendTimeout, task)
var response Response
if err != nil {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceError, "Remote Cluster send profile image failed",
mlog.String("remote", task.rc.DisplayName),
mlog.String("UserId", task.userID),
mlog.Err(err),
)
response.Status = ResponseStatusFail
response.Err = err.Error()
} else {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceDebug, "Remote Cluster profile image sent successfully",
mlog.String("remote", task.rc.DisplayName),
mlog.String("UserId", task.userID),
)
response.Status = ResponseStatusOK
}
// If callback provided then call it with the results.
if task.f != nil {
task.f(task.userID, task.rc, &response, err)
}
}
func (rcs *Service) sendProfileImageToRemote(timeout time.Duration, task sendProfileImageTask) error {
rcs.server.Log().Log(mlog.LvlRemoteClusterServiceDebug, "sending profile image to remote...",
mlog.String("remote", task.rc.DisplayName),
mlog.String("UserId", task.userID),
)
user, err := rcs.server.GetStore().User().Get(context.Background(), task.userID)
if err != nil {
return fmt.Errorf("error fetching user while sending profile image to remote %s: %w", task.rc.RemoteId, err)
}
img, _, appErr := task.provider.GetProfileImage(user) // get Reader for the file
if appErr != nil {
return fmt.Errorf("error fetching profile image for user (%s) while sending to remote %s: %w", task.userID, task.rc.RemoteId, appErr)
}
u, err := url.Parse(task.rc.SiteURL)
if err != nil {
return fmt.Errorf("invalid siteURL while sending file to remote %s: %w", task.rc.RemoteId, err)
}
u.Path = path.Join(u.Path, model.APIURLSuffix, "remotecluster", task.userID, "image")
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("image", "profile.png")
if err != nil {
return err
}
if _, err = io.Copy(part, bytes.NewBuffer(img)); err != nil {
return err
}
if err = writer.Close(); err != nil {
return err
}
req, err := http.NewRequest("POST", u.String(), body)
if err != nil {
return err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set(model.HeaderRemoteclusterId, task.rc.RemoteId)
req.Header.Set(model.HeaderRemoteclusterToken, task.rc.RemoteToken)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
resp, err := rcs.httpClient.Do(req.WithContext(ctx))
if err != nil {
return err
}
defer resp.Body.Close()
_, err = io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected response: %d - %s", resp.StatusCode, resp.Status)
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package remotecluster
import (
"context"
"net"
"net/http"
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
SendChanBuffer = 50
RecvChanBuffer = 50
ResultsChanBuffer = 50
ResultQueueDrainTimeoutMillis = 10000
MaxConcurrentSends = 10
SendMsgURL = "api/v4/remotecluster/msg"
SendTimeout = time.Minute
SendFileTimeout = time.Minute * 5
PingURL = "api/v4/remotecluster/ping"
PingFreq = time.Minute
PingTimeout = time.Second * 15
ConfirmInviteURL = "api/v4/remotecluster/confirm_invite"
InvitationTopic = "invitation"
PingTopic = "ping"
ResponseStatusOK = model.StatusOk
ResponseStatusFail = model.StatusFail
InviteExpiresAfter = time.Hour * 48
)
var (
disablePing bool // override for testing
)
type ServerIface interface {
Config() *model.Config
IsLeader() bool
AddClusterLeaderChangedListener(listener func()) string
RemoveClusterLeaderChangedListener(id string)
GetStore() store.Store
Log() *mlog.Logger
GetMetrics() einterfaces.MetricsInterface
}
// RemoteClusterServiceIFace is used to allow mocking where a remote cluster service is used (for testing).
// Unfortunately it lives here because the shared channel service, app layer, and server interface all need it.
// Putting it in app layer means shared channel service must import app package.
type RemoteClusterServiceIFace interface {
Shutdown() error
Start() error
Active() bool
AddTopicListener(topic string, listener TopicListener) string
RemoveTopicListener(listenerId string)
AddConnectionStateListener(listener ConnectionStateListener) string
RemoveConnectionStateListener(listenerId string)
SendMsg(ctx context.Context, msg model.RemoteClusterMsg, rc *model.RemoteCluster, f SendMsgResultFunc) error
SendFile(ctx context.Context, us *model.UploadSession, fi *model.FileInfo, rc *model.RemoteCluster, rp ReaderProvider, f SendFileResultFunc) error
SendProfileImage(ctx context.Context, userID string, rc *model.RemoteCluster, provider ProfileImageProvider, f SendProfileImageResultFunc) error
AcceptInvitation(invite *model.RemoteClusterInvite, name string, displayName string, creatorId string, teamId string, siteURL string) (*model.RemoteCluster, error)
ReceiveIncomingMsg(rc *model.RemoteCluster, msg model.RemoteClusterMsg) Response
}
// TopicListener is a callback signature used to listen for incoming messages for
// a specific topic.
type TopicListener func(msg model.RemoteClusterMsg, rc *model.RemoteCluster, resp *Response) error
// ConnectionStateListener is used to listen to remote cluster connection state changes.
type ConnectionStateListener func(rc *model.RemoteCluster, online bool)
// Service provides inter-cluster communication via topic based messages. In product these are called "Secured Connections".
type Service struct {
server ServerIface
httpClient *http.Client
send []chan any
// everything below guarded by `mux`
mux sync.RWMutex
active bool
leaderListenerId string
topicListeners map[string]map[string]TopicListener // maps topic id to a map of listenerid->listener
connectionStateListeners map[string]ConnectionStateListener // maps listener id to listener
done chan struct{}
}
// NewRemoteClusterService creates a RemoteClusterService instance. In product this is called a "Secured Connection".
func NewRemoteClusterService(server ServerIface) (*Service, error) {
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 200,
MaxIdleConnsPerHost: 2,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DisableCompression: false,
}
client := &http.Client{
Transport: transport,
Timeout: SendTimeout,
}
service := &Service{
server: server,
httpClient: client,
topicListeners: make(map[string]map[string]TopicListener),
connectionStateListeners: make(map[string]ConnectionStateListener),
}
service.send = make([]chan any, MaxConcurrentSends)
for i := range service.send {
service.send[i] = make(chan any, SendChanBuffer)
}
return service, nil
}
// Start is called by the server on server start-up.
func (rcs *Service) Start() error {
rcs.mux.Lock()
rcs.leaderListenerId = rcs.server.AddClusterLeaderChangedListener(rcs.onClusterLeaderChange)
rcs.mux.Unlock()
rcs.onClusterLeaderChange()
return nil
}
// Shutdown is called by the server on server shutdown.
func (rcs *Service) Shutdown() error {
rcs.server.RemoveClusterLeaderChangedListener(rcs.leaderListenerId)
rcs.pause()
return nil
}
// Active returns true if this instance of the remote cluster service is active.
// The active instance is responsible for pinging and sending messages to remotes.
func (rcs *Service) Active() bool {
rcs.mux.Lock()
defer rcs.mux.Unlock()
return rcs.active
}
// AddTopicListener registers a callback
func (rcs *Service) AddTopicListener(topic string, listener TopicListener) string {
rcs.mux.Lock()
defer rcs.mux.Unlock()
id := model.NewId()
listeners, ok := rcs.topicListeners[topic]
if !ok || listeners == nil {
rcs.topicListeners[topic] = make(map[string]TopicListener)
}
rcs.topicListeners[topic][id] = listener
return id
}
func (rcs *Service) RemoveTopicListener(listenerId string) {
rcs.mux.Lock()
defer rcs.mux.Unlock()
for topic, listeners := range rcs.topicListeners {
if _, ok := listeners[listenerId]; ok {
delete(listeners, listenerId)
if len(listeners) == 0 {
delete(rcs.topicListeners, topic)
}
break
}
}
}
func (rcs *Service) getTopicListeners(topic string) []TopicListener {
rcs.mux.RLock()
defer rcs.mux.RUnlock()
listeners, ok := rcs.topicListeners[topic]
if !ok {
return nil
}
listenersCopy := make([]TopicListener, 0, len(listeners))
for _, l := range listeners {
listenersCopy = append(listenersCopy, l)
}
return listenersCopy
}
func (rcs *Service) AddConnectionStateListener(listener ConnectionStateListener) string {
id := model.NewId()
rcs.mux.Lock()
defer rcs.mux.Unlock()
rcs.connectionStateListeners[id] = listener
return id
}
func (rcs *Service) RemoveConnectionStateListener(listenerId string) {
rcs.mux.Lock()
defer rcs.mux.Unlock()
delete(rcs.connectionStateListeners, listenerId)
}
// onClusterLeaderChange is called whenever the cluster leader may have changed.
func (rcs *Service) onClusterLeaderChange() {
if rcs.server.IsLeader() {
rcs.resume()
} else {
rcs.pause()
}
}
func (rcs *Service) resume() {
rcs.mux.Lock()
defer rcs.mux.Unlock()
if rcs.active {
return // already active
}
rcs.active = true
rcs.done = make(chan struct{})
if !disablePing {
rcs.pingLoop(rcs.done)
}
// create thread pool for concurrent message sending.
for i := range rcs.send {
go rcs.sendLoop(i, rcs.done)
}
rcs.server.Log().Debug("Remote Cluster Service active")
}
func (rcs *Service) pause() {
rcs.mux.Lock()
defer rcs.mux.Unlock()
if !rcs.active {
return // already inactive
}
rcs.active = false
close(rcs.done)
rcs.done = nil
rcs.server.Log().Debug("Remote Cluster Service inactive")
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package bleveengine
import (
"net/http"
"os"
"path/filepath"
"reflect"
"sync"
"sync/atomic"
"time"
"github.com/blevesearch/bleve/v2"
"github.com/blevesearch/bleve/v2/analysis/analyzer/keyword"
"github.com/blevesearch/bleve/v2/analysis/analyzer/standard"
"github.com/blevesearch/bleve/v2/mapping"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
EngineName = "bleve"
PostIndex = "posts"
FileIndex = "files"
UserIndex = "users"
ChannelIndex = "channels"
)
type BleveEngine struct {
PostIndex bleve.Index
FileIndex bleve.Index
UserIndex bleve.Index
ChannelIndex bleve.Index
Mutex sync.RWMutex
ready int32
cfg *model.Config
indexSync bool
}
var keywordMapping *mapping.FieldMapping
var standardMapping *mapping.FieldMapping
var dateMapping *mapping.FieldMapping
func init() {
keywordMapping = bleve.NewTextFieldMapping()
keywordMapping.Analyzer = keyword.Name
standardMapping = bleve.NewTextFieldMapping()
standardMapping.Analyzer = standard.Name
dateMapping = bleve.NewNumericFieldMapping()
}
func getChannelIndexMapping() *mapping.IndexMappingImpl {
channelMapping := bleve.NewDocumentMapping()
channelMapping.AddFieldMappingsAt("Id", keywordMapping)
channelMapping.AddFieldMappingsAt("Type", keywordMapping)
channelMapping.AddFieldMappingsAt("TeamId", keywordMapping)
channelMapping.AddFieldMappingsAt("NameSuggest", keywordMapping)
channelMapping.AddFieldMappingsAt("UserIDs", keywordMapping)
channelMapping.AddFieldMappingsAt("TeamMemberIDs", keywordMapping)
indexMapping := bleve.NewIndexMapping()
indexMapping.AddDocumentMapping("_default", channelMapping)
return indexMapping
}
func getPostIndexMapping() *mapping.IndexMappingImpl {
postMapping := bleve.NewDocumentMapping()
postMapping.AddFieldMappingsAt("Id", keywordMapping)
postMapping.AddFieldMappingsAt("TeamId", keywordMapping)
postMapping.AddFieldMappingsAt("ChannelId", keywordMapping)
postMapping.AddFieldMappingsAt("UserId", keywordMapping)
postMapping.AddFieldMappingsAt("CreateAt", dateMapping)
postMapping.AddFieldMappingsAt("Message", standardMapping)
postMapping.AddFieldMappingsAt("Type", keywordMapping)
postMapping.AddFieldMappingsAt("Hashtags", standardMapping)
postMapping.AddFieldMappingsAt("Attachments", standardMapping)
indexMapping := bleve.NewIndexMapping()
indexMapping.AddDocumentMapping("_default", postMapping)
return indexMapping
}
func getFileIndexMapping() *mapping.IndexMappingImpl {
fileMapping := bleve.NewDocumentMapping()
fileMapping.AddFieldMappingsAt("Id", keywordMapping)
fileMapping.AddFieldMappingsAt("CreatorId", keywordMapping)
fileMapping.AddFieldMappingsAt("ChannelId", keywordMapping)
fileMapping.AddFieldMappingsAt("CreateAt", dateMapping)
fileMapping.AddFieldMappingsAt("Name", standardMapping)
fileMapping.AddFieldMappingsAt("Content", standardMapping)
fileMapping.AddFieldMappingsAt("Extension", keywordMapping)
fileMapping.AddFieldMappingsAt("Content", standardMapping)
indexMapping := bleve.NewIndexMapping()
indexMapping.AddDocumentMapping("_default", fileMapping)
return indexMapping
}
func getUserIndexMapping() *mapping.IndexMappingImpl {
userMapping := bleve.NewDocumentMapping()
userMapping.AddFieldMappingsAt("Id", keywordMapping)
userMapping.AddFieldMappingsAt("SuggestionsWithFullname", keywordMapping)
userMapping.AddFieldMappingsAt("SuggestionsWithoutFullname", keywordMapping)
userMapping.AddFieldMappingsAt("TeamsIds", keywordMapping)
userMapping.AddFieldMappingsAt("ChannelsIds", keywordMapping)
indexMapping := bleve.NewIndexMapping()
indexMapping.AddDocumentMapping("_default", userMapping)
return indexMapping
}
func NewBleveEngine(cfg *model.Config) *BleveEngine {
return &BleveEngine{
cfg: cfg,
}
}
func (b *BleveEngine) getIndexDir(indexName string) string {
return filepath.Join(*b.cfg.BleveSettings.IndexDir, indexName+".bleve")
}
func (b *BleveEngine) createOrOpenIndex(indexName string, mapping *mapping.IndexMappingImpl) (bleve.Index, error) {
indexPath := b.getIndexDir(indexName)
if index, err := bleve.Open(indexPath); err == nil {
return index, nil
}
index, err := bleve.NewUsing(indexPath, mapping, "scorch", "scorch", map[string]any{
"forceSegmentType": "zap",
"forceSegmentVersion": 15,
})
if err != nil {
return nil, err
}
return index, nil
}
func (b *BleveEngine) openIndexes() *model.AppError {
if atomic.LoadInt32(&b.ready) != 0 {
return model.NewAppError("Bleveengine.Start", "bleveengine.already_started.error", nil, "", http.StatusInternalServerError)
}
var err error
b.PostIndex, err = b.createOrOpenIndex(PostIndex, getPostIndexMapping())
if err != nil {
return model.NewAppError("Bleveengine.Start", "bleveengine.create_post_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
b.FileIndex, err = b.createOrOpenIndex(FileIndex, getFileIndexMapping())
if err != nil {
return model.NewAppError("Bleveengine.Start", "bleveengine.create_file_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
b.UserIndex, err = b.createOrOpenIndex(UserIndex, getUserIndexMapping())
if err != nil {
return model.NewAppError("Bleveengine.Start", "bleveengine.create_user_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
b.ChannelIndex, err = b.createOrOpenIndex(ChannelIndex, getChannelIndexMapping())
if err != nil {
return model.NewAppError("Bleveengine.Start", "bleveengine.create_channel_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
atomic.StoreInt32(&b.ready, 1)
return nil
}
func (b *BleveEngine) Start() *model.AppError {
if !*b.cfg.BleveSettings.EnableIndexing || *b.cfg.BleveSettings.IndexDir == "" {
return nil
}
b.Mutex.Lock()
defer b.Mutex.Unlock()
mlog.Info("EXPERIMENTAL: Starting Bleve")
return b.openIndexes()
}
func (b *BleveEngine) closeIndexes() *model.AppError {
if b.IsActive() {
if err := b.PostIndex.Close(); err != nil {
return model.NewAppError("Bleveengine.Stop", "bleveengine.stop_post_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := b.FileIndex.Close(); err != nil {
return model.NewAppError("Bleveengine.Stop", "bleveengine.stop_file_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := b.UserIndex.Close(); err != nil {
return model.NewAppError("Bleveengine.Stop", "bleveengine.stop_user_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := b.ChannelIndex.Close(); err != nil {
return model.NewAppError("Bleveengine.Stop", "bleveengine.stop_channel_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
atomic.StoreInt32(&b.ready, 0)
return nil
}
func (b *BleveEngine) Stop() *model.AppError {
b.Mutex.Lock()
defer b.Mutex.Unlock()
mlog.Info("Stopping Bleve")
return b.closeIndexes()
}
func (b *BleveEngine) IsActive() bool {
return atomic.LoadInt32(&b.ready) == 1
}
func (b *BleveEngine) IsIndexingSync() bool {
return b.indexSync
}
func (b *BleveEngine) RefreshIndexes() *model.AppError {
return nil
}
func (b *BleveEngine) GetVersion() int {
return 0
}
func (b *BleveEngine) GetFullVersion() string {
return "0"
}
func (b *BleveEngine) GetPlugins() []string {
return []string{}
}
func (b *BleveEngine) GetName() string {
return EngineName
}
func (b *BleveEngine) TestConfig(cfg *model.Config) *model.AppError {
return nil
}
func (b *BleveEngine) deleteIndexes() *model.AppError {
if err := os.RemoveAll(b.getIndexDir(PostIndex)); err != nil {
return model.NewAppError("Bleveengine.PurgeIndexes", "bleveengine.purge_post_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := os.RemoveAll(b.getIndexDir(UserIndex)); err != nil {
return model.NewAppError("Bleveengine.PurgeIndexes", "bleveengine.purge_user_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := os.RemoveAll(b.getIndexDir(ChannelIndex)); err != nil {
return model.NewAppError("Bleveengine.PurgeIndexes", "bleveengine.purge_channel_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := os.RemoveAll(b.getIndexDir(FileIndex)); err != nil {
return model.NewAppError("Bleveengine.PurgeIndexes", "bleveengine.purge_file_index.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (b *BleveEngine) PurgeIndexes() *model.AppError {
if *b.cfg.BleveSettings.IndexDir == "" {
return nil
}
b.Mutex.Lock()
defer b.Mutex.Unlock()
mlog.Info("PurgeIndexes Bleve")
if err := b.closeIndexes(); err != nil {
return err
}
if err := b.deleteIndexes(); err != nil {
return err
}
return b.openIndexes()
}
func (b *BleveEngine) DataRetentionDeleteIndexes(cutoff time.Time) *model.AppError {
return nil
}
func (b *BleveEngine) IsAutocompletionEnabled() bool {
return *b.cfg.BleveSettings.EnableAutocomplete
}
func (b *BleveEngine) IsIndexingEnabled() bool {
return *b.cfg.BleveSettings.EnableIndexing
}
func (b *BleveEngine) IsSearchEnabled() bool {
return *b.cfg.BleveSettings.EnableSearching
}
func (b *BleveEngine) UpdateConfig(cfg *model.Config) {
b.Mutex.Lock()
defer b.Mutex.Unlock()
if reflect.DeepEqual(cfg.BleveSettings, b.cfg.BleveSettings) {
return
}
mlog.Info("UpdateConf Bleve")
if *cfg.BleveSettings.EnableIndexing != *b.cfg.BleveSettings.EnableIndexing || *cfg.BleveSettings.IndexDir != *b.cfg.BleveSettings.IndexDir {
if err := b.closeIndexes(); err != nil {
mlog.Error("Error closing Bleve indexes to update the config", mlog.Err(err))
return
}
b.cfg = cfg
if err := b.openIndexes(); err != nil {
mlog.Error("Error opening Bleve indexes after updating the config", mlog.Err(err))
}
return
}
b.cfg = cfg
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package bleveengine
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
)
type BLVChannel struct {
Id string
Type model.ChannelType
UserIDs []string
TeamId []string
TeamMemberIDs []string
NameSuggest []string
}
type BLVUser struct {
Id string
SuggestionsWithFullname []string
SuggestionsWithoutFullname []string
TeamsIds []string
ChannelsIds []string
}
type BLVPost struct {
Id string
TeamId string
ChannelId string
UserId string
CreateAt int64
Message string
Type string
Hashtags []string
Attachments string
}
type BLVFile struct {
Id string
CreatorId string
ChannelId string
CreateAt int64
Name string
Content string
Extension string
}
func BLVChannelFromChannel(channel *model.Channel, userIDs, teamMemberIDs []string) *BLVChannel {
displayNameInputs := searchengine.GetSuggestionInputsSplitBy(channel.DisplayName, " ")
nameInputs := searchengine.GetSuggestionInputsSplitByMultiple(channel.Name, []string{"-", "_"})
return &BLVChannel{
Id: channel.Id,
Type: channel.Type,
TeamId: []string{channel.TeamId},
NameSuggest: append(displayNameInputs, nameInputs...),
UserIDs: userIDs,
TeamMemberIDs: teamMemberIDs,
}
}
func BLVUserFromUserAndTeams(user *model.User, teamsIds, channelsIds []string) *BLVUser {
usernameSuggestions := searchengine.GetSuggestionInputsSplitByMultiple(user.Username, []string{".", "-", "_"})
fullnameStrings := []string{}
if user.FirstName != "" {
fullnameStrings = append(fullnameStrings, user.FirstName)
}
if user.LastName != "" {
fullnameStrings = append(fullnameStrings, user.LastName)
}
fullnameSuggestions := []string{}
if len(fullnameStrings) > 0 {
fullname := strings.Join(fullnameStrings, " ")
fullnameSuggestions = searchengine.GetSuggestionInputsSplitBy(fullname, " ")
}
nicknameSuggestions := []string{}
if user.Nickname != "" {
nicknameSuggestions = searchengine.GetSuggestionInputsSplitBy(user.Nickname, " ")
}
usernameAndNicknameSuggestions := append(usernameSuggestions, nicknameSuggestions...)
return &BLVUser{
Id: user.Id,
SuggestionsWithFullname: append(usernameAndNicknameSuggestions, fullnameSuggestions...),
SuggestionsWithoutFullname: usernameAndNicknameSuggestions,
TeamsIds: teamsIds,
ChannelsIds: channelsIds,
}
}
func BLVUserFromUserForIndexing(userForIndexing *model.UserForIndexing) *BLVUser {
user := &model.User{
Id: userForIndexing.Id,
Username: userForIndexing.Username,
Nickname: userForIndexing.Nickname,
FirstName: userForIndexing.FirstName,
LastName: userForIndexing.LastName,
CreateAt: userForIndexing.CreateAt,
DeleteAt: userForIndexing.DeleteAt,
}
return BLVUserFromUserAndTeams(user, userForIndexing.TeamsIds, userForIndexing.ChannelsIds)
}
func BLVPostFromPost(post *model.Post, teamId string) *BLVPost {
p := &model.PostForIndexing{
TeamId: teamId,
}
post.ShallowCopy(&p.Post)
return BLVPostFromPostForIndexing(p)
}
func BLVPostFromPostForIndexing(post *model.PostForIndexing) *BLVPost {
return &BLVPost{
Id: post.Id,
TeamId: post.TeamId,
ChannelId: post.ChannelId,
UserId: post.UserId,
CreateAt: post.CreateAt,
Message: post.Message,
Type: post.Type,
Hashtags: strings.Fields(post.Hashtags),
}
}
func splitFilenameWords(name string) string {
result := name
result = strings.ReplaceAll(result, "-", " ")
result = strings.ReplaceAll(result, ".", " ")
return result
}
func BLVFileFromFileInfo(fileInfo *model.FileInfo, channelId string) *BLVFile {
return &BLVFile{
Id: fileInfo.Id,
ChannelId: channelId,
CreatorId: fileInfo.CreatorId,
CreateAt: fileInfo.CreateAt,
Content: fileInfo.Content,
Extension: fileInfo.Extension,
Name: fileInfo.Name + " " + splitFilenameWords(fileInfo.Name),
}
}
func BLVFileFromFileForIndexing(file *model.FileForIndexing) *BLVFile {
return &BLVFile{
Id: file.Id,
ChannelId: file.ChannelId,
CreatorId: file.CreatorId,
CreateAt: file.CreateAt,
Content: file.Content,
Extension: file.Extension,
Name: file.Name + " " + splitFilenameWords(file.Name),
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package indexer
import (
"context"
"net/http"
"strconv"
"sync/atomic"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/jobs"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine/bleveengine"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
timeBetweenBatches = 100 * time.Millisecond
estimatedPostCount = 10000000
estimatedFilesCount = 100000
estimatedChannelCount = 100000
estimatedUserCount = 10000
)
type BleveIndexerWorker struct {
name string
stop chan struct{}
stopped chan bool
jobs chan model.Job
jobServer *jobs.JobServer
engine *bleveengine.BleveEngine
closed int32
}
func MakeWorker(jobServer *jobs.JobServer, engine *bleveengine.BleveEngine) model.Worker {
if engine == nil {
return nil
}
return &BleveIndexerWorker{
name: "BleveIndexer",
stop: make(chan struct{}),
stopped: make(chan bool, 1),
jobs: make(chan model.Job),
jobServer: jobServer,
engine: engine,
}
}
type IndexingProgress struct {
Now time.Time
StartAtTime int64
EndAtTime int64
LastEntityTime int64
TotalPostsCount int64
DonePostsCount int64
DonePosts bool
LastPostID string
TotalFilesCount int64
DoneFilesCount int64
DoneFiles bool
LastFileID string
TotalChannelsCount int64
DoneChannelsCount int64
DoneChannels bool
LastChannelID string
TotalUsersCount int64
DoneUsersCount int64
DoneUsers bool
LastUserID string
}
func (ip *IndexingProgress) CurrentProgress() int64 {
return (ip.DonePostsCount + ip.DoneChannelsCount + ip.DoneUsersCount + ip.DoneFilesCount) * 100 / (ip.TotalPostsCount + ip.TotalChannelsCount + ip.TotalUsersCount + ip.TotalFilesCount)
}
func (ip *IndexingProgress) IsDone() bool {
return ip.DonePosts && ip.DoneChannels && ip.DoneUsers && ip.DoneFiles
}
func (worker *BleveIndexerWorker) JobChannel() chan<- model.Job {
return worker.jobs
}
func (worker *BleveIndexerWorker) IsEnabled(cfg *model.Config) bool {
return true
}
func (worker *BleveIndexerWorker) Run() {
// Set to open if closed before. We are not bothered about multiple opens.
if atomic.CompareAndSwapInt32(&worker.closed, 1, 0) {
worker.stop = make(chan struct{})
}
mlog.Debug("Worker Started", mlog.String("workername", worker.name))
defer func() {
mlog.Debug("Worker: Finished", mlog.String("workername", worker.name))
worker.stopped <- true
}()
for {
select {
case <-worker.stop:
mlog.Debug("Worker: Received stop signal", mlog.String("workername", worker.name))
return
case job := <-worker.jobs:
mlog.Debug("Worker: Received a new candidate job.", mlog.String("workername", worker.name))
worker.DoJob(&job)
}
}
}
func (worker *BleveIndexerWorker) Stop() {
// Set to close, and if already closed before, then return.
if !atomic.CompareAndSwapInt32(&worker.closed, 0, 1) {
return
}
mlog.Debug("Worker Stopping", mlog.String("workername", worker.name))
close(worker.stop)
<-worker.stopped
}
func (worker *BleveIndexerWorker) DoJob(job *model.Job) {
claimed, err := worker.jobServer.ClaimJob(job)
if err != nil {
mlog.Warn("Worker: Error occurred while trying to claim job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
return
}
if !claimed {
return
}
mlog.Info("Worker: Indexing job claimed by worker", mlog.String("workername", worker.name), mlog.String("job_id", job.Id))
if !worker.engine.IsActive() {
appError := model.NewAppError("BleveIndexerWorker", "bleveengine.indexer.do_job.engine_inactive", nil, "", http.StatusInternalServerError)
if err := worker.jobServer.SetJobError(job, appError); err != nil {
mlog.Error("Worker: Failed to run job as ")
}
return
}
progress := IndexingProgress{
Now: time.Now(),
DonePosts: false,
DoneChannels: false,
DoneUsers: false,
DoneFiles: false,
StartAtTime: 0,
EndAtTime: model.GetMillis(),
}
// Extract the start and end times, if they are set.
if startString, ok := job.Data["start_time"]; ok {
startInt, err := strconv.ParseInt(startString, 10, 64)
if err != nil {
mlog.Error("Worker: Failed to parse start_time for job", mlog.String("workername", worker.name), mlog.String("start_time", startString), mlog.String("job_id", job.Id), mlog.Err(err))
appError := model.NewAppError("BleveIndexerWorker", "bleveengine.indexer.do_job.parse_start_time.error", nil, "", http.StatusInternalServerError).Wrap(err)
if err := worker.jobServer.SetJobError(job, appError); err != nil {
mlog.Error("Worker: Failed to set job error", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err), mlog.NamedErr("set_error", appError))
}
return
}
progress.StartAtTime = startInt
} else {
// Set start time to oldest entity in the database.
// A user or a channel may be created before any post.
oldestEntityCreationTime, err := worker.jobServer.Store.Post().GetOldestEntityCreationTime()
if err != nil {
mlog.Error("Worker: Failed to fetch oldest entity for job.", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.String("start_time", startString), mlog.Err(err))
appError := model.NewAppError("BleveIndexerWorker", "bleveengine.indexer.do_job.get_oldest_entity.error", nil, "", http.StatusInternalServerError).Wrap(err)
if err := worker.jobServer.SetJobError(job, appError); err != nil {
mlog.Error("Worker: Failed to set job error", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err), mlog.NamedErr("set_error", appError))
}
return
}
progress.StartAtTime = oldestEntityCreationTime
}
progress.LastEntityTime = progress.StartAtTime
if endString, ok := job.Data["end_time"]; ok {
endInt, err := strconv.ParseInt(endString, 10, 64)
if err != nil {
mlog.Error("Worker: Failed to parse end_time for job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.String("end_time", endString), mlog.Err(err))
appError := model.NewAppError("BleveIndexerWorker", "bleveengine.indexer.do_job.parse_end_time.error", nil, "", http.StatusInternalServerError).Wrap(err)
if err := worker.jobServer.SetJobError(job, appError); err != nil {
mlog.Error("Worker: Failed to set job errorv", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err), mlog.NamedErr("set_error", appError))
}
return
}
progress.EndAtTime = endInt
}
if id, ok := job.Data["start_post_id"]; ok {
progress.LastPostID = id
}
if id, ok := job.Data["start_channel_id"]; ok {
progress.LastChannelID = id
}
if id, ok := job.Data["start_user_id"]; ok {
progress.LastUserID = id
}
if id, ok := job.Data["start_file_id"]; ok {
progress.LastFileID = id
}
// Counting all posts may fail or timeout when the posts table is large. If this happens, log a warning, but carry
// on with the indexing job anyway. The only issue is that the progress % reporting will be inaccurate.
if count, err := worker.jobServer.Store.Post().AnalyticsPostCount(&model.PostCountOptions{}); err != nil {
mlog.Warn("Worker: Failed to fetch total post count for job. An estimated value will be used for progress reporting.", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
progress.TotalPostsCount = estimatedPostCount
} else {
progress.TotalPostsCount = count
}
// Same possible fail as above can happen when counting channels
if count, err := worker.jobServer.Store.Channel().AnalyticsTypeCount("", ""); err != nil {
mlog.Warn("Worker: Failed to fetch total channel count for job. An estimated value will be used for progress reporting.", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
progress.TotalChannelsCount = estimatedChannelCount
} else {
progress.TotalChannelsCount = count
}
// Same possible fail as above can happen when counting users
if count, err := worker.jobServer.Store.User().Count(model.UserCountOptions{
IncludeBotAccounts: true, // This actually doesn't join with the bots table
// since ExcludeRegularUsers is set to false
}); err != nil {
mlog.Warn("Worker: Failed to fetch total user count for job. An estimated value will be used for progress reporting.", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
progress.TotalUsersCount = estimatedUserCount
} else {
progress.TotalUsersCount = count
}
// Counting all files may fail or timeout when the file_info table is large. If this happens, log a warning, but carry
// on with the indexing job anyway. The only issue is that the progress % reporting will be inaccurate.
if count, err := worker.jobServer.Store.FileInfo().CountAll(); err != nil {
mlog.Warn("Worker: Failed to fetch total file info count for job. An estimated value will be used for progress reporting.", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
progress.TotalFilesCount = estimatedFilesCount
} else {
progress.TotalFilesCount = count
}
cancelCtx, cancelCancelWatcher := context.WithCancel(context.Background())
cancelWatcherChan := make(chan struct{}, 1)
go worker.jobServer.CancellationWatcher(cancelCtx, job.Id, cancelWatcherChan)
defer cancelCancelWatcher()
for {
select {
case <-cancelWatcherChan:
mlog.Info("Worker: Indexing job has been canceled via CancellationWatcher", mlog.String("workername", worker.name), mlog.String("job_id", job.Id))
if err := worker.jobServer.SetJobCanceled(job); err != nil {
mlog.Error("Worker: Failed to mark job as cancelled", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
}
return
case <-worker.stop:
mlog.Info("Worker: Indexing has been canceled via Worker Stop", mlog.String("workername", worker.name), mlog.String("job_id", job.Id))
if err := worker.jobServer.SetJobCanceled(job); err != nil {
mlog.Error("Worker: Failed to mark job as canceled", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
}
return
case <-time.After(timeBetweenBatches):
var err *model.AppError
if progress, err = worker.IndexBatch(progress); err != nil {
mlog.Error("Worker: Failed to index batch for job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
if err2 := worker.jobServer.SetJobError(job, err); err2 != nil {
mlog.Error("Worker: Failed to set job error", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err2), mlog.NamedErr("set_error", err))
}
return
}
// Storing the batch progress in metadata.
if job.Data == nil {
job.Data = make(model.StringMap)
}
job.Data["start_time"] = strconv.FormatInt(progress.LastEntityTime, 10)
job.Data["start_post_id"] = progress.LastPostID
job.Data["start_channel_id"] = progress.LastChannelID
job.Data["start_user_id"] = progress.LastUserID
job.Data["start_file_id"] = progress.LastFileID
job.Data["original_start_time"] = strconv.FormatInt(progress.StartAtTime, 10)
job.Data["end_time"] = strconv.FormatInt(progress.EndAtTime, 10)
if err := worker.jobServer.SetJobProgress(job, progress.CurrentProgress()); err != nil {
mlog.Error("Worker: Failed to set progress for job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
if err2 := worker.jobServer.SetJobError(job, err); err2 != nil {
mlog.Error("Worker: Failed to set error for job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err2), mlog.NamedErr("set_error", err))
}
return
}
if progress.IsDone() {
if err := worker.jobServer.SetJobSuccess(job); err != nil {
mlog.Error("Worker: Failed to set success for job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
if err2 := worker.jobServer.SetJobError(job, err); err2 != nil {
mlog.Error("Worker: Failed to set error for job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err2), mlog.NamedErr("set_error", err))
}
}
mlog.Info("Worker: Indexing job finished successfully", mlog.String("workername", worker.name), mlog.String("job_id", job.Id))
return
}
}
}
}
func (worker *BleveIndexerWorker) IndexBatch(progress IndexingProgress) (IndexingProgress, *model.AppError) {
if !progress.DonePosts {
return worker.IndexPostsBatch(progress)
}
if !progress.DoneChannels {
return worker.IndexChannelsBatch(progress)
}
if !progress.DoneUsers {
return worker.IndexUsersBatch(progress)
}
if !progress.DoneFiles {
return worker.IndexFilesBatch(progress)
}
return progress, model.NewAppError("BleveIndexerWorker", "bleveengine.indexer.index_batch.nothing_left_to_index.error", nil, "", http.StatusInternalServerError)
}
func (worker *BleveIndexerWorker) IndexPostsBatch(progress IndexingProgress) (IndexingProgress, *model.AppError) {
var posts []*model.PostForIndexing
tries := 0
for posts == nil {
var err error
posts, err = worker.jobServer.Store.Post().GetPostsBatchForIndexing(progress.LastEntityTime, progress.LastPostID, *worker.jobServer.Config().BleveSettings.BatchSize)
if err != nil {
if tries >= 10 {
return progress, model.NewAppError("IndexPostsBatch", "app.post.get_posts_batch_for_indexing.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
mlog.Warn("Failed to get posts batch for indexing. Retrying.", mlog.Err(err))
// Wait a bit before trying again.
time.Sleep(15 * time.Second)
}
tries++
}
// Handle zero messages.
if len(posts) == 0 {
progress.DonePosts = true
progress.LastEntityTime = progress.StartAtTime
return progress, nil
}
lastPost, err := worker.BulkIndexPosts(posts, progress)
if err != nil {
return progress, err
}
// Our exit condition is when the last post's createAt reaches the initial endAtTime
// set during job creation.
if progress.EndAtTime <= lastPost.CreateAt {
progress.DonePosts = true
progress.LastEntityTime = progress.StartAtTime
} else {
progress.LastEntityTime = lastPost.CreateAt
}
progress.LastPostID = lastPost.Id
progress.DonePostsCount += int64(len(posts))
return progress, nil
}
func (worker *BleveIndexerWorker) BulkIndexPosts(posts []*model.PostForIndexing, progress IndexingProgress) (*model.Post, *model.AppError) {
batch := worker.engine.PostIndex.NewBatch()
for _, post := range posts {
if post.DeleteAt == 0 {
searchPost := bleveengine.BLVPostFromPostForIndexing(post)
batch.Index(searchPost.Id, searchPost)
} else {
batch.Delete(post.Id)
}
}
worker.engine.Mutex.RLock()
defer worker.engine.Mutex.RUnlock()
if err := worker.engine.PostIndex.Batch(batch); err != nil {
return nil, model.NewAppError("BleveIndexerWorker.BulkIndexPosts", "bleveengine.indexer.do_job.bulk_index_posts.batch_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &posts[len(posts)-1].Post, nil
}
func (worker *BleveIndexerWorker) IndexFilesBatch(progress IndexingProgress) (IndexingProgress, *model.AppError) {
var files []*model.FileForIndexing
tries := 0
for files == nil {
var err error
files, err = worker.jobServer.Store.FileInfo().GetFilesBatchForIndexing(progress.LastEntityTime, progress.LastFileID, *worker.jobServer.Config().BleveSettings.BatchSize)
if err != nil {
if tries >= 10 {
return progress, model.NewAppError("IndexFilesBatch", "app.post.get_files_batch_for_indexing.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
mlog.Warn("Failed to get files batch for indexing. Retrying.", mlog.Err(err))
// Wait a bit before trying again.
time.Sleep(15 * time.Second)
}
tries++
}
if len(files) == 0 {
progress.DoneFiles = true
progress.LastEntityTime = progress.StartAtTime
return progress, nil
}
lastFile, err := worker.BulkIndexFiles(files, progress)
if err != nil {
return progress, err
}
// Our exit condition is when the last file's createAt reaches the initial endAtTime
// set during job creation.
if progress.EndAtTime <= lastFile.CreateAt {
progress.DoneFiles = true
progress.LastEntityTime = progress.StartAtTime
} else {
progress.LastEntityTime = lastFile.CreateAt
}
progress.LastFileID = lastFile.Id
progress.DoneFilesCount += int64(len(files))
return progress, nil
}
func (worker *BleveIndexerWorker) BulkIndexFiles(files []*model.FileForIndexing, progress IndexingProgress) (*model.FileInfo, *model.AppError) {
batch := worker.engine.FileIndex.NewBatch()
for _, file := range files {
if file.DeleteAt == 0 {
searchFile := bleveengine.BLVFileFromFileForIndexing(file)
batch.Index(searchFile.Id, searchFile)
} else {
batch.Delete(file.Id)
}
}
worker.engine.Mutex.RLock()
defer worker.engine.Mutex.RUnlock()
if err := worker.engine.FileIndex.Batch(batch); err != nil {
return nil, model.NewAppError("BleveIndexerWorker.BulkIndexPosts", "bleveengine.indexer.do_job.bulk_index_files.batch_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &files[len(files)-1].FileInfo, nil
}
func (worker *BleveIndexerWorker) IndexChannelsBatch(progress IndexingProgress) (IndexingProgress, *model.AppError) {
var channels []*model.Channel
tries := 0
for channels == nil {
var nErr error
channels, nErr = worker.jobServer.Store.Channel().GetChannelsBatchForIndexing(progress.LastEntityTime, progress.LastChannelID, *worker.jobServer.Config().BleveSettings.BatchSize)
if nErr != nil {
if tries >= 10 {
return progress, model.NewAppError("BleveIndexerWorker.IndexChannelsBatch", "app.channel.get_channels_batch_for_indexing.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
mlog.Warn("Failed to get channels batch for indexing. Retrying.", mlog.Err(nErr))
// Wait a bit before trying again.
time.Sleep(15 * time.Second)
}
tries++
}
if len(channels) == 0 {
progress.DoneChannels = true
progress.LastEntityTime = progress.StartAtTime
return progress, nil
}
lastChannel, err := worker.BulkIndexChannels(channels, progress)
if err != nil {
return progress, err
}
// Our exit condition is when the last channel's createAt reaches the initial endAtTime
// set during job creation.
if progress.EndAtTime <= lastChannel.CreateAt {
progress.DoneChannels = true
progress.LastEntityTime = progress.StartAtTime
} else {
progress.LastEntityTime = lastChannel.CreateAt
}
progress.LastChannelID = lastChannel.Id
progress.DoneChannelsCount += int64(len(channels))
return progress, nil
}
func (worker *BleveIndexerWorker) BulkIndexChannels(channels []*model.Channel, progress IndexingProgress) (*model.Channel, *model.AppError) {
batch := worker.engine.ChannelIndex.NewBatch()
for _, channel := range channels {
if channel.DeleteAt == 0 {
var userIDs []string
var err error
if channel.Type == model.ChannelTypePrivate {
userIDs, err = worker.jobServer.Store.Channel().GetAllChannelMembersById(channel.Id)
if err != nil {
return nil, model.NewAppError("BleveIndexerWorker.BulkIndexChannels", "bleveengine.indexer.do_job.bulk_index_channels.batch_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// Get teamMember ids from channelid
teamMemberIDs, err := worker.jobServer.Store.Channel().GetTeamMembersForChannel(channel.Id)
if err != nil {
return nil, model.NewAppError("BleveIndexerWorker.BulkIndexChannels", "bleveengine.indexer.do_job.bulk_index_channels.batch_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
searchChannel := bleveengine.BLVChannelFromChannel(channel, userIDs, teamMemberIDs)
batch.Index(searchChannel.Id, searchChannel)
} else {
batch.Delete(channel.Id)
}
}
worker.engine.Mutex.RLock()
defer worker.engine.Mutex.RUnlock()
if err := worker.engine.ChannelIndex.Batch(batch); err != nil {
return nil, model.NewAppError("BleveIndexerWorker.BulkIndexChannels", "bleveengine.indexer.do_job.bulk_index_channels.batch_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channels[len(channels)-1], nil
}
func (worker *BleveIndexerWorker) IndexUsersBatch(progress IndexingProgress) (IndexingProgress, *model.AppError) {
var users []*model.UserForIndexing
tries := 0
for users == nil {
if usersBatch, err := worker.jobServer.Store.User().GetUsersBatchForIndexing(progress.LastEntityTime, progress.LastUserID, *worker.jobServer.Config().BleveSettings.BatchSize); err != nil {
if tries >= 10 {
return progress, model.NewAppError("IndexUsersBatch", "app.user.get_users_batch_for_indexing.get_users.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
mlog.Warn("Failed to get users batch for indexing. Retrying.", mlog.Err(err))
// Wait a bit before trying again.
time.Sleep(15 * time.Second)
} else {
users = usersBatch
}
tries++
}
if len(users) == 0 {
progress.DoneUsers = true
progress.LastEntityTime = progress.StartAtTime
return progress, nil
}
lastUser, err := worker.BulkIndexUsers(users, progress)
if err != nil {
return progress, err
}
// Our exit condition is when the last user's createAt reaches the initial endAtTime
// set during job creation.
if progress.EndAtTime <= lastUser.CreateAt {
progress.DoneUsers = true
progress.LastEntityTime = progress.StartAtTime
} else {
progress.LastEntityTime = lastUser.CreateAt
}
progress.LastUserID = lastUser.Id
progress.DoneUsersCount += int64(len(users))
return progress, nil
}
func (worker *BleveIndexerWorker) BulkIndexUsers(users []*model.UserForIndexing, progress IndexingProgress) (*model.UserForIndexing, *model.AppError) {
batch := worker.engine.UserIndex.NewBatch()
for _, user := range users {
if user.DeleteAt == 0 {
searchUser := bleveengine.BLVUserFromUserForIndexing(user)
batch.Index(searchUser.Id, searchUser)
} else {
batch.Delete(user.Id)
}
}
worker.engine.Mutex.RLock()
defer worker.engine.Mutex.RUnlock()
if err := worker.engine.UserIndex.Batch(batch); err != nil {
return nil, model.NewAppError("BleveIndexerWorker.BulkIndexUsers", "bleveengine.indexer.do_job.bulk_index_users.batch_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users[len(users)-1], nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package bleveengine
import (
"net/http"
"strings"
"github.com/blevesearch/bleve/v2"
"github.com/blevesearch/bleve/v2/search/query"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const DeletePostsBatchSize = 500
const DeleteFilesBatchSize = 500
func (b *BleveEngine) IndexPost(post *model.Post, teamId string) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
blvPost := BLVPostFromPost(post, teamId)
if err := b.PostIndex.Index(blvPost.Id, blvPost); err != nil {
return model.NewAppError("Bleveengine.IndexPost", "bleveengine.index_post.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (b *BleveEngine) SearchPosts(channels model.ChannelList, searchParams []*model.SearchParams, page, perPage int) ([]string, model.PostSearchMatches, *model.AppError) {
channelQueries := []query.Query{}
for _, channel := range channels {
channelIdQ := bleve.NewTermQuery(channel.Id)
channelIdQ.SetField("ChannelId")
channelQueries = append(channelQueries, channelIdQ)
}
channelDisjunctionQ := bleve.NewDisjunctionQuery(channelQueries...)
var termQueries []query.Query
var notTermQueries []query.Query
var filters []query.Query
var notFilters []query.Query
typeQ := bleve.NewTermQuery("")
typeQ.SetField("Type")
filters = append(filters, typeQ)
for i, params := range searchParams {
var termOperator query.MatchQueryOperator = query.MatchQueryOperatorAnd
if searchParams[0].OrTerms {
termOperator = query.MatchQueryOperatorOr
}
// Date, channels and FromUsers filters come in all
// searchParams iteration, and as they are global to the
// query, we only need to process them once
if i == 0 {
if len(params.InChannels) > 0 {
inChannels := []query.Query{}
for _, channelId := range params.InChannels {
channelQ := bleve.NewTermQuery(channelId)
channelQ.SetField("ChannelId")
inChannels = append(inChannels, channelQ)
}
filters = append(filters, bleve.NewDisjunctionQuery(inChannels...))
}
if len(params.ExcludedChannels) > 0 {
excludedChannels := []query.Query{}
for _, channelId := range params.ExcludedChannels {
channelQ := bleve.NewTermQuery(channelId)
channelQ.SetField("ChannelId")
excludedChannels = append(excludedChannels, channelQ)
}
notFilters = append(notFilters, bleve.NewDisjunctionQuery(excludedChannels...))
}
if len(params.FromUsers) > 0 {
fromUsers := []query.Query{}
for _, userId := range params.FromUsers {
userQ := bleve.NewTermQuery(userId)
userQ.SetField("UserId")
fromUsers = append(fromUsers, userQ)
}
filters = append(filters, bleve.NewDisjunctionQuery(fromUsers...))
}
if len(params.ExcludedUsers) > 0 {
excludedUsers := []query.Query{}
for _, userId := range params.ExcludedUsers {
userQ := bleve.NewTermQuery(userId)
userQ.SetField("UserId")
excludedUsers = append(excludedUsers, userQ)
}
notFilters = append(notFilters, bleve.NewDisjunctionQuery(excludedUsers...))
}
if params.OnDate != "" {
before, after := params.GetOnDateMillis()
beforeFloat64 := float64(before)
afterFloat64 := float64(after)
onDateQ := bleve.NewNumericRangeQuery(&beforeFloat64, &afterFloat64)
onDateQ.SetField("CreateAt")
filters = append(filters, onDateQ)
} else {
if params.AfterDate != "" || params.BeforeDate != "" {
var min, max *float64
if params.AfterDate != "" {
minf := float64(params.GetAfterDateMillis())
min = &minf
}
if params.BeforeDate != "" {
maxf := float64(params.GetBeforeDateMillis())
max = &maxf
}
dateQ := bleve.NewNumericRangeQuery(min, max)
dateQ.SetField("CreateAt")
filters = append(filters, dateQ)
}
if params.ExcludedAfterDate != "" {
minf := float64(params.GetExcludedAfterDateMillis())
dateQ := bleve.NewNumericRangeQuery(&minf, nil)
dateQ.SetField("CreateAt")
notFilters = append(notFilters, dateQ)
}
if params.ExcludedBeforeDate != "" {
maxf := float64(params.GetExcludedBeforeDateMillis())
dateQ := bleve.NewNumericRangeQuery(nil, &maxf)
dateQ.SetField("CreateAt")
notFilters = append(notFilters, dateQ)
}
if params.ExcludedDate != "" {
before, after := params.GetExcludedDateMillis()
beforef := float64(before)
afterf := float64(after)
onDateQ := bleve.NewNumericRangeQuery(&beforef, &afterf)
onDateQ.SetField("CreateAt")
notFilters = append(notFilters, onDateQ)
}
}
}
if params.IsHashtag {
if params.Terms != "" {
hashtagQ := bleve.NewMatchQuery(params.Terms)
hashtagQ.SetField("Hashtags")
hashtagQ.SetOperator(termOperator)
termQueries = append(termQueries, hashtagQ)
} else if params.ExcludedTerms != "" {
hashtagQ := bleve.NewMatchQuery(params.ExcludedTerms)
hashtagQ.SetField("Hashtags")
hashtagQ.SetOperator(termOperator)
notTermQueries = append(notTermQueries, hashtagQ)
}
} else {
if params.Terms != "" {
terms := []string{}
for _, term := range strings.Split(params.Terms, " ") {
if strings.HasSuffix(term, "*") {
messageQ := bleve.NewWildcardQuery(term)
messageQ.SetField("Message")
termQueries = append(termQueries, messageQ)
} else {
terms = append(terms, term)
}
}
if len(terms) > 0 {
messageQ := bleve.NewMatchQuery(strings.Join(terms, " "))
messageQ.SetField("Message")
messageQ.SetOperator(termOperator)
termQueries = append(termQueries, messageQ)
}
}
if params.ExcludedTerms != "" {
messageQ := bleve.NewMatchQuery(params.ExcludedTerms)
messageQ.SetField("Message")
messageQ.SetOperator(termOperator)
notTermQueries = append(notTermQueries, messageQ)
}
}
}
allTermsQ := bleve.NewBooleanQuery()
allTermsQ.AddMustNot(notTermQueries...)
if searchParams[0].OrTerms {
allTermsQ.AddShould(termQueries...)
} else {
allTermsQ.AddMust(termQueries...)
}
query := bleve.NewBooleanQuery()
query.AddMust(channelDisjunctionQ)
if len(termQueries) > 0 || len(notTermQueries) > 0 {
query.AddMust(allTermsQ)
}
if len(filters) > 0 {
query.AddMust(bleve.NewConjunctionQuery(filters...))
}
if len(notFilters) > 0 {
query.AddMustNot(notFilters...)
}
search := bleve.NewSearchRequestOptions(query, perPage, page*perPage, false)
search.SortBy([]string{"-CreateAt"})
results, err := b.PostIndex.Search(search)
if err != nil {
return nil, nil, model.NewAppError("Bleveengine.SearchPosts", "bleveengine.search_posts.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
postIds := []string{}
matches := model.PostSearchMatches{}
for _, r := range results.Hits {
postIds = append(postIds, r.ID)
}
return postIds, matches, nil
}
func (b *BleveEngine) deletePosts(searchRequest *bleve.SearchRequest, batchSize int) (int64, error) {
resultsCount := int64(0)
for {
// As we are deleting the posts after fetching them, we need to keep
// From fixed always to 0
searchRequest.From = 0
searchRequest.Size = batchSize
results, err := b.PostIndex.Search(searchRequest)
if err != nil {
return -1, err
}
batch := b.PostIndex.NewBatch()
for _, post := range results.Hits {
batch.Delete(post.ID)
}
if err := b.PostIndex.Batch(batch); err != nil {
return -1, err
}
resultsCount += int64(results.Hits.Len())
if results.Hits.Len() < batchSize {
break
}
}
return resultsCount, nil
}
func (b *BleveEngine) DeleteChannelPosts(channelID string) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
query := bleve.NewTermQuery(channelID)
query.SetField("ChannelId")
search := bleve.NewSearchRequest(query)
deleted, err := b.deletePosts(search, DeletePostsBatchSize)
if err != nil {
return model.NewAppError("Bleveengine.DeleteChannelPosts",
"bleveengine.delete_channel_posts.error", nil,
err.Error(), http.StatusInternalServerError)
}
mlog.Info("Posts for channel deleted", mlog.String("channel_id", channelID), mlog.Int64("deleted", deleted))
return nil
}
func (b *BleveEngine) DeleteUserPosts(userID string) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
query := bleve.NewTermQuery(userID)
query.SetField("UserId")
search := bleve.NewSearchRequest(query)
deleted, err := b.deletePosts(search, DeletePostsBatchSize)
if err != nil {
return model.NewAppError("Bleveengine.DeleteUserPosts",
"bleveengine.delete_user_posts.error", nil,
err.Error(), http.StatusInternalServerError)
}
mlog.Info("Posts for user deleted", mlog.String("user_id", userID), mlog.Int64("deleted", deleted))
return nil
}
func (b *BleveEngine) DeletePost(post *model.Post) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
if err := b.PostIndex.Delete(post.Id); err != nil {
return model.NewAppError("Bleveengine.DeletePost", "bleveengine.delete_post.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (b *BleveEngine) IndexChannel(channel *model.Channel, userIDs, teamMemberIDs []string) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
blvChannel := BLVChannelFromChannel(channel, userIDs, teamMemberIDs)
if err := b.ChannelIndex.Index(blvChannel.Id, blvChannel); err != nil {
return model.NewAppError("Bleveengine.IndexChannel", "bleveengine.index_channel.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (b *BleveEngine) SearchChannels(teamId, userID, term string, isGuest bool) ([]string, *model.AppError) {
// This query essentially boils down to (if teamID is passed):
// match teamID == <>
// AND
// match term == <>
// AND
// match (channelType != 'P' || (<> in userIDs && channelType == 'P'))
// (or if teamID is not passed)
// <> in teamMemberIds
// AND
// match term == <>
// AND
// match (channelType != 'P' || (<> in userIDs && channelType == 'P'))
// (or if isGuest is true)
// <> in teamMemberIds
// AND
// match term == <>
// AND
// match (<> in userIDs)
queries := []query.Query{}
if teamId != "" {
teamIdQ := bleve.NewTermQuery(teamId)
teamIdQ.SetField("TeamId")
queries = append(queries, teamIdQ)
} else {
teamMemberQ := bleve.NewTermQuery(userID)
teamMemberQ.SetField("TeamMemberIDs")
queries = append(queries, teamMemberQ)
}
if isGuest {
userQ := bleve.NewBooleanQuery()
userIDQ := bleve.NewTermQuery(userID)
userIDQ.SetField("UserIDs")
userQ.AddMust(userIDQ)
queries = append(queries, userIDQ)
} else {
boolNotPrivate := bleve.NewBooleanQuery()
privateQ := bleve.NewTermQuery(string(model.ChannelTypePrivate))
privateQ.SetField("Type")
boolNotPrivate.AddMustNot(privateQ)
userQ := bleve.NewBooleanQuery()
userIDQ := bleve.NewTermQuery(userID)
userIDQ.SetField("UserIDs")
userQ.AddMust(userIDQ)
userQ.AddMust(privateQ)
channelTypeQ := bleve.NewDisjunctionQuery()
channelTypeQ.AddQuery(boolNotPrivate)
channelTypeQ.AddQuery(userQ) // userID && 'p'
queries = append(queries, channelTypeQ)
}
if term != "" {
nameSuggestQ := bleve.NewPrefixQuery(strings.ToLower(term))
nameSuggestQ.SetField("NameSuggest")
queries = append(queries, nameSuggestQ)
}
query := bleve.NewSearchRequest(bleve.NewConjunctionQuery(queries...))
query.Size = model.ChannelSearchDefaultLimit
results, err := b.ChannelIndex.Search(query)
if err != nil {
return nil, model.NewAppError("Bleveengine.SearchChannels", "bleveengine.search_channels.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
channelIds := []string{}
for _, result := range results.Hits {
channelIds = append(channelIds, result.ID)
}
return channelIds, nil
}
func (b *BleveEngine) DeleteChannel(channel *model.Channel) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
if err := b.ChannelIndex.Delete(channel.Id); err != nil {
return model.NewAppError("Bleveengine.DeleteChannel", "bleveengine.delete_channel.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (b *BleveEngine) IndexUser(user *model.User, teamsIds, channelsIds []string) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
blvUser := BLVUserFromUserAndTeams(user, teamsIds, channelsIds)
if err := b.UserIndex.Index(blvUser.Id, blvUser); err != nil {
return model.NewAppError("Bleveengine.IndexUser", "bleveengine.index_user.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (b *BleveEngine) SearchUsersInChannel(teamId, channelId string, restrictedToChannels []string, term string, options *model.UserSearchOptions) ([]string, []string, *model.AppError) {
if restrictedToChannels != nil && len(restrictedToChannels) == 0 {
return []string{}, []string{}, nil
}
// users in channel
var queries []query.Query
if term != "" {
termQ := bleve.NewPrefixQuery(strings.ToLower(term))
if options.AllowFullNames {
termQ.SetField("SuggestionsWithFullname")
} else {
termQ.SetField("SuggestionsWithoutFullname")
}
queries = append(queries, termQ)
}
channelIdQ := bleve.NewTermQuery(channelId)
channelIdQ.SetField("ChannelsIds")
queries = append(queries, channelIdQ)
query := bleve.NewConjunctionQuery(queries...)
uchanSearch := bleve.NewSearchRequest(query)
uchanSearch.Size = options.Limit
uchan, err := b.UserIndex.Search(uchanSearch)
if err != nil {
return nil, nil, model.NewAppError("Bleveengine.SearchUsersInChannel", "bleveengine.search_users_in_channel.uchan.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// users not in channel
boolQ := bleve.NewBooleanQuery()
if term != "" {
termQ := bleve.NewPrefixQuery(strings.ToLower(term))
if options.AllowFullNames {
termQ.SetField("SuggestionsWithFullname")
} else {
termQ.SetField("SuggestionsWithoutFullname")
}
boolQ.AddMust(termQ)
}
teamIdQ := bleve.NewTermQuery(teamId)
teamIdQ.SetField("TeamsIds")
boolQ.AddMust(teamIdQ)
outsideChannelIdQ := bleve.NewTermQuery(channelId)
outsideChannelIdQ.SetField("ChannelsIds")
boolQ.AddMustNot(outsideChannelIdQ)
if len(restrictedToChannels) > 0 {
restrictedChannelsQ := bleve.NewDisjunctionQuery()
for _, channelId := range restrictedToChannels {
restrictedChannelQ := bleve.NewTermQuery(channelId)
restrictedChannelsQ.AddQuery(restrictedChannelQ)
}
boolQ.AddMust(restrictedChannelsQ)
}
nuchanSearch := bleve.NewSearchRequest(boolQ)
nuchanSearch.Size = options.Limit
nuchan, err := b.UserIndex.Search(nuchanSearch)
if err != nil {
return nil, nil, model.NewAppError("Bleveengine.SearchUsersInChannel", "bleveengine.search_users_in_channel.nuchan.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
uchanIds := []string{}
for _, result := range uchan.Hits {
uchanIds = append(uchanIds, result.ID)
}
nuchanIds := []string{}
for _, result := range nuchan.Hits {
nuchanIds = append(nuchanIds, result.ID)
}
return uchanIds, nuchanIds, nil
}
func (b *BleveEngine) SearchUsersInTeam(teamId string, restrictedToChannels []string, term string, options *model.UserSearchOptions) ([]string, *model.AppError) {
if restrictedToChannels != nil && len(restrictedToChannels) == 0 {
return []string{}, nil
}
var rootQ query.Query
if term == "" && teamId == "" && restrictedToChannels == nil {
rootQ = bleve.NewMatchAllQuery()
} else {
boolQ := bleve.NewBooleanQuery()
if term != "" {
termQ := bleve.NewPrefixQuery(strings.ToLower(term))
if options.AllowFullNames {
termQ.SetField("SuggestionsWithFullname")
} else {
termQ.SetField("SuggestionsWithoutFullname")
}
boolQ.AddMust(termQ)
}
if len(restrictedToChannels) > 0 {
// restricted channels are already filtered by team, so we
// can search only those matches
restrictedChannelsQ := []query.Query{}
for _, channelId := range restrictedToChannels {
channelIdQ := bleve.NewTermQuery(channelId)
channelIdQ.SetField("ChannelsIds")
restrictedChannelsQ = append(restrictedChannelsQ, channelIdQ)
}
boolQ.AddMust(bleve.NewDisjunctionQuery(restrictedChannelsQ...))
} else {
// this means that we only need to restrict by team
if teamId != "" {
teamIdQ := bleve.NewTermQuery(teamId)
teamIdQ.SetField("TeamsIds")
boolQ.AddMust(teamIdQ)
}
}
rootQ = boolQ
}
search := bleve.NewSearchRequest(rootQ)
search.Size = options.Limit
results, err := b.UserIndex.Search(search)
if err != nil {
return nil, model.NewAppError("Bleveengine.SearchUsersInTeam", "bleveengine.search_users_in_team.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
usersIds := []string{}
for _, r := range results.Hits {
usersIds = append(usersIds, r.ID)
}
return usersIds, nil
}
func (b *BleveEngine) DeleteUser(user *model.User) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
if err := b.UserIndex.Delete(user.Id); err != nil {
return model.NewAppError("Bleveengine.DeleteUser", "bleveengine.delete_user.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (b *BleveEngine) IndexFile(file *model.FileInfo, channelId string) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
blvFile := BLVFileFromFileInfo(file, channelId)
if err := b.FileIndex.Index(blvFile.Id, blvFile); err != nil {
return model.NewAppError("Bleveengine.IndexFile", "bleveengine.index_file.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (b *BleveEngine) SearchFiles(channels model.ChannelList, searchParams []*model.SearchParams, page, perPage int) ([]string, *model.AppError) {
channelQueries := []query.Query{}
for _, channel := range channels {
channelIdQ := bleve.NewTermQuery(channel.Id)
channelIdQ.SetField("ChannelId")
channelQueries = append(channelQueries, channelIdQ)
}
channelDisjunctionQ := bleve.NewDisjunctionQuery(channelQueries...)
var termQueries []query.Query
var notTermQueries []query.Query
var filters []query.Query
var notFilters []query.Query
for i, params := range searchParams {
var termOperator query.MatchQueryOperator = query.MatchQueryOperatorAnd
if searchParams[0].OrTerms {
termOperator = query.MatchQueryOperatorOr
}
// Date, channels and FromUsers filters come in all
// searchParams iteration, and as they are global to the
// query, we only need to process them once
if i == 0 {
if len(params.InChannels) > 0 {
inChannels := []query.Query{}
for _, channelId := range params.InChannels {
channelQ := bleve.NewTermQuery(channelId)
channelQ.SetField("ChannelId")
inChannels = append(inChannels, channelQ)
}
filters = append(filters, bleve.NewDisjunctionQuery(inChannels...))
}
if len(params.ExcludedChannels) > 0 {
excludedChannels := []query.Query{}
for _, channelId := range params.ExcludedChannels {
channelQ := bleve.NewTermQuery(channelId)
channelQ.SetField("ChannelId")
excludedChannels = append(excludedChannels, channelQ)
}
notFilters = append(notFilters, bleve.NewDisjunctionQuery(excludedChannels...))
}
if len(params.FromUsers) > 0 {
fromUsers := []query.Query{}
for _, userId := range params.FromUsers {
userQ := bleve.NewTermQuery(userId)
userQ.SetField("CreatorId")
fromUsers = append(fromUsers, userQ)
}
filters = append(filters, bleve.NewDisjunctionQuery(fromUsers...))
}
if len(params.ExcludedUsers) > 0 {
excludedUsers := []query.Query{}
for _, userId := range params.ExcludedUsers {
userQ := bleve.NewTermQuery(userId)
userQ.SetField("CreatorId")
excludedUsers = append(excludedUsers, userQ)
}
notFilters = append(notFilters, bleve.NewDisjunctionQuery(excludedUsers...))
}
if len(params.Extensions) > 0 {
extensions := []query.Query{}
for _, extension := range params.Extensions {
extensionQ := bleve.NewTermQuery(extension)
extensionQ.SetField("Extension")
extensions = append(extensions, extensionQ)
}
filters = append(filters, bleve.NewDisjunctionQuery(extensions...))
}
if len(params.ExcludedExtensions) > 0 {
excludedExtensions := []query.Query{}
for _, extension := range params.ExcludedExtensions {
extensionQ := bleve.NewTermQuery(extension)
extensionQ.SetField("Extension")
excludedExtensions = append(excludedExtensions, extensionQ)
}
notFilters = append(notFilters, bleve.NewDisjunctionQuery(excludedExtensions...))
}
if params.OnDate != "" {
before, after := params.GetOnDateMillis()
beforeFloat64 := float64(before)
afterFloat64 := float64(after)
onDateQ := bleve.NewNumericRangeQuery(&beforeFloat64, &afterFloat64)
onDateQ.SetField("CreateAt")
filters = append(filters, onDateQ)
} else {
if params.AfterDate != "" || params.BeforeDate != "" {
var min, max *float64
if params.AfterDate != "" {
minf := float64(params.GetAfterDateMillis())
min = &minf
}
if params.BeforeDate != "" {
maxf := float64(params.GetBeforeDateMillis())
max = &maxf
}
dateQ := bleve.NewNumericRangeQuery(min, max)
dateQ.SetField("CreateAt")
filters = append(filters, dateQ)
}
if params.ExcludedAfterDate != "" {
minf := float64(params.GetExcludedAfterDateMillis())
dateQ := bleve.NewNumericRangeQuery(&minf, nil)
dateQ.SetField("CreateAt")
notFilters = append(notFilters, dateQ)
}
if params.ExcludedBeforeDate != "" {
maxf := float64(params.GetExcludedBeforeDateMillis())
dateQ := bleve.NewNumericRangeQuery(nil, &maxf)
dateQ.SetField("CreateAt")
notFilters = append(notFilters, dateQ)
}
if params.ExcludedDate != "" {
before, after := params.GetExcludedDateMillis()
beforef := float64(before)
afterf := float64(after)
onDateQ := bleve.NewNumericRangeQuery(&beforef, &afterf)
onDateQ.SetField("CreateAt")
notFilters = append(notFilters, onDateQ)
}
}
}
if params.Terms != "" {
terms := []string{}
for _, term := range strings.Split(params.Terms, " ") {
if strings.HasSuffix(term, "*") {
nameQ := bleve.NewWildcardQuery(term)
nameQ.SetField("Name")
contentQ := bleve.NewWildcardQuery(term)
contentQ.SetField("Content")
termQueries = append(termQueries, bleve.NewDisjunctionQuery(nameQ, contentQ))
} else {
terms = append(terms, term)
}
}
if len(terms) > 0 {
nameQ := bleve.NewMatchQuery(strings.Join(terms, " "))
nameQ.SetField("Name")
nameQ.SetOperator(termOperator)
contentQ := bleve.NewMatchQuery(strings.Join(terms, " "))
contentQ.SetField("Content")
contentQ.SetOperator(termOperator)
termQueries = append(termQueries, bleve.NewDisjunctionQuery(nameQ, contentQ))
}
}
if params.ExcludedTerms != "" {
nameQ := bleve.NewMatchQuery(params.ExcludedTerms)
nameQ.SetField("Name")
nameQ.SetOperator(termOperator)
contentQ := bleve.NewMatchQuery(params.ExcludedTerms)
contentQ.SetField("Content")
contentQ.SetOperator(termOperator)
notTermQueries = append(notTermQueries, bleve.NewDisjunctionQuery(nameQ, contentQ))
}
}
allTermsQ := bleve.NewBooleanQuery()
allTermsQ.AddMustNot(notTermQueries...)
if searchParams[0].OrTerms {
allTermsQ.AddShould(termQueries...)
} else {
allTermsQ.AddMust(termQueries...)
}
query := bleve.NewBooleanQuery()
query.AddMust(channelDisjunctionQ)
if len(termQueries) > 0 || len(notTermQueries) > 0 {
query.AddMust(allTermsQ)
}
if len(filters) > 0 {
query.AddMust(bleve.NewConjunctionQuery(filters...))
}
if len(notFilters) > 0 {
query.AddMustNot(notFilters...)
}
search := bleve.NewSearchRequestOptions(query, perPage, page*perPage, false)
search.SortBy([]string{"-CreateAt"})
results, err := b.FileIndex.Search(search)
if err != nil {
return nil, model.NewAppError("Bleveengine.SearchFiles", "bleveengine.search_files.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
fileIds := []string{}
for _, r := range results.Hits {
fileIds = append(fileIds, r.ID)
}
return fileIds, nil
}
func (b *BleveEngine) DeleteFile(fileID string) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
if err := b.FileIndex.Delete(fileID); err != nil {
return model.NewAppError("Bleveengine.DeleteFile", "bleveengine.delete_file.error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (b *BleveEngine) deleteFiles(searchRequest *bleve.SearchRequest, batchSize int) (int64, error) {
resultsCount := int64(0)
for {
// As we are deleting the files after fetching them, we need to keep
// From fixed always to 0
searchRequest.From = 0
searchRequest.Size = batchSize
results, err := b.FileIndex.Search(searchRequest)
if err != nil {
return -1, err
}
batch := b.FileIndex.NewBatch()
for _, file := range results.Hits {
batch.Delete(file.ID)
}
if err := b.FileIndex.Batch(batch); err != nil {
return -1, err
}
resultsCount += int64(results.Hits.Len())
if results.Hits.Len() < batchSize {
break
}
}
return resultsCount, nil
}
func (b *BleveEngine) DeleteUserFiles(userID string) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
query := bleve.NewTermQuery(userID)
query.SetField("CreatorId")
search := bleve.NewSearchRequest(query)
deleted, err := b.deleteFiles(search, DeleteFilesBatchSize)
if err != nil {
return model.NewAppError("Bleveengine.DeleteUserFiles",
"bleveengine.delete_user_files.error", nil,
err.Error(), http.StatusInternalServerError)
}
mlog.Info("Files for user deleted", mlog.String("user_id", userID), mlog.Int64("deleted", deleted))
return nil
}
func (b *BleveEngine) DeletePostFiles(postID string) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
query := bleve.NewTermQuery(postID)
query.SetField("PostId")
search := bleve.NewSearchRequest(query)
deleted, err := b.deleteFiles(search, DeleteFilesBatchSize)
if err != nil {
return model.NewAppError("Bleveengine.DeletePostFiles",
"bleveengine.delete_post_files.error", nil,
err.Error(), http.StatusInternalServerError)
}
mlog.Info("Files for post deleted", mlog.String("post_id", postID), mlog.Int64("deleted", deleted))
return nil
}
func (b *BleveEngine) DeleteFilesBatch(endTime, limit int64) *model.AppError {
b.Mutex.RLock()
defer b.Mutex.RUnlock()
endTimeFloat := float64(endTime)
query := bleve.NewNumericRangeQuery(nil, &endTimeFloat)
query.SetField("CreateAt")
search := bleve.NewSearchRequestOptions(query, int(limit), 0, false)
search.SortBy([]string{"-CreateAt"})
deleted, err := b.deleteFiles(search, DeleteFilesBatchSize)
if err != nil {
return model.NewAppError("Bleveengine.DeleteFilesBatch",
"bleveengine.delete_files_batch.error", nil,
err.Error(), http.StatusInternalServerError)
}
mlog.Info("Files in batch deleted", mlog.Int64("endTime", endTime), mlog.Int64("limit", limit), mlog.Int64("deleted", deleted))
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package bleveengine
import (
"fmt"
"github.com/mattermost/mattermost-server/v6/model"
)
func createPost(userId string, channelId string) *model.Post {
post := &model.Post{
Message: model.NewRandomString(15),
ChannelId: channelId,
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
UserId: userId,
CreateAt: 1000000,
}
post.PreSave()
return post
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make searchengine-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
time "time"
)
// SearchEngineInterface is an autogenerated mock type for the SearchEngineInterface type
type SearchEngineInterface struct {
mock.Mock
}
// DataRetentionDeleteIndexes provides a mock function with given fields: cutoff
func (_m *SearchEngineInterface) DataRetentionDeleteIndexes(cutoff time.Time) *model.AppError {
ret := _m.Called(cutoff)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(time.Time) *model.AppError); ok {
r0 = rf(cutoff)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// DeleteChannel provides a mock function with given fields: channel
func (_m *SearchEngineInterface) DeleteChannel(channel *model.Channel) *model.AppError {
ret := _m.Called(channel)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(*model.Channel) *model.AppError); ok {
r0 = rf(channel)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// DeleteChannelPosts provides a mock function with given fields: channelID
func (_m *SearchEngineInterface) DeleteChannelPosts(channelID string) *model.AppError {
ret := _m.Called(channelID)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(string) *model.AppError); ok {
r0 = rf(channelID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// DeleteFile provides a mock function with given fields: fileID
func (_m *SearchEngineInterface) DeleteFile(fileID string) *model.AppError {
ret := _m.Called(fileID)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(string) *model.AppError); ok {
r0 = rf(fileID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// DeleteFilesBatch provides a mock function with given fields: endTime, limit
func (_m *SearchEngineInterface) DeleteFilesBatch(endTime int64, limit int64) *model.AppError {
ret := _m.Called(endTime, limit)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(int64, int64) *model.AppError); ok {
r0 = rf(endTime, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// DeletePost provides a mock function with given fields: post
func (_m *SearchEngineInterface) DeletePost(post *model.Post) *model.AppError {
ret := _m.Called(post)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(*model.Post) *model.AppError); ok {
r0 = rf(post)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// DeletePostFiles provides a mock function with given fields: postID
func (_m *SearchEngineInterface) DeletePostFiles(postID string) *model.AppError {
ret := _m.Called(postID)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(string) *model.AppError); ok {
r0 = rf(postID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// DeleteUser provides a mock function with given fields: user
func (_m *SearchEngineInterface) DeleteUser(user *model.User) *model.AppError {
ret := _m.Called(user)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(*model.User) *model.AppError); ok {
r0 = rf(user)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// DeleteUserFiles provides a mock function with given fields: userID
func (_m *SearchEngineInterface) DeleteUserFiles(userID string) *model.AppError {
ret := _m.Called(userID)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(string) *model.AppError); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// DeleteUserPosts provides a mock function with given fields: userID
func (_m *SearchEngineInterface) DeleteUserPosts(userID string) *model.AppError {
ret := _m.Called(userID)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(string) *model.AppError); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// GetFullVersion provides a mock function with given fields:
func (_m *SearchEngineInterface) GetFullVersion() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// GetName provides a mock function with given fields:
func (_m *SearchEngineInterface) GetName() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// GetPlugins provides a mock function with given fields:
func (_m *SearchEngineInterface) GetPlugins() []string {
ret := _m.Called()
var r0 []string
if rf, ok := ret.Get(0).(func() []string); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
return r0
}
// GetVersion provides a mock function with given fields:
func (_m *SearchEngineInterface) GetVersion() int {
ret := _m.Called()
var r0 int
if rf, ok := ret.Get(0).(func() int); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int)
}
return r0
}
// IndexChannel provides a mock function with given fields: channel, userIDs, teamMemberIDs
func (_m *SearchEngineInterface) IndexChannel(channel *model.Channel, userIDs []string, teamMemberIDs []string) *model.AppError {
ret := _m.Called(channel, userIDs, teamMemberIDs)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(*model.Channel, []string, []string) *model.AppError); ok {
r0 = rf(channel, userIDs, teamMemberIDs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// IndexFile provides a mock function with given fields: file, channelId
func (_m *SearchEngineInterface) IndexFile(file *model.FileInfo, channelId string) *model.AppError {
ret := _m.Called(file, channelId)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(*model.FileInfo, string) *model.AppError); ok {
r0 = rf(file, channelId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// IndexPost provides a mock function with given fields: post, teamId
func (_m *SearchEngineInterface) IndexPost(post *model.Post, teamId string) *model.AppError {
ret := _m.Called(post, teamId)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(*model.Post, string) *model.AppError); ok {
r0 = rf(post, teamId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// IndexUser provides a mock function with given fields: user, teamsIds, channelsIds
func (_m *SearchEngineInterface) IndexUser(user *model.User, teamsIds []string, channelsIds []string) *model.AppError {
ret := _m.Called(user, teamsIds, channelsIds)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(*model.User, []string, []string) *model.AppError); ok {
r0 = rf(user, teamsIds, channelsIds)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// IsActive provides a mock function with given fields:
func (_m *SearchEngineInterface) IsActive() bool {
ret := _m.Called()
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// IsAutocompletionEnabled provides a mock function with given fields:
func (_m *SearchEngineInterface) IsAutocompletionEnabled() bool {
ret := _m.Called()
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// IsIndexingEnabled provides a mock function with given fields:
func (_m *SearchEngineInterface) IsIndexingEnabled() bool {
ret := _m.Called()
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// IsIndexingSync provides a mock function with given fields:
func (_m *SearchEngineInterface) IsIndexingSync() bool {
ret := _m.Called()
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// IsSearchEnabled provides a mock function with given fields:
func (_m *SearchEngineInterface) IsSearchEnabled() bool {
ret := _m.Called()
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// PurgeIndexes provides a mock function with given fields:
func (_m *SearchEngineInterface) PurgeIndexes() *model.AppError {
ret := _m.Called()
var r0 *model.AppError
if rf, ok := ret.Get(0).(func() *model.AppError); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// RefreshIndexes provides a mock function with given fields:
func (_m *SearchEngineInterface) RefreshIndexes() *model.AppError {
ret := _m.Called()
var r0 *model.AppError
if rf, ok := ret.Get(0).(func() *model.AppError); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// SearchChannels provides a mock function with given fields: teamId, userID, term, isGuest
func (_m *SearchEngineInterface) SearchChannels(teamId string, userID string, term string, isGuest bool) ([]string, *model.AppError) {
ret := _m.Called(teamId, userID, term, isGuest)
var r0 []string
if rf, ok := ret.Get(0).(func(string, string, string, bool) []string); ok {
r0 = rf(teamId, userID, term, isGuest)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(string, string, string, bool) *model.AppError); ok {
r1 = rf(teamId, userID, term, isGuest)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// SearchFiles provides a mock function with given fields: channels, searchParams, page, perPage
func (_m *SearchEngineInterface) SearchFiles(channels model.ChannelList, searchParams []*model.SearchParams, page int, perPage int) ([]string, *model.AppError) {
ret := _m.Called(channels, searchParams, page, perPage)
var r0 []string
if rf, ok := ret.Get(0).(func(model.ChannelList, []*model.SearchParams, int, int) []string); ok {
r0 = rf(channels, searchParams, page, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(model.ChannelList, []*model.SearchParams, int, int) *model.AppError); ok {
r1 = rf(channels, searchParams, page, perPage)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// SearchPosts provides a mock function with given fields: channels, searchParams, page, perPage
func (_m *SearchEngineInterface) SearchPosts(channels model.ChannelList, searchParams []*model.SearchParams, page int, perPage int) ([]string, model.PostSearchMatches, *model.AppError) {
ret := _m.Called(channels, searchParams, page, perPage)
var r0 []string
if rf, ok := ret.Get(0).(func(model.ChannelList, []*model.SearchParams, int, int) []string); ok {
r0 = rf(channels, searchParams, page, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 model.PostSearchMatches
if rf, ok := ret.Get(1).(func(model.ChannelList, []*model.SearchParams, int, int) model.PostSearchMatches); ok {
r1 = rf(channels, searchParams, page, perPage)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(model.PostSearchMatches)
}
}
var r2 *model.AppError
if rf, ok := ret.Get(2).(func(model.ChannelList, []*model.SearchParams, int, int) *model.AppError); ok {
r2 = rf(channels, searchParams, page, perPage)
} else {
if ret.Get(2) != nil {
r2 = ret.Get(2).(*model.AppError)
}
}
return r0, r1, r2
}
// SearchUsersInChannel provides a mock function with given fields: teamId, channelId, restrictedToChannels, term, options
func (_m *SearchEngineInterface) SearchUsersInChannel(teamId string, channelId string, restrictedToChannels []string, term string, options *model.UserSearchOptions) ([]string, []string, *model.AppError) {
ret := _m.Called(teamId, channelId, restrictedToChannels, term, options)
var r0 []string
if rf, ok := ret.Get(0).(func(string, string, []string, string, *model.UserSearchOptions) []string); ok {
r0 = rf(teamId, channelId, restrictedToChannels, term, options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 []string
if rf, ok := ret.Get(1).(func(string, string, []string, string, *model.UserSearchOptions) []string); ok {
r1 = rf(teamId, channelId, restrictedToChannels, term, options)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).([]string)
}
}
var r2 *model.AppError
if rf, ok := ret.Get(2).(func(string, string, []string, string, *model.UserSearchOptions) *model.AppError); ok {
r2 = rf(teamId, channelId, restrictedToChannels, term, options)
} else {
if ret.Get(2) != nil {
r2 = ret.Get(2).(*model.AppError)
}
}
return r0, r1, r2
}
// SearchUsersInTeam provides a mock function with given fields: teamId, restrictedToChannels, term, options
func (_m *SearchEngineInterface) SearchUsersInTeam(teamId string, restrictedToChannels []string, term string, options *model.UserSearchOptions) ([]string, *model.AppError) {
ret := _m.Called(teamId, restrictedToChannels, term, options)
var r0 []string
if rf, ok := ret.Get(0).(func(string, []string, string, *model.UserSearchOptions) []string); ok {
r0 = rf(teamId, restrictedToChannels, term, options)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(string, []string, string, *model.UserSearchOptions) *model.AppError); ok {
r1 = rf(teamId, restrictedToChannels, term, options)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// Start provides a mock function with given fields:
func (_m *SearchEngineInterface) Start() *model.AppError {
ret := _m.Called()
var r0 *model.AppError
if rf, ok := ret.Get(0).(func() *model.AppError); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// Stop provides a mock function with given fields:
func (_m *SearchEngineInterface) Stop() *model.AppError {
ret := _m.Called()
var r0 *model.AppError
if rf, ok := ret.Get(0).(func() *model.AppError); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// TestConfig provides a mock function with given fields: cfg
func (_m *SearchEngineInterface) TestConfig(cfg *model.Config) *model.AppError {
ret := _m.Called(cfg)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(*model.Config) *model.AppError); ok {
r0 = rf(cfg)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// UpdateConfig provides a mock function with given fields: cfg
func (_m *SearchEngineInterface) UpdateConfig(cfg *model.Config) {
_m.Called(cfg)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchengine
import (
"github.com/mattermost/mattermost-server/v6/model"
)
func NewBroker(cfg *model.Config) *Broker {
return &Broker{
cfg: cfg,
}
}
func (seb *Broker) RegisterElasticsearchEngine(es SearchEngineInterface) {
seb.ElasticsearchEngine = es
}
func (seb *Broker) RegisterBleveEngine(be SearchEngineInterface) {
seb.BleveEngine = be
}
type Broker struct {
cfg *model.Config
ElasticsearchEngine SearchEngineInterface
BleveEngine SearchEngineInterface
}
func (seb *Broker) UpdateConfig(cfg *model.Config) *model.AppError {
seb.cfg = cfg
if seb.ElasticsearchEngine != nil {
seb.ElasticsearchEngine.UpdateConfig(cfg)
}
if seb.BleveEngine != nil {
seb.BleveEngine.UpdateConfig(cfg)
}
return nil
}
func (seb *Broker) GetActiveEngines() []SearchEngineInterface {
engines := []SearchEngineInterface{}
if seb.ElasticsearchEngine != nil && seb.ElasticsearchEngine.IsActive() {
engines = append(engines, seb.ElasticsearchEngine)
}
if seb.BleveEngine != nil && seb.BleveEngine.IsActive() {
engines = append(engines, seb.BleveEngine)
}
return engines
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchengine
import (
"regexp"
"strings"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
var EmailRegex = regexp.MustCompile(`^[^\s"]+@[^\s"]+$`)
func GetSuggestionInputsSplitBy(term, splitStr string) []string {
splitTerm := strings.Split(strings.ToLower(term), splitStr)
var initialSuggestionList []string
for i := range splitTerm {
initialSuggestionList = append(initialSuggestionList, strings.Join(splitTerm[i:], splitStr))
}
suggestionList := []string{}
// If splitStr is not an empty space, we create a suggestion with it at the beginning
if splitStr == " " {
suggestionList = initialSuggestionList
} else {
for i, suggestion := range initialSuggestionList {
if i == 0 {
suggestionList = append(suggestionList, suggestion)
} else {
suggestionList = append(suggestionList, splitStr+suggestion, suggestion)
}
}
}
return suggestionList
}
func GetSuggestionInputsSplitByMultiple(term string, splitStrs []string) []string {
suggestionList := []string{}
for _, splitStr := range splitStrs {
suggestionList = append(suggestionList, GetSuggestionInputsSplitBy(term, splitStr)...)
}
return utils.RemoveDuplicatesFromStringArray(suggestionList)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sharedchannel
import (
"context"
"encoding/json"
"errors"
"fmt"
"sync"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// postsToAttachments returns the file attachments for a slice of posts that need to be synchronized.
func (scs *Service) shouldSyncAttachment(fi *model.FileInfo, rc *model.RemoteCluster) bool {
sca, err := scs.server.GetStore().SharedChannel().GetAttachment(fi.Id, rc.RemoteId)
if err != nil {
if _, ok := err.(errNotFound); !ok {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "error fetching shared channel attachment",
mlog.String("file_id", fi.Id),
mlog.String("remote_id", rc.RemoteId),
mlog.Err(err),
)
}
// no record so sync is needed
return true
}
return sca.LastSyncAt < fi.UpdateAt
}
// sendAttachmentForRemote asynchronously sends a file attachment to a remote cluster.
func (scs *Service) sendAttachmentForRemote(fi *model.FileInfo, post *model.Post, rc *model.RemoteCluster) error {
rcs := scs.server.GetRemoteClusterService()
if rcs == nil {
return fmt.Errorf("cannot update remote cluster for remote id %s; Remote Cluster Service not enabled", rc.RemoteId)
}
us := &model.UploadSession{
Id: model.NewId(),
Type: model.UploadTypeAttachment,
UserId: post.UserId,
ChannelId: post.ChannelId,
Filename: fi.Name,
FileSize: fi.Size,
RemoteId: rc.RemoteId,
ReqFileId: fi.Id,
}
payload, err := json.Marshal(us)
if err != nil {
return err
}
msg := model.NewRemoteClusterMsg(TopicUploadCreate, payload)
ctx, cancel := context.WithTimeout(context.Background(), remotecluster.SendTimeout)
defer cancel()
var usResp model.UploadSession
var respErr error
var wg sync.WaitGroup
wg.Add(1)
// creating the upload session on the remote server needs to be done synchronously.
err = rcs.SendMsg(ctx, msg, rc, func(msg model.RemoteClusterMsg, rc *model.RemoteCluster, resp *remotecluster.Response, err error) {
defer wg.Done()
if err != nil {
respErr = err
return
}
if !resp.IsSuccess() {
respErr = errors.New(resp.Err)
return
}
respErr = json.Unmarshal(resp.Payload, &usResp)
})
if err != nil {
return fmt.Errorf("error sending create upload session to remote %s for post %s: %w", rc.RemoteId, post.Id, err)
}
wg.Wait()
if respErr != nil {
return fmt.Errorf("invalid create upload session response for remote %s and post %s: %w", rc.RemoteId, post.Id, respErr)
}
ctx2, cancel2 := context.WithTimeout(context.Background(), remotecluster.SendFileTimeout)
defer cancel2()
return rcs.SendFile(ctx2, &usResp, fi, rc, scs.app, func(us *model.UploadSession, rc *model.RemoteCluster, resp *remotecluster.Response, err error) {
if err != nil {
return // this means the response could not be parsed; already logged
}
if !resp.IsSuccess() {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "send file failed",
mlog.String("remote", rc.DisplayName),
mlog.String("uploadId", usResp.Id),
mlog.String("err", resp.Err),
)
return
}
// response payload should be a model.FileInfo.
var fi model.FileInfo
if err2 := json.Unmarshal(resp.Payload, &fi); err2 != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "invalid file info response after send file",
mlog.String("remote", rc.DisplayName),
mlog.String("uploadId", usResp.Id),
mlog.Err(err2),
)
return
}
// save file attachment record in SharedChannelAttachments table
sca := &model.SharedChannelAttachment{
FileId: fi.Id,
RemoteId: rc.RemoteId,
}
if _, err2 := scs.server.GetStore().SharedChannel().UpsertAttachment(sca); err2 != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "error saving SharedChannelAttachment",
mlog.String("remote", rc.DisplayName),
mlog.String("uploadId", usResp.Id),
mlog.Err(err2),
)
return
}
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "send file successful",
mlog.String("remote", rc.DisplayName),
mlog.String("uploadId", usResp.Id),
)
})
}
// onReceiveUploadCreate is called when a message requesting to create an upload session is received. An upload session is
// created and the id returned in the response.
func (scs *Service) onReceiveUploadCreate(msg model.RemoteClusterMsg, rc *model.RemoteCluster, response *remotecluster.Response) error {
var us model.UploadSession
if err := json.Unmarshal(msg.Payload, &us); err != nil {
return fmt.Errorf("invalid upload session request: %w", err)
}
// make sure channel is shared for the remote sender
if _, err := scs.server.GetStore().SharedChannel().GetRemoteByIds(us.ChannelId, rc.RemoteId); err != nil {
return fmt.Errorf("could not validate upload session for remote: %w", err)
}
us.RemoteId = rc.RemoteId // don't let remotes try to impersonate each other
// create upload session.
usSaved, appErr := scs.app.CreateUploadSession(request.EmptyContext(scs.server.Log()), &us)
if appErr != nil {
return appErr
}
response.SetPayload(usSaved)
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sharedchannel
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// channelInviteMsg represents an invitation for a remote cluster to start sharing a channel.
type channelInviteMsg struct {
ChannelId string `json:"channel_id"`
TeamId string `json:"team_id"`
ReadOnly bool `json:"read_only"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
Header string `json:"header"`
Purpose string `json:"purpose"`
Type model.ChannelType `json:"type"`
DirectParticipantIDs []string `json:"direct_participant_ids"`
}
type InviteOption func(msg *channelInviteMsg)
func WithDirectParticipantID(participantID string) InviteOption {
return func(msg *channelInviteMsg) {
msg.DirectParticipantIDs = append(msg.DirectParticipantIDs, participantID)
}
}
// SendChannelInvite asynchronously sends a channel invite to a remote cluster. The remote cluster is
// expected to create a new channel with the same channel id, and respond with status OK.
// If an error occurs on the remote cluster then an ephemeral message is posted to in the channel for userId.
func (scs *Service) SendChannelInvite(channel *model.Channel, userId string, rc *model.RemoteCluster, options ...InviteOption) error {
rcs := scs.server.GetRemoteClusterService()
if rcs == nil {
return fmt.Errorf("cannot invite remote cluster for channel id %s; Remote Cluster Service not enabled", channel.Id)
}
sc, err := scs.server.GetStore().SharedChannel().Get(channel.Id)
if err != nil {
return err
}
invite := channelInviteMsg{
ChannelId: channel.Id,
TeamId: rc.RemoteTeamId,
ReadOnly: sc.ReadOnly,
Name: sc.ShareName,
DisplayName: sc.ShareDisplayName,
Header: sc.ShareHeader,
Purpose: sc.SharePurpose,
Type: channel.Type,
}
for _, option := range options {
option(&invite)
}
json, err := json.Marshal(invite)
if err != nil {
return err
}
msg := model.NewRemoteClusterMsg(TopicChannelInvite, json)
ctx, cancel := context.WithTimeout(context.Background(), remotecluster.SendTimeout)
defer cancel()
return rcs.SendMsg(ctx, msg, rc, func(msg model.RemoteClusterMsg, rc *model.RemoteCluster, resp *remotecluster.Response, err error) {
if err != nil || !resp.IsSuccess() {
scs.sendEphemeralPost(channel.Id, userId, fmt.Sprintf("Error sending channel invite for %s: %s", rc.DisplayName, combineErrors(err, resp.Err)))
return
}
scr := &model.SharedChannelRemote{
ChannelId: sc.ChannelId,
CreatorId: userId,
RemoteId: rc.RemoteId,
IsInviteAccepted: true,
IsInviteConfirmed: true,
}
if _, err = scs.server.GetStore().SharedChannel().SaveRemote(scr); err != nil {
scs.sendEphemeralPost(channel.Id, userId, fmt.Sprintf("Error confirming channel invite for %s: %v", rc.DisplayName, err))
return
}
scs.NotifyChannelChanged(sc.ChannelId)
scs.sendEphemeralPost(channel.Id, userId, fmt.Sprintf("`%s` has been added to channel.", rc.DisplayName))
})
}
func combineErrors(err error, serror string) string {
var sb strings.Builder
if err != nil {
sb.WriteString(err.Error())
}
if serror != "" {
if sb.Len() > 0 {
sb.WriteString("; ")
}
sb.WriteString(serror)
}
return sb.String()
}
func (scs *Service) onReceiveChannelInvite(msg model.RemoteClusterMsg, rc *model.RemoteCluster, _ *remotecluster.Response) error {
if len(msg.Payload) == 0 {
return nil
}
var invite channelInviteMsg
if err := json.Unmarshal(msg.Payload, &invite); err != nil {
return fmt.Errorf("invalid channel invite: %w", err)
}
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Channel invite received",
mlog.String("remote", rc.DisplayName),
mlog.String("channel_id", invite.ChannelId),
mlog.String("channel_name", invite.Name),
mlog.String("team_id", invite.TeamId),
)
// create channel if it doesn't exist; the channel may already exist, such as if it was shared then unshared at some point.
channel, err := scs.server.GetStore().Channel().Get(invite.ChannelId, true)
if err != nil {
if channel, err = scs.handleChannelCreation(invite, rc); err != nil {
return err
}
}
if invite.ReadOnly {
if err := scs.makeChannelReadOnly(channel); err != nil {
return fmt.Errorf("cannot make channel readonly `%s`: %w", invite.ChannelId, err)
}
}
sharedChannel := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
Home: false,
ReadOnly: invite.ReadOnly,
ShareName: channel.Name,
ShareDisplayName: channel.DisplayName,
SharePurpose: channel.Purpose,
ShareHeader: channel.Header,
CreatorId: rc.CreatorId,
RemoteId: rc.RemoteId,
Type: channel.Type,
}
if _, err := scs.server.GetStore().SharedChannel().Save(sharedChannel); err != nil {
scs.app.PermanentDeleteChannel(request.EmptyContext(scs.server.Log()), channel)
return fmt.Errorf("cannot create shared channel (channel_id=%s): %w", invite.ChannelId, err)
}
sharedChannelRemote := &model.SharedChannelRemote{
Id: model.NewId(),
ChannelId: channel.Id,
CreatorId: channel.CreatorId,
IsInviteAccepted: true,
IsInviteConfirmed: true,
RemoteId: rc.RemoteId,
}
if _, err := scs.server.GetStore().SharedChannel().SaveRemote(sharedChannelRemote); err != nil {
scs.app.PermanentDeleteChannel(request.EmptyContext(scs.server.Log()), channel)
scs.server.GetStore().SharedChannel().Delete(sharedChannel.ChannelId)
return fmt.Errorf("cannot create shared channel remote (channel_id=%s): %w", invite.ChannelId, err)
}
return nil
}
func (scs *Service) handleChannelCreation(invite channelInviteMsg, rc *model.RemoteCluster) (*model.Channel, error) {
if invite.Type == model.ChannelTypeDirect {
return scs.createDirectChannel(invite)
}
channelNew := &model.Channel{
Id: invite.ChannelId,
TeamId: invite.TeamId,
Type: invite.Type,
DisplayName: invite.DisplayName,
Name: invite.Name,
Header: invite.Header,
Purpose: invite.Purpose,
CreatorId: rc.CreatorId,
Shared: model.NewBool(true),
}
// check user perms?
channel, appErr := scs.app.CreateChannelWithUser(request.EmptyContext(scs.server.Log()), channelNew, rc.CreatorId)
if appErr != nil {
return nil, fmt.Errorf("cannot create channel `%s`: %w", invite.ChannelId, appErr)
}
return channel, nil
}
func (scs *Service) createDirectChannel(invite channelInviteMsg) (*model.Channel, error) {
if len(invite.DirectParticipantIDs) != 2 {
return nil, fmt.Errorf("cannot create direct channel `%s` insufficient participant count `%d`", invite.ChannelId, len(invite.DirectParticipantIDs))
}
channel, err := scs.app.GetOrCreateDirectChannel(request.EmptyContext(scs.server.Log()), invite.DirectParticipantIDs[0], invite.DirectParticipantIDs[1], model.WithID(invite.ChannelId))
if err != nil {
return nil, fmt.Errorf("cannot create direct channel `%s`: %w", invite.ChannelId, err)
}
return channel, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sharedchannel
import (
"encoding/json"
"github.com/mattermost/mattermost-server/v6/model"
)
// syncMsg represents a change in content (post add/edit/delete, reaction add/remove, users).
// It is sent to remote clusters as the payload of a `RemoteClusterMsg`.
type syncMsg struct {
Id string `json:"id"`
ChannelId string `json:"channel_id"`
Users map[string]*model.User `json:"users,omitempty"`
Posts []*model.Post `json:"posts,omitempty"`
Reactions []*model.Reaction `json:"reactions,omitempty"`
}
func newSyncMsg(channelID string) *syncMsg {
return &syncMsg{
Id: model.NewId(),
ChannelId: channelID,
}
}
func (sm *syncMsg) ToJSON() ([]byte, error) {
b, err := json.Marshal(sm)
if err != nil {
return nil, err
}
return b, nil
}
func (sm *syncMsg) String() string {
json, err := sm.ToJSON()
if err != nil {
return ""
}
return string(json)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sharedchannel
import (
"context"
"net/url"
"regexp"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var (
// Team name regex taken from model.IsValidTeamName
permaLinkRegex = regexp.MustCompile(`https?://[0-9.\-A-Za-z]+/[a-z0-9]+([a-z\-0-9]+|(__)?)[a-z0-9]+/pl/([a-zA-Z0-9]+)`)
permaLinkSharedRegex = regexp.MustCompile(`https?://[0-9.\-A-Za-z]+/[a-z0-9]+([a-z\-0-9]+|(__)?)[a-z0-9]+/plshared/([a-zA-Z0-9]+)`)
)
const (
permalinkMarker = "plshared"
)
// processPermalinkToRemote processes all permalinks going towards a remote site.
func (scs *Service) processPermalinkToRemote(p *model.Post) string {
var sent bool
return permaLinkRegex.ReplaceAllStringFunc(p.Message, func(msg string) string {
// Extract the postID (This is simple enough not to warrant full-blown URL parsing.)
lastSlash := strings.LastIndexByte(msg, '/')
postID := msg[lastSlash+1:]
opts := model.GetPostsOptions{
SkipFetchThreads: true,
}
postList, err := scs.server.GetStore().Post().Get(context.Background(), postID, opts, "", map[string]bool{})
if err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceWarn, "Unable to get post during replacing permalinks", mlog.Err(err))
return msg
}
if len(postList.Order) == 0 {
scs.server.Log().Log(mlog.LvlSharedChannelServiceWarn, "No post found for permalink", mlog.String("postID", postID))
return msg
}
// If postID is for a different channel
if postList.Posts[postList.Order[0]].ChannelId != p.ChannelId {
// Send ephemeral message to OP (only once per message).
if !sent {
scs.sendEphemeralPost(p.ChannelId, p.UserId, i18n.T("sharedchannel.permalink.not_found"))
sent = true
}
// But don't modify msg
return msg
}
// Otherwise, modify pl to plshared as a marker to be replaced by remote sites
return strings.Replace(msg, "/pl/", "/"+permalinkMarker+"/", 1)
})
}
// processPermalinkFromRemote processes all permalinks coming from a remote site.
func (scs *Service) processPermalinkFromRemote(p *model.Post, team *model.Team) string {
return permaLinkSharedRegex.ReplaceAllStringFunc(p.Message, func(remoteLink string) string {
// Extract host name
parsed, err := url.Parse(remoteLink)
if err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceWarn, "Unable to parse the remote link during replacing permalinks", mlog.Err(err))
return remoteLink
}
// Replace with local SiteURL
parsed.Scheme = scs.siteURL.Scheme
parsed.Host = scs.siteURL.Host
// Replace team name with local team
teamEnd := strings.Index(parsed.Path, "/"+permalinkMarker)
parsed.Path = "/" + team.Name + parsed.Path[teamEnd:]
// Replace plshared with pl
return strings.Replace(parsed.String(), "/"+permalinkMarker+"/", "/pl/", 1)
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sharedchannel
import (
"errors"
"fmt"
"net/url"
"sync"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
TopicSync = "sharedchannel_sync"
TopicChannelInvite = "sharedchannel_invite"
TopicUploadCreate = "sharedchannel_upload"
MaxRetries = 3
MaxPostsPerSync = 12 // a bit more than one typical screenfull of posts
MaxUsersPerSync = 25
NotifyRemoteOfflineThreshold = time.Second * 10
NotifyMinimumDelay = time.Second * 2
MaxUpsertRetries = 25
ProfileImageSyncTimeout = time.Second * 5
KeyRemoteUsername = "RemoteUsername"
KeyRemoteEmail = "RemoteEmail"
)
// Mocks can be re-generated with `make sharedchannel-mocks`.
type ServerIface interface {
Config() *model.Config
IsLeader() bool
AddClusterLeaderChangedListener(listener func()) string
RemoveClusterLeaderChangedListener(id string)
GetStore() store.Store
Log() *mlog.Logger
GetRemoteClusterService() remotecluster.RemoteClusterServiceIFace
}
type AppIface interface {
SendEphemeralPost(c request.CTX, userId string, post *model.Post) *model.Post
CreateChannelWithUser(c request.CTX, channel *model.Channel, userId string) (*model.Channel, *model.AppError)
GetOrCreateDirectChannel(c request.CTX, userId, otherUserId string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError)
AddUserToChannel(c request.CTX, user *model.User, channel *model.Channel, skipTeamMemberIntegrityCheck bool) (*model.ChannelMember, *model.AppError)
AddUserToTeamByTeamId(c *request.Context, teamId string, user *model.User) *model.AppError
PermanentDeleteChannel(c request.CTX, channel *model.Channel) *model.AppError
CreatePost(c request.CTX, post *model.Post, channel *model.Channel, triggerWebhooks bool, setOnline bool) (savedPost *model.Post, err *model.AppError)
UpdatePost(c *request.Context, post *model.Post, safeUpdate bool) (*model.Post, *model.AppError)
DeletePost(c request.CTX, postID, deleteByID string) (*model.Post, *model.AppError)
SaveReactionForPost(c *request.Context, reaction *model.Reaction) (*model.Reaction, *model.AppError)
DeleteReactionForPost(c *request.Context, reaction *model.Reaction) *model.AppError
PatchChannelModerationsForChannel(c request.CTX, channel *model.Channel, channelModerationsPatch []*model.ChannelModerationPatch) ([]*model.ChannelModeration, *model.AppError)
CreateUploadSession(c request.CTX, us *model.UploadSession) (*model.UploadSession, *model.AppError)
FileReader(path string) (filestore.ReadCloseSeeker, *model.AppError)
MentionsToTeamMembers(c request.CTX, message, teamID string) model.UserMentionMap
GetProfileImage(user *model.User) ([]byte, bool, *model.AppError)
InvalidateCacheForUser(userID string)
NotifySharedChannelUserUpdate(user *model.User)
}
// errNotFound allows checking against Store.ErrNotFound errors without making Store a dependency.
type errNotFound interface {
IsErrNotFound() bool
}
// errInvalidInput allows checking against Store.ErrInvalidInput errors without making Store a dependency.
type errInvalidInput interface {
InvalidInputInfo() (entity string, field string, value any)
}
// Service provides shared channel synchronization.
type Service struct {
server ServerIface
app AppIface
changeSignal chan struct{}
// everything below guarded by `mux`
mux sync.RWMutex
active bool
leaderListenerId string
connectionStateListenerId string
done chan struct{}
tasks map[string]syncTask
syncTopicListenerId string
inviteTopicListenerId string
uploadTopicListenerId string
siteURL *url.URL
}
// NewSharedChannelService creates a RemoteClusterService instance.
func NewSharedChannelService(server ServerIface, app AppIface) (*Service, error) {
service := &Service{
server: server,
app: app,
changeSignal: make(chan struct{}, 1),
tasks: make(map[string]syncTask),
}
parsed, err := url.Parse(*server.Config().ServiceSettings.SiteURL)
if err != nil {
return nil, fmt.Errorf("unable to parse SiteURL: %w", err)
}
service.siteURL = parsed
return service, nil
}
// Start is called by the server on server start-up.
func (scs *Service) Start() error {
rcs := scs.server.GetRemoteClusterService()
if rcs == nil {
return errors.New("Shared Channel Service cannot activate: requires Remote Cluster Service")
}
scs.mux.Lock()
scs.leaderListenerId = scs.server.AddClusterLeaderChangedListener(scs.onClusterLeaderChange)
scs.syncTopicListenerId = rcs.AddTopicListener(TopicSync, scs.onReceiveSyncMessage)
scs.inviteTopicListenerId = rcs.AddTopicListener(TopicChannelInvite, scs.onReceiveChannelInvite)
scs.uploadTopicListenerId = rcs.AddTopicListener(TopicUploadCreate, scs.onReceiveUploadCreate)
scs.connectionStateListenerId = rcs.AddConnectionStateListener(scs.onConnectionStateChange)
scs.mux.Unlock()
scs.onClusterLeaderChange()
return nil
}
// Shutdown is called by the server on server shutdown.
func (scs *Service) Shutdown() error {
rcs := scs.server.GetRemoteClusterService()
if rcs == nil {
return errors.New("Shared Channel Service cannot shutdown: requires Remote Cluster Service")
}
scs.mux.Lock()
id := scs.leaderListenerId
rcs.RemoveTopicListener(scs.syncTopicListenerId)
scs.syncTopicListenerId = ""
rcs.RemoveTopicListener(scs.inviteTopicListenerId)
scs.inviteTopicListenerId = ""
rcs.RemoveConnectionStateListener(scs.connectionStateListenerId)
scs.connectionStateListenerId = ""
scs.mux.Unlock()
scs.server.RemoveClusterLeaderChangedListener(id)
scs.pause()
return nil
}
// Active determines whether the service is active on the node or not.
func (scs *Service) Active() bool {
scs.mux.Lock()
defer scs.mux.Unlock()
return scs.active
}
func (scs *Service) sendEphemeralPost(channelId string, userId string, text string) {
ephemeral := &model.Post{
ChannelId: channelId,
Message: text,
CreateAt: model.GetMillis(),
}
scs.app.SendEphemeralPost(request.EmptyContext(scs.server.Log()), userId, ephemeral)
}
// onClusterLeaderChange is called whenever the cluster leader may have changed.
func (scs *Service) onClusterLeaderChange() {
if scs.server.IsLeader() {
scs.resume()
} else {
scs.pause()
}
}
func (scs *Service) resume() {
scs.mux.Lock()
defer scs.mux.Unlock()
if scs.active {
return // already active
}
scs.active = true
scs.done = make(chan struct{})
go scs.syncLoop(scs.done)
scs.server.Log().Debug("Shared Channel Service active")
}
func (scs *Service) pause() {
scs.mux.Lock()
defer scs.mux.Unlock()
if !scs.active {
return // already inactive
}
scs.active = false
close(scs.done)
scs.done = nil
scs.server.Log().Debug("Shared Channel Service inactive")
}
// Makes the remote channel to be read-only(announcement mode, only admins can create posts and reactions).
func (scs *Service) makeChannelReadOnly(channel *model.Channel) *model.AppError {
createPostPermission := model.ChannelModeratedPermissionsMap[model.PermissionCreatePost.Id]
createReactionPermission := model.ChannelModeratedPermissionsMap[model.PermissionAddReaction.Id]
updateMap := model.ChannelModeratedRolesPatch{
Guests: model.NewBool(false),
Members: model.NewBool(false),
}
readonlyChannelModerations := []*model.ChannelModerationPatch{
{
Name: &createPostPermission,
Roles: &updateMap,
},
{
Name: &createReactionPermission,
Roles: &updateMap,
},
}
_, err := scs.app.PatchChannelModerationsForChannel(request.EmptyContext(scs.server.Log()), channel, readonlyChannelModerations)
return err
}
// onConnectionStateChange is called whenever the connection state of a remote cluster changes,
// for example when one comes back online.
func (scs *Service) onConnectionStateChange(rc *model.RemoteCluster, online bool) {
if online {
// when a previously offline remote comes back online force a sync.
scs.ForceSyncForRemote(rc)
}
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Remote cluster connection status changed",
mlog.String("remote", rc.DisplayName),
mlog.String("remoteId", rc.RemoteId),
mlog.Bool("online", online),
)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sharedchannel
import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (scs *Service) onReceiveSyncMessage(msg model.RemoteClusterMsg, rc *model.RemoteCluster, response *remotecluster.Response) error {
if msg.Topic != TopicSync {
return fmt.Errorf("wrong topic, expected `%s`, got `%s`", TopicSync, msg.Topic)
}
if len(msg.Payload) == 0 {
return errors.New("empty sync message")
}
if scs.server.Log().IsLevelEnabled(mlog.LvlSharedChannelServiceMessagesInbound) {
scs.server.Log().Log(mlog.LvlSharedChannelServiceMessagesInbound, "inbound message",
mlog.String("remote", rc.DisplayName),
mlog.String("msg", string(msg.Payload)),
)
}
var sm syncMsg
if err := json.Unmarshal(msg.Payload, &sm); err != nil {
return fmt.Errorf("invalid sync message: %w", err)
}
return scs.processSyncMessage(request.EmptyContext(scs.server.Log()), &sm, rc, response)
}
func (scs *Service) processSyncMessage(c request.CTX, syncMsg *syncMsg, rc *model.RemoteCluster, response *remotecluster.Response) error {
var channel *model.Channel
var team *model.Team
var err error
syncResp := SyncResponse{
UserErrors: make([]string, 0),
UsersSyncd: make([]string, 0),
PostErrors: make([]string, 0),
ReactionErrors: make([]string, 0),
}
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Sync msg received",
mlog.String("remote", rc.Name),
mlog.String("channel_id", syncMsg.ChannelId),
mlog.Int("user_count", len(syncMsg.Users)),
mlog.Int("post_count", len(syncMsg.Posts)),
mlog.Int("reaction_count", len(syncMsg.Reactions)),
)
if channel, err = scs.server.GetStore().Channel().Get(syncMsg.ChannelId, true); err != nil {
// if the channel doesn't exist then none of these sync items are going to work.
return fmt.Errorf("channel not found processing sync message: %w", err)
}
// add/update users before posts
for _, user := range syncMsg.Users {
if userSaved, err := scs.upsertSyncUser(c, user, channel, rc); err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error upserting sync user",
mlog.String("remote", rc.Name),
mlog.String("channel_id", syncMsg.ChannelId),
mlog.String("user_id", user.Id),
mlog.Err(err))
} else {
syncResp.UsersSyncd = append(syncResp.UsersSyncd, userSaved.Id)
if syncResp.UsersLastUpdateAt < user.UpdateAt {
syncResp.UsersLastUpdateAt = user.UpdateAt
}
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "User upserted via sync",
mlog.String("remote", rc.Name),
mlog.String("channel_id", syncMsg.ChannelId),
mlog.String("user_id", user.Id),
)
}
}
for _, post := range syncMsg.Posts {
if syncMsg.ChannelId != post.ChannelId {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "ChannelId mismatch",
mlog.String("remote", rc.Name),
mlog.String("sm.ChannelId", syncMsg.ChannelId),
mlog.String("sm.Post.ChannelId", post.ChannelId),
mlog.String("PostId", post.Id),
)
syncResp.PostErrors = append(syncResp.PostErrors, post.Id)
continue
}
if channel.Type != model.ChannelTypeDirect && team == nil {
var err2 error
team, err2 = scs.server.GetStore().Channel().GetTeamForChannel(syncMsg.ChannelId)
if err2 != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error getting Team for Channel",
mlog.String("ChannelId", post.ChannelId),
mlog.String("PostId", post.Id),
mlog.String("remote", rc.Name),
mlog.Err(err2),
)
syncResp.PostErrors = append(syncResp.PostErrors, post.Id)
continue
}
}
// process perma-links for remote
if team != nil {
post.Message = scs.processPermalinkFromRemote(post, team)
}
// add/update post
rpost, err := scs.upsertSyncPost(post, channel, rc)
if err != nil {
syncResp.PostErrors = append(syncResp.PostErrors, post.Id)
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error upserting sync post",
mlog.String("post_id", post.Id),
mlog.String("channel_id", post.ChannelId),
mlog.String("remote", rc.Name),
mlog.Err(err),
)
} else if syncResp.PostsLastUpdateAt < rpost.UpdateAt {
syncResp.PostsLastUpdateAt = rpost.UpdateAt
}
}
// add/remove reactions
for _, reaction := range syncMsg.Reactions {
if _, err := scs.upsertSyncReaction(reaction, rc); err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error upserting sync reaction",
mlog.String("remote", rc.Name),
mlog.String("user_id", reaction.UserId),
mlog.String("post_id", reaction.PostId),
mlog.String("emoji", reaction.EmojiName),
mlog.Int64("delete_at", reaction.DeleteAt),
mlog.Err(err),
)
} else {
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Reaction upserted via sync",
mlog.String("remote", rc.Name),
mlog.String("user_id", reaction.UserId),
mlog.String("post_id", reaction.PostId),
mlog.String("emoji", reaction.EmojiName),
mlog.Int64("delete_at", reaction.DeleteAt),
)
if syncResp.ReactionsLastUpdateAt < reaction.UpdateAt {
syncResp.ReactionsLastUpdateAt = reaction.UpdateAt
}
}
}
response.SetPayload(syncResp)
return nil
}
func (scs *Service) upsertSyncUser(c request.CTX, user *model.User, channel *model.Channel, rc *model.RemoteCluster) (*model.User, error) {
var err error
if user.RemoteId == nil || *user.RemoteId == "" {
user.RemoteId = model.NewString(rc.RemoteId)
}
// Check if user already exists
euser, err := scs.server.GetStore().User().Get(context.Background(), user.Id)
if err != nil {
if _, ok := err.(errNotFound); !ok {
return nil, fmt.Errorf("error checking sync user: %w", err)
}
}
var userSaved *model.User
if euser == nil {
if userSaved, err = scs.insertSyncUser(user, channel, rc); err != nil {
return nil, err
}
} else {
patch := &model.UserPatch{
Username: &user.Username,
Nickname: &user.Nickname,
FirstName: &user.FirstName,
LastName: &user.LastName,
Email: &user.Email,
Props: user.Props,
Position: &user.Position,
Locale: &user.Locale,
Timezone: user.Timezone,
RemoteId: user.RemoteId,
}
if userSaved, err = scs.updateSyncUser(patch, euser, channel, rc); err != nil {
return nil, err
}
}
// Add user to team. We do this here regardless of whether the user was
// just created or patched since there are three steps to adding a user
// (insert rec, add to team, add to channel) and any one could fail.
// Instead of undoing what succeeded on any failure we simply do all steps each
// time. AddUserToChannel & AddUserToTeamByTeamId do not error if user was already
// added and exit quickly.
if err := scs.app.AddUserToTeamByTeamId(request.EmptyContext(scs.server.Log()), channel.TeamId, userSaved); err != nil {
return nil, fmt.Errorf("error adding sync user to Team: %w", err)
}
// add user to channel
if _, err := scs.app.AddUserToChannel(c, userSaved, channel, false); err != nil {
return nil, fmt.Errorf("error adding sync user to ChannelMembers: %w", err)
}
return userSaved, nil
}
func (scs *Service) insertSyncUser(user *model.User, channel *model.Channel, rc *model.RemoteCluster) (*model.User, error) {
var err error
var userSaved *model.User
var suffix string
// ensure the new user is created with system_user role and random password.
user = sanitizeUserForSync(user)
// save the original username and email in props (if not already done by another remote)
if _, ok := user.GetProp(KeyRemoteUsername); !ok {
user.SetProp(KeyRemoteUsername, user.Username)
}
if _, ok := user.GetProp(KeyRemoteEmail); !ok {
user.SetProp(KeyRemoteEmail, user.Email)
}
// Apply a suffix to the username until it is unique. Collisions will be quite
// rare since we are joining a username that is unique at a remote site with a unique
// name for that site. However we need to truncate the combined name to 64 chars and
// that might introduce a collision.
for i := 1; i <= MaxUpsertRetries; i++ {
if i > 1 {
suffix = strconv.FormatInt(int64(i), 10)
}
user.Username = mungUsername(user.Username, rc.Name, suffix, model.UserNameMaxLength)
user.Email = mungEmail(rc.Name, model.UserEmailMaxLength)
if userSaved, err = scs.server.GetStore().User().Save(user); err != nil {
e, ok := err.(errInvalidInput)
if !ok {
break
}
_, field, value := e.InvalidInputInfo()
if field == "email" || field == "username" {
// username or email collision; try again with different suffix
scs.server.Log().Log(mlog.LvlSharedChannelServiceWarn, "Collision inserting sync user",
mlog.String("field", field),
mlog.Any("value", value),
mlog.Int("attempt", i),
mlog.Err(err),
)
}
} else {
scs.app.NotifySharedChannelUserUpdate(userSaved)
return userSaved, nil
}
}
return nil, fmt.Errorf("error inserting sync user %s: %w", user.Id, err)
}
func (scs *Service) updateSyncUser(patch *model.UserPatch, user *model.User, channel *model.Channel, rc *model.RemoteCluster) (*model.User, error) {
var err error
var update *model.UserUpdate
var suffix string
// preserve existing real username/email since Patch will over-write them;
// the real username/email in props can be updated if they don't contain colons,
// meaning the update is coming from the user's origin server (not munged).
realUsername, _ := user.GetProp(KeyRemoteUsername)
realEmail, _ := user.GetProp(KeyRemoteEmail)
if patch.Username != nil && !strings.Contains(*patch.Username, ":") {
realUsername = *patch.Username
}
if patch.Email != nil && !strings.Contains(*patch.Email, ":") {
realEmail = *patch.Email
}
user.Patch(patch)
user = sanitizeUserForSync(user)
user.SetProp(KeyRemoteUsername, realUsername)
user.SetProp(KeyRemoteEmail, realEmail)
// Apply a suffix to the username until it is unique.
for i := 1; i <= MaxUpsertRetries; i++ {
if i > 1 {
suffix = strconv.FormatInt(int64(i), 10)
}
user.Username = mungUsername(user.Username, rc.Name, suffix, model.UserNameMaxLength)
user.Email = mungEmail(rc.Name, model.UserEmailMaxLength)
if update, err = scs.server.GetStore().User().Update(user, false); err != nil {
e, ok := err.(errInvalidInput)
if !ok {
break
}
_, field, value := e.InvalidInputInfo()
if field == "email" || field == "username" {
// username or email collision; try again with different suffix
scs.server.Log().Log(mlog.LvlSharedChannelServiceWarn, "Collision updating sync user",
mlog.String("field", field),
mlog.Any("value", value),
mlog.Int("attempt", i),
mlog.Err(err),
)
}
} else {
scs.app.InvalidateCacheForUser(update.New.Id)
scs.app.NotifySharedChannelUserUpdate(update.New)
return update.New, nil
}
}
return nil, fmt.Errorf("error updating sync user %s: %w", user.Id, err)
}
func (scs *Service) upsertSyncPost(post *model.Post, channel *model.Channel, rc *model.RemoteCluster) (*model.Post, error) {
var appErr *model.AppError
post.RemoteId = model.NewString(rc.RemoteId)
rpost, err := scs.server.GetStore().Post().GetSingle(post.Id, true)
if err != nil {
if _, ok := err.(errNotFound); !ok {
return nil, fmt.Errorf("error checking sync post: %w", err)
}
}
if rpost == nil {
// post doesn't exist; create new one
rpost, appErr = scs.app.CreatePost(request.EmptyContext(scs.server.Log()), post, channel, true, true)
if appErr == nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Created sync post",
mlog.String("post_id", post.Id),
mlog.String("channel_id", post.ChannelId),
)
}
} else if post.DeleteAt > 0 {
// delete post
rpost, appErr = scs.app.DeletePost(request.EmptyContext(scs.server.Log()), post.Id, post.UserId)
if appErr == nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Deleted sync post",
mlog.String("post_id", post.Id),
mlog.String("channel_id", post.ChannelId),
)
}
} else if post.EditAt > rpost.EditAt || post.Message != rpost.Message {
// update post
rpost, appErr = scs.app.UpdatePost(request.EmptyContext(scs.server.Log()), post, false)
if appErr == nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Updated sync post",
mlog.String("post_id", post.Id),
mlog.String("channel_id", post.ChannelId),
)
}
} else {
// nothing to update
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Update to sync post ignored",
mlog.String("post_id", post.Id),
mlog.String("channel_id", post.ChannelId),
)
}
var rerr error
if appErr != nil {
rerr = errors.New(appErr.Error())
}
return rpost, rerr
}
func (scs *Service) upsertSyncReaction(reaction *model.Reaction, rc *model.RemoteCluster) (*model.Reaction, error) {
savedReaction := reaction
var appErr *model.AppError
reaction.RemoteId = model.NewString(rc.RemoteId)
if reaction.DeleteAt == 0 {
savedReaction, appErr = scs.app.SaveReactionForPost(request.EmptyContext(scs.server.Log()), reaction)
} else {
appErr = scs.app.DeleteReactionForPost(request.EmptyContext(scs.server.Log()), reaction)
}
var err error
if appErr != nil {
err = errors.New(appErr.Error())
}
return savedReaction, err
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sharedchannel
import (
"context"
"fmt"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type syncTask struct {
id string
channelID string
remoteID string
AddedAt time.Time
retryCount int
retryMsg *syncMsg
schedule time.Time
}
func newSyncTask(channelID string, remoteID string, retryMsg *syncMsg) syncTask {
var retryID string
if retryMsg != nil {
retryID = retryMsg.Id
}
return syncTask{
id: channelID + remoteID + retryID, // combination of ids to avoid duplicates
channelID: channelID,
remoteID: remoteID, // empty means update all remote clusters
retryMsg: retryMsg,
schedule: time.Now(),
}
}
// incRetry increments the retry counter and returns true if MaxRetries not exceeded.
func (st *syncTask) incRetry() bool {
st.retryCount++
return st.retryCount <= MaxRetries
}
// NotifyChannelChanged is called to indicate that a shared channel has been modified,
// thus triggering an update to all remote clusters.
func (scs *Service) NotifyChannelChanged(channelID string) {
if rcs := scs.server.GetRemoteClusterService(); rcs == nil {
return
}
task := newSyncTask(channelID, "", nil)
task.schedule = time.Now().Add(NotifyMinimumDelay)
scs.addTask(task)
}
// NotifyUserProfileChanged is called to indicate that a user belonging to at least one
// shared channel has modified their user profile (name, username, email, custom status, profile image)
func (scs *Service) NotifyUserProfileChanged(userID string) {
if rcs := scs.server.GetRemoteClusterService(); rcs == nil {
return
}
scusers, err := scs.server.GetStore().SharedChannel().GetUsersForUser(userID)
if err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to fetch shared channel users",
mlog.String("userID", userID),
mlog.Err(err),
)
return
}
if len(scusers) == 0 {
return
}
notified := make(map[string]struct{})
for _, user := range scusers {
// update every channel + remote combination they belong to.
// Redundant updates (ie. to same remote for multiple channels) will be
// filtered out.
combo := user.ChannelId + user.RemoteId
if _, ok := notified[combo]; ok {
continue
}
notified[combo] = struct{}{}
task := newSyncTask(user.ChannelId, user.RemoteId, nil)
task.schedule = time.Now().Add(NotifyMinimumDelay)
scs.addTask(task)
}
}
// ForceSyncForRemote causes all channels shared with the remote to be synchronized.
func (scs *Service) ForceSyncForRemote(rc *model.RemoteCluster) {
if rcs := scs.server.GetRemoteClusterService(); rcs == nil {
return
}
// fetch all channels shared with this remote.
opts := model.SharedChannelRemoteFilterOpts{
RemoteId: rc.RemoteId,
}
scrs, err := scs.server.GetStore().SharedChannel().GetRemotes(opts)
if err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to fetch shared channel remotes",
mlog.String("remote", rc.DisplayName),
mlog.String("remoteId", rc.RemoteId),
mlog.Err(err),
)
return
}
for _, scr := range scrs {
task := newSyncTask(scr.ChannelId, rc.RemoteId, nil)
task.schedule = time.Now().Add(NotifyMinimumDelay)
scs.addTask(task)
}
}
// addTask adds or re-adds a task to the queue.
func (scs *Service) addTask(task syncTask) {
task.AddedAt = time.Now()
scs.mux.Lock()
if _, ok := scs.tasks[task.id]; !ok {
scs.tasks[task.id] = task
}
scs.mux.Unlock()
// wake up the sync goroutine
select {
case scs.changeSignal <- struct{}{}:
default:
// that's ok, the sync routine is already busy
}
}
// syncLoop is called via a dedicated goroutine to wait for notifications of channel changes and
// updates each remote based on those changes.
func (scs *Service) syncLoop(done chan struct{}) {
// create a timer to periodically check the task queue, but only if there is
// a delayed task in the queue.
delay := time.NewTimer(NotifyMinimumDelay)
defer stopTimer(delay)
// wait for channel changed signal and update for oldest task.
for {
select {
case <-scs.changeSignal:
if wait := scs.doSync(); wait > 0 {
stopTimer(delay)
delay.Reset(wait)
}
case <-delay.C:
if wait := scs.doSync(); wait > 0 {
delay.Reset(wait)
}
case <-done:
return
}
}
}
func stopTimer(timer *time.Timer) {
timer.Stop()
select {
case <-timer.C:
default:
}
}
// doSync checks the task queue for any tasks to be processed and processes all that are ready.
// If any delayed tasks remain in queue then the duration until the next scheduled task is returned.
func (scs *Service) doSync() time.Duration {
var task syncTask
var ok bool
var shortestWait time.Duration
for {
task, ok, shortestWait = scs.removeOldestTask()
if !ok {
break
}
if err := scs.processTask(task); err != nil {
// put task back into map so it will update again
if task.incRetry() {
scs.addTask(task)
} else {
scs.server.Log().Error("Failed to synchronize shared channel",
mlog.String("channelId", task.channelID),
mlog.String("remoteId", task.remoteID),
mlog.Err(err),
)
}
}
}
return shortestWait
}
// removeOldestTask removes and returns the oldest task in the task map.
// A task coming in via NotifyChannelChanged must stay in queue for at least
// `NotifyMinimumDelay` to ensure we don't go nuts trying to sync during a bulk update.
// If no tasks are available then false is returned.
func (scs *Service) removeOldestTask() (syncTask, bool, time.Duration) {
scs.mux.Lock()
defer scs.mux.Unlock()
var oldestTask syncTask
var oldestKey string
var shortestWait time.Duration
for key, task := range scs.tasks {
// check if task is ready
if wait := time.Until(task.schedule); wait > 0 {
if wait < shortestWait || shortestWait == 0 {
shortestWait = wait
}
continue
}
// task is ready; check if it's the oldest ready task
if task.AddedAt.Before(oldestTask.AddedAt) || oldestTask.AddedAt.IsZero() {
oldestKey = key
oldestTask = task
}
}
if oldestKey != "" {
delete(scs.tasks, oldestKey)
return oldestTask, true, shortestWait
}
return oldestTask, false, shortestWait
}
// processTask updates one or more remote clusters with any new channel content.
func (scs *Service) processTask(task syncTask) error {
var err error
var remotes []*model.RemoteCluster
if task.remoteID == "" {
filter := model.RemoteClusterQueryFilter{
InChannel: task.channelID,
OnlyConfirmed: true,
}
remotes, err = scs.server.GetStore().RemoteCluster().GetAll(filter)
if err != nil {
return err
}
} else {
rc, err := scs.server.GetStore().RemoteCluster().Get(task.remoteID)
if err != nil {
return err
}
if !rc.IsOnline() {
return fmt.Errorf("Failed updating shared channel '%s' for offline remote cluster '%s'", task.channelID, rc.DisplayName)
}
remotes = []*model.RemoteCluster{rc}
}
for _, rc := range remotes {
rtask := task
rtask.remoteID = rc.RemoteId
if err := scs.syncForRemote(rtask, rc); err != nil {
// retry...
if rtask.incRetry() {
scs.addTask(rtask)
} else {
scs.server.Log().Error("Failed to synchronize shared channel for remote cluster",
mlog.String("channelId", rtask.channelID),
mlog.String("remote", rc.DisplayName),
mlog.Err(err),
)
}
}
}
return nil
}
func (scs *Service) handlePostError(postId string, task syncTask, rc *model.RemoteCluster) {
if task.retryMsg != nil && len(task.retryMsg.Posts) == 1 && task.retryMsg.Posts[0].Id == postId {
// this was a retry for specific post that failed previously. Try again if within MaxRetries.
if task.incRetry() {
scs.addTask(task)
} else {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "error syncing post",
mlog.String("remote", rc.DisplayName),
mlog.String("post_id", postId),
)
}
return
}
// this post failed as part of a group of posts. Retry as an individual post.
post, err := scs.server.GetStore().Post().GetSingle(postId, true)
if err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "error fetching post for sync retry",
mlog.String("remote", rc.DisplayName),
mlog.String("post_id", postId),
)
return
}
syncMsg := newSyncMsg(task.channelID)
syncMsg.Posts = []*model.Post{post}
scs.addTask(newSyncTask(task.channelID, task.remoteID, syncMsg))
}
// notifyRemoteOffline creates an ephemeral post to the author for any posts created recently to remotes
// that are offline.
func (scs *Service) notifyRemoteOffline(posts []*model.Post, rc *model.RemoteCluster) {
// only send one ephemeral post per author.
notified := make(map[string]bool)
// range the slice in reverse so the newest posts are visited first; this ensures an ephemeral
// get added where it is mostly likely to be seen.
for i := len(posts) - 1; i >= 0; i-- {
post := posts[i]
if didNotify := notified[post.UserId]; didNotify {
continue
}
postCreateAt := model.GetTimeForMillis(post.CreateAt)
if post.DeleteAt == 0 && post.UserId != "" && time.Since(postCreateAt) < NotifyRemoteOfflineThreshold {
T := scs.getUserTranslations(post.UserId)
ephemeral := &model.Post{
ChannelId: post.ChannelId,
Message: T("sharedchannel.cannot_deliver_post", map[string]any{"Remote": rc.DisplayName}),
CreateAt: post.CreateAt + 1,
}
scs.app.SendEphemeralPost(request.EmptyContext(scs.server.Log()), post.UserId, ephemeral)
notified[post.UserId] = true
}
}
}
func (scs *Service) updateCursorForRemote(scrId string, rc *model.RemoteCluster, cursor model.GetPostsSinceForSyncCursor) {
if err := scs.server.GetStore().SharedChannel().UpdateRemoteCursor(scrId, cursor); err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "error updating cursor for shared channel remote",
mlog.String("remote", rc.DisplayName),
mlog.Err(err),
)
return
}
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "updated cursor for remote",
mlog.String("remote_id", rc.RemoteId),
mlog.String("remote", rc.DisplayName),
mlog.Int64("last_post_update_at", cursor.LastPostUpdateAt),
mlog.String("last_post_id", cursor.LastPostId),
)
}
func (scs *Service) getUserTranslations(userId string) i18n.TranslateFunc {
var locale string
user, err := scs.server.GetStore().User().Get(context.Background(), userId)
if err == nil {
locale = user.Locale
}
if locale == "" {
locale = model.DefaultLocale
}
return i18n.GetUserTranslations(locale)
}
// shouldUserSync determines if a user needs to be synchronized.
// User should be synchronized if it has no entry in the SharedChannelUsers table for the specified channel,
// or there is an entry but the LastSyncAt is less than user.UpdateAt
func (scs *Service) shouldUserSync(user *model.User, channelID string, rc *model.RemoteCluster) (sync bool, syncImage bool, err error) {
// don't sync users with the remote they originated from.
if user.RemoteId != nil && *user.RemoteId == rc.RemoteId {
return false, false, nil
}
scu, err := scs.server.GetStore().SharedChannel().GetSingleUser(user.Id, channelID, rc.RemoteId)
if err != nil {
if _, ok := err.(errNotFound); !ok {
return false, false, err
}
// user not in the SharedChannelUsers table, so we must add them.
scu = &model.SharedChannelUser{
UserId: user.Id,
RemoteId: rc.RemoteId,
ChannelId: channelID,
}
if _, err = scs.server.GetStore().SharedChannel().SaveUser(scu); err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error adding user to shared channel users",
mlog.String("remote_id", rc.RemoteId),
mlog.String("user_id", user.Id),
mlog.String("channel_id", user.Id),
mlog.Err(err),
)
}
return true, true, nil
}
return user.UpdateAt > scu.LastSyncAt, user.LastPictureUpdate > scu.LastSyncAt, nil
}
func (scs *Service) syncProfileImage(user *model.User, channelID string, rc *model.RemoteCluster) {
rcs := scs.server.GetRemoteClusterService()
if rcs == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), ProfileImageSyncTimeout)
defer cancel()
rcs.SendProfileImage(ctx, user.Id, rc, scs.app, func(userId string, rc *model.RemoteCluster, resp *remotecluster.Response, err error) {
if resp.IsSuccess() {
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Users profile image synchronized",
mlog.String("remote_id", rc.RemoteId),
mlog.String("user_id", user.Id),
)
if err2 := scs.server.GetStore().SharedChannel().UpdateUserLastSyncAt(user.Id, channelID, rc.RemoteId); err2 != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error updating users LastSyncTime after profile image update",
mlog.String("remote_id", rc.RemoteId),
mlog.String("user_id", user.Id),
mlog.Err(err2),
)
}
return
}
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error synchronizing users profile image",
mlog.String("remote_id", rc.RemoteId),
mlog.String("user_id", user.Id),
mlog.Err(err),
)
})
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sharedchannel
import (
"context"
"encoding/json"
"fmt"
"sync"
"github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type sendSyncMsgResultFunc func(syncResp SyncResponse, err error)
type attachment struct {
fi *model.FileInfo
post *model.Post
}
type syncData struct {
task syncTask
rc *model.RemoteCluster
scr *model.SharedChannelRemote
users map[string]*model.User
profileImages map[string]*model.User
posts []*model.Post
reactions []*model.Reaction
attachments []attachment
resultRepeat bool
resultNextCursor model.GetPostsSinceForSyncCursor
}
func newSyncData(task syncTask, rc *model.RemoteCluster, scr *model.SharedChannelRemote) *syncData {
return &syncData{
task: task,
rc: rc,
scr: scr,
users: make(map[string]*model.User),
profileImages: make(map[string]*model.User),
resultNextCursor: model.GetPostsSinceForSyncCursor{LastPostUpdateAt: scr.LastPostUpdateAt, LastPostId: scr.LastPostId},
}
}
func (sd *syncData) isEmpty() bool {
return len(sd.users) == 0 && len(sd.profileImages) == 0 && len(sd.posts) == 0 && len(sd.reactions) == 0 && len(sd.attachments) == 0
}
func (sd *syncData) isCursorChanged() bool {
return sd.scr.LastPostUpdateAt != sd.resultNextCursor.LastPostUpdateAt || sd.scr.LastPostId != sd.resultNextCursor.LastPostId
}
// syncForRemote updates a remote cluster with any new posts/reactions for a specific
// channel. If many changes are found, only the oldest X changes are sent and the channel
// is re-added to the task map. This ensures no channels are starved for updates even if some
// channels are very active.
// Returning an error forces a retry on the task.
func (scs *Service) syncForRemote(task syncTask, rc *model.RemoteCluster) error {
rcs := scs.server.GetRemoteClusterService()
if rcs == nil {
return fmt.Errorf("cannot update remote cluster %s for channel id %s; Remote Cluster Service not enabled", rc.Name, task.channelID)
}
scr, err := scs.server.GetStore().SharedChannel().GetRemoteByIds(task.channelID, rc.RemoteId)
if err != nil {
return err
}
// if this is retrying a failed msg, just send it again.
if task.retryMsg != nil {
sd := newSyncData(task, rc, scr)
sd.users = task.retryMsg.Users
sd.posts = task.retryMsg.Posts
sd.reactions = task.retryMsg.Reactions
return scs.sendSyncData(sd)
}
sd := newSyncData(task, rc, scr)
// schedule another sync if the repeat flag is set at some point.
defer func(rpt *bool) {
if *rpt {
scs.addTask(newSyncTask(task.channelID, task.remoteID, nil))
}
}(&sd.resultRepeat)
// fetch new posts or retry post.
if err := scs.fetchPostsForSync(sd); err != nil {
return fmt.Errorf("cannot fetch posts for sync %v: %w", sd, err)
}
if !rc.IsOnline() {
if len(sd.posts) != 0 {
scs.notifyRemoteOffline(sd.posts, rc)
}
sd.resultRepeat = false
return nil
}
// fetch users that have updated their user profile or image.
if err := scs.fetchUsersForSync(sd); err != nil {
return fmt.Errorf("cannot fetch users for sync %v: %w", sd, err)
}
// fetch reactions for posts
if err := scs.fetchReactionsForSync(sd); err != nil {
return fmt.Errorf("cannot fetch reactions for sync %v: %w", sd, err)
}
// fetch users associated with posts & reactions
if err := scs.fetchPostUsersForSync(sd); err != nil {
return fmt.Errorf("cannot fetch post users for sync %v: %w", sd, err)
}
// filter out any posts that don't need to be sent.
scs.filterPostsForSync(sd)
// fetch attachments for posts
if err := scs.fetchPostAttachmentsForSync(sd); err != nil {
return fmt.Errorf("cannot fetch post attachments for sync %v: %w", sd, err)
}
if sd.isEmpty() {
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Not sending sync data; everything filtered out",
mlog.String("remote", rc.DisplayName),
mlog.String("channel_id", task.channelID),
mlog.Bool("repeat", sd.resultRepeat),
)
if sd.isCursorChanged() {
scs.updateCursorForRemote(sd.scr.Id, sd.rc, sd.resultNextCursor)
}
return nil
}
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Sending sync data",
mlog.String("remote", rc.DisplayName),
mlog.String("channel_id", task.channelID),
mlog.Bool("repeat", sd.resultRepeat),
mlog.Int("users", len(sd.users)),
mlog.Int("images", len(sd.profileImages)),
mlog.Int("posts", len(sd.posts)),
mlog.Int("reactions", len(sd.reactions)),
mlog.Int("attachments", len(sd.attachments)),
)
return scs.sendSyncData(sd)
}
// fetchUsersForSync populates the sync data with any channel users who updated their user profile
// since the last sync.
func (scs *Service) fetchUsersForSync(sd *syncData) error {
filter := model.GetUsersForSyncFilter{
ChannelID: sd.task.channelID,
Limit: MaxUsersPerSync,
}
users, err := scs.server.GetStore().SharedChannel().GetUsersForSync(filter)
if err != nil {
return err
}
for _, u := range users {
if u.GetRemoteID() != sd.rc.RemoteId {
sd.users[u.Id] = u
}
}
filter.CheckProfileImage = true
usersImage, err := scs.server.GetStore().SharedChannel().GetUsersForSync(filter)
if err != nil {
return err
}
for _, u := range usersImage {
if u.GetRemoteID() != sd.rc.RemoteId {
sd.profileImages[u.Id] = u
}
}
return nil
}
// fetchPostsForSync populates the sync data with any new posts since the last sync.
func (scs *Service) fetchPostsForSync(sd *syncData) error {
options := model.GetPostsSinceForSyncOptions{
ChannelId: sd.task.channelID,
IncludeDeleted: true,
}
cursor := model.GetPostsSinceForSyncCursor{
LastPostUpdateAt: sd.scr.LastPostUpdateAt,
LastPostId: sd.scr.LastPostId,
}
posts, nextCursor, err := scs.server.GetStore().Post().GetPostsSinceForSync(options, cursor, MaxPostsPerSync)
if err != nil {
return fmt.Errorf("could not fetch new posts for sync: %w", err)
}
// Append the posts individually, checking for root posts that might appear later in the list.
// This is due to the UpdateAt collision handling algorithm where the order of posts is not based
// on UpdateAt or CreateAt when the posts have the same UpdateAt value. Here we are guarding
// against a root post with the same UpdateAt (and probably the same CreateAt) appearing later
// in the list and must be sync'd before the child post. This is and edge case that likely only
// happens during load testing or bulk imports.
for _, p := range posts {
if p.RootId != "" {
root, err := scs.server.GetStore().Post().GetSingle(p.RootId, true)
if err == nil {
if (root.CreateAt >= cursor.LastPostUpdateAt || root.UpdateAt >= cursor.LastPostUpdateAt) && !containsPost(sd.posts, root) {
sd.posts = append(sd.posts, root)
}
}
}
sd.posts = append(sd.posts, p)
}
sd.resultNextCursor = nextCursor
sd.resultRepeat = len(posts) == MaxPostsPerSync
return nil
}
func containsPost(posts []*model.Post, post *model.Post) bool {
for _, p := range posts {
if p.Id == post.Id {
return true
}
}
return false
}
// fetchReactionsForSync populates the sync data with any new reactions since the last sync.
func (scs *Service) fetchReactionsForSync(sd *syncData) error {
merr := merror.New()
for _, post := range sd.posts {
// any reactions originating from the remote cluster are filtered out
reactions, err := scs.server.GetStore().Reaction().GetForPostSince(post.Id, sd.scr.LastPostUpdateAt, sd.rc.RemoteId, true)
if err != nil {
merr.Append(fmt.Errorf("could not get reactions for post %s: %w", post.Id, err))
continue
}
sd.reactions = append(sd.reactions, reactions...)
}
return merr.ErrorOrNil()
}
// fetchPostUsersForSync populates the sync data with all users associated with posts.
func (scs *Service) fetchPostUsersForSync(sd *syncData) error {
sc, err := scs.server.GetStore().SharedChannel().Get(sd.task.channelID)
if err != nil {
return fmt.Errorf("cannot determine teamID: %w", err)
}
type p2mm struct {
post *model.Post
mentionMap model.UserMentionMap
}
userIDs := make(map[string]p2mm)
for _, reaction := range sd.reactions {
userIDs[reaction.UserId] = p2mm{}
}
for _, post := range sd.posts {
// add author
userIDs[post.UserId] = p2mm{}
// get mentions and users for each mention
mentionMap := scs.app.MentionsToTeamMembers(request.EmptyContext(scs.server.Log()), post.Message, sc.TeamId)
for _, userID := range mentionMap {
userIDs[userID] = p2mm{
post: post,
mentionMap: mentionMap,
}
}
}
merr := merror.New()
for userID, v := range userIDs {
user, err := scs.server.GetStore().User().Get(context.Background(), userID)
if err != nil {
merr.Append(fmt.Errorf("could not get user %s: %w", userID, err))
continue
}
sync, syncImage, err2 := scs.shouldUserSync(user, sd.task.channelID, sd.rc)
if err2 != nil {
merr.Append(fmt.Errorf("could not check should sync user %s: %w", userID, err))
continue
}
if sync {
sd.users[user.Id] = user
}
if syncImage {
sd.profileImages[user.Id] = user
}
// if this was a mention then put the real username in place of the username+remotename, but only
// when sending to the remote that the user belongs to.
if v.post != nil && user.RemoteId != nil && *user.RemoteId == sd.rc.RemoteId {
fixMention(v.post, v.mentionMap, user)
}
}
return merr.ErrorOrNil()
}
// fetchPostAttachmentsForSync populates the sync data with any file attachments for new posts.
func (scs *Service) fetchPostAttachmentsForSync(sd *syncData) error {
merr := merror.New()
for _, post := range sd.posts {
fis, err := scs.server.GetStore().FileInfo().GetForPost(post.Id, false, true, true)
if err != nil {
merr.Append(fmt.Errorf("could not get file attachment info for post %s: %w", post.Id, err))
continue
}
for _, fi := range fis {
if scs.shouldSyncAttachment(fi, sd.rc) {
sd.attachments = append(sd.attachments, attachment{fi: fi, post: post})
}
}
}
return merr.ErrorOrNil()
}
// filterPostsforSync removes any posts that do not need to sync.
func (scs *Service) filterPostsForSync(sd *syncData) {
filtered := make([]*model.Post, 0, len(sd.posts))
for _, p := range sd.posts {
// Don't resend an existing post where only the reactions changed.
// Posts we must send:
// - new posts (EditAt == 0)
// - edited posts (EditAt >= LastPostUpdateAt)
// - deleted posts (DeleteAt > 0)
if p.EditAt > 0 && p.EditAt < sd.scr.LastPostUpdateAt && p.DeleteAt == 0 {
continue
}
// Don't send a deleted post if it is just the original copy from an edit.
if p.DeleteAt > 0 && p.OriginalId != "" {
continue
}
// don't sync a post back to the remote it came from.
if p.GetRemoteID() == sd.rc.RemoteId {
continue
}
// parse out all permalinks in the message.
p.Message = scs.processPermalinkToRemote(p)
filtered = append(filtered, p)
}
sd.posts = filtered
}
// sendSyncData sends all the collected users, posts, reactions, images, and attachments to the
// remote cluster.
// The order of items sent is important: users -> attachments -> posts -> reactions -> profile images
func (scs *Service) sendSyncData(sd *syncData) error {
merr := merror.New()
sanitizeSyncData(sd)
// send users
if len(sd.users) != 0 {
if err := scs.sendUserSyncData(sd); err != nil {
merr.Append(fmt.Errorf("cannot send user sync data: %w", err))
}
}
// send attachments
if len(sd.attachments) != 0 {
scs.sendAttachmentSyncData(sd)
}
// send posts
if len(sd.posts) != 0 {
if err := scs.sendPostSyncData(sd); err != nil {
merr.Append(fmt.Errorf("cannot send post sync data: %w", err))
}
} else if sd.isCursorChanged() {
scs.updateCursorForRemote(sd.scr.Id, sd.rc, sd.resultNextCursor)
}
// send reactions
if len(sd.reactions) != 0 {
if err := scs.sendReactionSyncData(sd); err != nil {
merr.Append(fmt.Errorf("cannot send reaction sync data: %w", err))
}
}
// send user profile images
if len(sd.profileImages) != 0 {
scs.sendProfileImageSyncData(sd)
}
return merr.ErrorOrNil()
}
// sendUserSyncData sends the collected user updates to the remote cluster.
func (scs *Service) sendUserSyncData(sd *syncData) error {
msg := newSyncMsg(sd.task.channelID)
msg.Users = sd.users
err := scs.sendSyncMsgToRemote(msg, sd.rc, func(syncResp SyncResponse, errResp error) {
for _, userID := range syncResp.UsersSyncd {
if err := scs.server.GetStore().SharedChannel().UpdateUserLastSyncAt(userID, sd.task.channelID, sd.rc.RemoteId); err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Cannot update shared channel user LastSyncAt",
mlog.String("user_id", userID),
mlog.String("channel_id", sd.task.channelID),
mlog.String("remote_id", sd.rc.RemoteId),
mlog.Err(err),
)
}
}
if len(syncResp.UserErrors) != 0 {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Response indicates error for user(s) sync",
mlog.String("channel_id", sd.task.channelID),
mlog.String("remote_id", sd.rc.RemoteId),
mlog.Any("users", syncResp.UserErrors),
)
}
})
return err
}
// sendAttachmentSyncData sends the collected post updates to the remote cluster.
func (scs *Service) sendAttachmentSyncData(sd *syncData) {
for _, a := range sd.attachments {
if err := scs.sendAttachmentForRemote(a.fi, a.post, sd.rc); err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Cannot sync post attachment",
mlog.String("post_id", a.post.Id),
mlog.String("channel_id", sd.task.channelID),
mlog.String("remote_id", sd.rc.RemoteId),
mlog.Err(err),
)
}
// updating SharedChannelAttachments with LastSyncAt is already done.
}
}
// sendPostSyncData sends the collected post updates to the remote cluster.
func (scs *Service) sendPostSyncData(sd *syncData) error {
msg := newSyncMsg(sd.task.channelID)
msg.Posts = sd.posts
return scs.sendSyncMsgToRemote(msg, sd.rc, func(syncResp SyncResponse, errResp error) {
if len(syncResp.PostErrors) != 0 {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Response indicates error for post(s) sync",
mlog.String("channel_id", sd.task.channelID),
mlog.String("remote_id", sd.rc.RemoteId),
mlog.Any("posts", syncResp.PostErrors),
)
for _, postID := range syncResp.PostErrors {
scs.handlePostError(postID, sd.task, sd.rc)
}
}
scs.updateCursorForRemote(sd.scr.Id, sd.rc, sd.resultNextCursor)
})
}
// sendReactionSyncData sends the collected reaction updates to the remote cluster.
func (scs *Service) sendReactionSyncData(sd *syncData) error {
msg := newSyncMsg(sd.task.channelID)
msg.Reactions = sd.reactions
return scs.sendSyncMsgToRemote(msg, sd.rc, func(syncResp SyncResponse, errResp error) {
if len(syncResp.ReactionErrors) != 0 {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Response indicates error for reactions(s) sync",
mlog.String("channel_id", sd.task.channelID),
mlog.String("remote_id", sd.rc.RemoteId),
mlog.Any("reaction_posts", syncResp.ReactionErrors),
)
}
})
}
// sendProfileImageSyncData sends the collected user profile image updates to the remote cluster.
func (scs *Service) sendProfileImageSyncData(sd *syncData) {
for _, user := range sd.profileImages {
scs.syncProfileImage(user, sd.task.channelID, sd.rc)
}
}
// sendSyncMsgToRemote synchronously sends the sync message to the remote cluster.
func (scs *Service) sendSyncMsgToRemote(msg *syncMsg, rc *model.RemoteCluster, f sendSyncMsgResultFunc) error {
rcs := scs.server.GetRemoteClusterService()
if rcs == nil {
return fmt.Errorf("cannot update remote cluster %s for channel id %s; Remote Cluster Service not enabled", rc.Name, msg.ChannelId)
}
b, err := json.Marshal(msg)
if err != nil {
return err
}
rcMsg := model.NewRemoteClusterMsg(TopicSync, b)
ctx, cancel := context.WithTimeout(context.Background(), remotecluster.SendTimeout)
defer cancel()
var wg sync.WaitGroup
wg.Add(1)
err = rcs.SendMsg(ctx, rcMsg, rc, func(rcMsg model.RemoteClusterMsg, rc *model.RemoteCluster, rcResp *remotecluster.Response, errResp error) {
defer wg.Done()
var syncResp SyncResponse
if err2 := json.Unmarshal(rcResp.Payload, &syncResp); err2 != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Invalid sync msg response from remote cluster",
mlog.String("remote", rc.Name),
mlog.String("channel_id", msg.ChannelId),
mlog.Err(err2),
)
return
}
if f != nil {
f(syncResp, errResp)
}
})
wg.Wait()
return err
}
func sanitizeSyncData(sd *syncData) {
for id, user := range sd.users {
sd.users[id] = sanitizeUserForSync(user)
}
for id, user := range sd.profileImages {
sd.profileImages[id] = sanitizeUserForSync(user)
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sharedchannel
import (
"fmt"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
)
// fixMention replaces any mentions in a post for the user with the user's real username.
func fixMention(post *model.Post, mentionMap model.UserMentionMap, user *model.User) {
if post == nil || len(mentionMap) == 0 {
return
}
realUsername, ok := user.GetProp(KeyRemoteUsername)
if !ok {
return
}
// there may be more than one mention for each user so we have to walk the whole map.
for mention, id := range mentionMap {
if id == user.Id && strings.Contains(mention, ":") {
post.Message = strings.ReplaceAll(post.Message, "@"+mention, "@"+realUsername)
}
}
}
func sanitizeUserForSync(user *model.User) *model.User {
user.Password = model.NewId()
user.AuthData = nil
user.AuthService = ""
user.Roles = "system_user"
user.AllowMarketing = false
user.NotifyProps = model.StringMap{}
user.LastPasswordUpdate = 0
user.LastPictureUpdate = 0
user.FailedAttempts = 0
user.MfaActive = false
user.MfaSecret = ""
return user
}
// mungUsername creates a new username by combining username and remote cluster name, plus
// a suffix to create uniqueness. If the resulting username exceeds the max length then
// it is truncated and ellipses added.
func mungUsername(username string, remotename string, suffix string, maxLen int) string {
if suffix != "" {
suffix = "~" + suffix
}
// If the username already contains a colon then another server already munged it.
// In that case we can split on the colon and use the existing remote name.
// We still need to re-mung with suffix in case of collision.
comps := strings.Split(username, ":")
if len(comps) >= 2 {
username = comps[0]
remotename = strings.Join(comps[1:], "")
}
var userEllipses string
var remoteEllipses string
// The remotename is allowed to use up to half the maxLen, and the username gets the remaining space.
// Username might have a suffix to account for, and remotename always has a preceding colon.
half := maxLen / 2
// If the remotename is less than half the maxLen, then the left over space can be given to
// the username.
extra := half - (len(remotename) + 1)
if extra < 0 {
extra = 0
}
truncUser := (len(username) + len(suffix)) - (half + extra)
if truncUser > 0 {
username = username[:len(username)-truncUser-3]
userEllipses = "..."
}
truncRemote := (len(remotename) + 1) - (maxLen - (len(username) + len(userEllipses) + len(suffix)))
if truncRemote > 0 {
remotename = remotename[:len(remotename)-truncRemote-3]
remoteEllipses = "..."
}
return fmt.Sprintf("%s%s%s:%s%s", username, suffix, userEllipses, remotename, remoteEllipses)
}
// mungEmail creates a unique email address using a UID and remote name.
func mungEmail(remotename string, maxLen int) string {
s := fmt.Sprintf("%s@%s", model.NewId(), remotename)
if len(s) > maxLen {
s = s[:maxLen]
}
return s
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slackimport
import (
"regexp"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func slackConvertTimeStamp(ts string) int64 {
timeString := strings.SplitN(ts, ".", 2)[0]
timeStamp, err := strconv.ParseInt(timeString, 10, 64)
if err != nil {
mlog.Warn("Slack Import: Bad timestamp detected.")
return 1
}
return timeStamp * 1000 // Convert to milliseconds
}
func slackConvertChannelName(channelName string, channelId string) string {
newName := strings.Trim(channelName, "_-")
if len(newName) == 1 {
return "slack-channel-" + newName
}
if isValidChannelNameCharacters(newName) {
return newName
}
return strings.ToLower(channelId)
}
func slackConvertUserMentions(users []slackUser, posts map[string][]slackPost) map[string][]slackPost {
var regexes = make(map[string]*regexp.Regexp, len(users))
for _, user := range users {
r, err := regexp.Compile("<@" + user.Id + `(\|` + user.Username + ")?>")
if err != nil {
mlog.Warn("Slack Import: Unable to compile the @mention, matching regular expression for the Slack user.", mlog.String("user_name", user.Username), mlog.String("user_id", user.Id))
continue
}
regexes["@"+user.Username] = r
}
// Special cases.
regexes["@here"], _ = regexp.Compile(`<!here\|@here>`)
regexes["@channel"], _ = regexp.Compile("<!channel>")
regexes["@all"], _ = regexp.Compile("<!everyone>")
for channelName, channelPosts := range posts {
for postIdx, post := range channelPosts {
for mention, r := range regexes {
post.Text = r.ReplaceAllString(post.Text, mention)
posts[channelName][postIdx] = post
}
}
}
return posts
}
func slackConvertChannelMentions(channels []slackChannel, posts map[string][]slackPost) map[string][]slackPost {
var regexes = make(map[string]*regexp.Regexp, len(channels))
for _, channel := range channels {
r, err := regexp.Compile("<#" + channel.Id + `(\|` + channel.Name + ")?>")
if err != nil {
mlog.Warn("Slack Import: Unable to compile the !channel, matching regular expression for the Slack channel.", mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
continue
}
regexes["~"+channel.Name] = r
}
for channelName, channelPosts := range posts {
for postIdx, post := range channelPosts {
for channelReplace, r := range regexes {
post.Text = r.ReplaceAllString(post.Text, channelReplace)
posts[channelName][postIdx] = post
}
}
}
return posts
}
func slackConvertPostsMarkup(posts map[string][]slackPost) map[string][]slackPost {
regexReplaceAllString := []struct {
regex *regexp.Regexp
rpl string
}{
// URL
{
regexp.MustCompile(`<([^|<>]+)\|([^|<>]+)>`),
"[$2]($1)",
},
// bold
{
regexp.MustCompile(`(^|[\s.;,])\*(\S[^*\n]+)\*`),
"$1**$2**",
},
// strikethrough
{
regexp.MustCompile(`(^|[\s.;,])\~(\S[^~\n]+)\~`),
"$1~~$2~~",
},
// single paragraph blockquote
// Slack converts > character to >
{
regexp.MustCompile(`(?sm)^>`),
">",
},
}
regexReplaceAllStringFunc := []struct {
regex *regexp.Regexp
fn func(string) string
}{
// multiple paragraphs blockquotes
{
regexp.MustCompile(`(?sm)^>>>(.+)$`),
func(src string) string {
// remove >>> prefix, might have leading \n
prefixRegexp := regexp.MustCompile(`^([\n])?>>>(.*)`)
src = prefixRegexp.ReplaceAllString(src, "$1$2")
// append > to start of line
appendRegexp := regexp.MustCompile(`(?m)^`)
return appendRegexp.ReplaceAllString(src, ">$0")
},
},
}
for channelName, channelPosts := range posts {
for postIdx, post := range channelPosts {
result := post.Text
for _, rule := range regexReplaceAllString {
result = rule.regex.ReplaceAllString(result, rule.rpl)
}
for _, rule := range regexReplaceAllStringFunc {
result = rule.regex.ReplaceAllStringFunc(result, rule.fn)
}
posts[channelName][postIdx].Text = result
}
}
return posts
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slackimport
import (
"encoding/json"
"io"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func slackParseChannels(data io.Reader, channelType model.ChannelType) ([]slackChannel, error) {
decoder := json.NewDecoder(data)
var channels []slackChannel
if err := decoder.Decode(&channels); err != nil {
mlog.Warn("Slack Import: Error occurred when parsing some Slack channels. Import may work anyway.", mlog.Err(err))
return channels, err
}
for i := range channels {
channels[i].Type = channelType
}
return channels, nil
}
func slackParseUsers(data io.Reader) ([]slackUser, error) {
decoder := json.NewDecoder(data)
var users []slackUser
err := decoder.Decode(&users)
// This actually returns errors that are ignored.
// In this case it is erroring because of a null that Slack
// introduced. So we just return the users here.
return users, err
}
func slackParsePosts(data io.Reader) ([]slackPost, error) {
decoder := json.NewDecoder(data)
var posts []slackPost
if err := decoder.Decode(&posts); err != nil {
mlog.Warn("Slack Import: Error occurred when parsing some Slack posts. Import may work anyway.", mlog.Err(err))
return posts, err
}
return posts, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slackimport
import (
"archive/zip"
"bytes"
"errors"
"image"
"io"
"mime/multipart"
"net/http"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
"unicode/utf8"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type slackChannel struct {
Id string `json:"id"`
Name string `json:"name"`
Creator string `json:"creator"`
Members []string `json:"members"`
Purpose slackChannelSub `json:"purpose"`
Topic slackChannelSub `json:"topic"`
Type model.ChannelType
}
type slackChannelSub struct {
Value string `json:"value"`
}
type slackProfile struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
}
type slackUser struct {
Id string `json:"id"`
Username string `json:"name"`
Profile slackProfile `json:"profile"`
}
type slackFile struct {
Id string `json:"id"`
Title string `json:"title"`
}
type slackPost struct {
User string `json:"user"`
BotId string `json:"bot_id"`
BotUsername string `json:"username"`
Text string `json:"text"`
TimeStamp string `json:"ts"`
ThreadTS string `json:"thread_ts"`
Type string `json:"type"`
SubType string `json:"subtype"`
Comment *slackComment `json:"comment"`
Upload bool `json:"upload"`
File *slackFile `json:"file"`
Files []*slackFile `json:"files"`
Attachments []*model.SlackAttachment `json:"attachments"`
}
var isValidChannelNameCharacters = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`).MatchString
const slackImportMaxFileSize = 1024 * 1024 * 70
type slackComment struct {
User string `json:"user"`
Comment string `json:"comment"`
}
// Actions provides the actions that needs to be used for import slack data
type Actions struct {
UpdateActive func(*model.User, bool) (*model.User, *model.AppError)
AddUserToChannel func(request.CTX, *model.User, *model.Channel, bool) (*model.ChannelMember, *model.AppError)
JoinUserToTeam func(*model.Team, *model.User, string) (*model.TeamMember, *model.AppError)
CreateDirectChannel func(request.CTX, string, string, ...model.ChannelOption) (*model.Channel, *model.AppError)
CreateGroupChannel func(request.CTX, []string) (*model.Channel, *model.AppError)
CreateChannel func(*model.Channel, bool) (*model.Channel, *model.AppError)
DoUploadFile func(time.Time, string, string, string, string, []byte) (*model.FileInfo, *model.AppError)
GenerateThumbnailImage func(image.Image, string, string)
GeneratePreviewImage func(image.Image, string, string)
InvalidateAllCaches func()
MaxPostSize func() int
PrepareImage func(fileData []byte) (image.Image, string, func(), error)
}
// SlackImporter is a service that allows to import slack dumps into mattermost
type SlackImporter struct {
store store.Store
actions Actions
config *model.Config
}
// New creates a new SlackImporter service instance. It receive a store, a set of actions and the current config.
// It is expected to be used right away and discarded after that
func New(store store.Store, actions Actions, config *model.Config) *SlackImporter {
return &SlackImporter{
store: store,
actions: actions,
config: config,
}
}
func (si *SlackImporter) SlackImport(c request.CTX, fileData multipart.File, fileSize int64, teamID string) (*model.AppError, *bytes.Buffer) {
// Create log file
log := bytes.NewBufferString(i18n.T("api.slackimport.slack_import.log"))
zipreader, err := zip.NewReader(fileData, fileSize)
if err != nil || zipreader.File == nil {
log.WriteString(i18n.T("api.slackimport.slack_import.zip.app_error"))
return model.NewAppError("SlackImport", "api.slackimport.slack_import.zip.app_error", nil, "", http.StatusBadRequest).Wrap(err), log
}
var channels []slackChannel
var publicChannels []slackChannel
var privateChannels []slackChannel
var groupChannels []slackChannel
var directChannels []slackChannel
var users []slackUser
posts := make(map[string][]slackPost)
uploads := make(map[string]*zip.File)
for _, file := range zipreader.File {
fileReader, err := file.Open()
if err != nil {
log.WriteString(i18n.T("api.slackimport.slack_import.open.app_error", map[string]any{"Filename": file.Name}))
return model.NewAppError("SlackImport", "api.slackimport.slack_import.open.app_error", map[string]any{"Filename": file.Name}, "", http.StatusInternalServerError).Wrap(err), log
}
reader := utils.NewLimitedReaderWithError(fileReader, slackImportMaxFileSize)
if file.Name == "channels.json" {
publicChannels, err = slackParseChannels(reader, model.ChannelTypeOpen)
if errors.Is(err, utils.SizeLimitExceeded) {
log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name}))
continue
}
channels = append(channels, publicChannels...)
} else if file.Name == "dms.json" {
directChannels, err = slackParseChannels(reader, model.ChannelTypeDirect)
if errors.Is(err, utils.SizeLimitExceeded) {
log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name}))
continue
}
channels = append(channels, directChannels...)
} else if file.Name == "groups.json" {
privateChannels, err = slackParseChannels(reader, model.ChannelTypePrivate)
if errors.Is(err, utils.SizeLimitExceeded) {
log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name}))
continue
}
channels = append(channels, privateChannels...)
} else if file.Name == "mpims.json" {
groupChannels, err = slackParseChannels(reader, model.ChannelTypeGroup)
if errors.Is(err, utils.SizeLimitExceeded) {
log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name}))
continue
}
channels = append(channels, groupChannels...)
} else if file.Name == "users.json" {
users, err = slackParseUsers(reader)
if errors.Is(err, utils.SizeLimitExceeded) {
log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name}))
continue
}
} else {
spl := strings.Split(file.Name, "/")
if len(spl) == 2 && strings.HasSuffix(spl[1], ".json") {
newposts, err := slackParsePosts(reader)
if errors.Is(err, utils.SizeLimitExceeded) {
log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name}))
continue
}
channel := spl[0]
if _, ok := posts[channel]; !ok {
posts[channel] = newposts
} else {
posts[channel] = append(posts[channel], newposts...)
}
} else if len(spl) == 3 && spl[0] == "__uploads" {
uploads[spl[1]] = file
}
}
}
posts = slackConvertUserMentions(users, posts)
posts = slackConvertChannelMentions(channels, posts)
posts = slackConvertPostsMarkup(posts)
addedUsers := si.slackAddUsers(teamID, users, log)
botUser := si.slackAddBotUser(teamID, log)
si.slackAddChannels(c, teamID, channels, posts, addedUsers, uploads, botUser, log)
if botUser != nil {
si.deactivateSlackBotUser(botUser)
}
si.actions.InvalidateAllCaches()
log.WriteString(i18n.T("api.slackimport.slack_import.notes"))
log.WriteString("=======\r\n\r\n")
log.WriteString(i18n.T("api.slackimport.slack_import.note1"))
log.WriteString(i18n.T("api.slackimport.slack_import.note2"))
log.WriteString(i18n.T("api.slackimport.slack_import.note3"))
return nil, log
}
func truncateRunes(s string, i int) string {
runes := []rune(s)
if len(runes) > i {
return string(runes[:i])
}
return s
}
func (si *SlackImporter) slackAddUsers(teamId string, slackusers []slackUser, importerLog *bytes.Buffer) map[string]*model.User {
// Log header
importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.created"))
importerLog.WriteString("===============\r\n\r\n")
addedUsers := make(map[string]*model.User)
// Need the team
team, err := si.store.Team().Get(teamId)
if err != nil {
importerLog.WriteString(i18n.T("api.slackimport.slack_import.team_fail"))
return addedUsers
}
for _, sUser := range slackusers {
firstName := sUser.Profile.FirstName
lastName := sUser.Profile.LastName
email := sUser.Profile.Email
if email == "" {
email = sUser.Username + "@example.com"
importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.missing_email_address", map[string]any{"Email": email, "Username": sUser.Username}))
mlog.Warn("Slack Import: User does not have an email address in the Slack export. Used username as a placeholder. The user should update their email address once logged in to the system.", mlog.String("user_email", email), mlog.String("user_name", sUser.Username))
}
password := model.NewId()
// Check for email conflict and use existing user if found
if existingUser, err := si.store.User().GetByEmail(email); err == nil {
addedUsers[sUser.Id] = existingUser
if _, err := si.actions.JoinUserToTeam(team, addedUsers[sUser.Id], ""); err != nil {
importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.merge_existing_failed", map[string]any{"Email": existingUser.Email, "Username": existingUser.Username}))
} else {
importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.merge_existing", map[string]any{"Email": existingUser.Email, "Username": existingUser.Username}))
}
continue
}
email = strings.ToLower(email)
newUser := model.User{
Username: sUser.Username,
FirstName: firstName,
LastName: lastName,
Email: email,
Password: password,
}
mUser := si.oldImportUser(team, &newUser)
if mUser == nil {
importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.unable_import", map[string]any{"Username": sUser.Username}))
continue
}
addedUsers[sUser.Id] = mUser
importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.email_pwd", map[string]any{"Email": newUser.Email, "Password": password}))
}
return addedUsers
}
func (si *SlackImporter) slackAddBotUser(teamId string, log *bytes.Buffer) *model.User {
team, err := si.store.Team().Get(teamId)
if err != nil {
log.WriteString(i18n.T("api.slackimport.slack_import.team_fail"))
return nil
}
password := model.NewId()
username := "slackimportuser_" + model.NewId()
email := username + "@localhost"
botUser := model.User{
Username: username,
FirstName: "",
LastName: "",
Email: email,
Password: password,
}
mUser := si.oldImportUser(team, &botUser)
if mUser == nil {
log.WriteString(i18n.T("api.slackimport.slack_add_bot_user.unable_import", map[string]any{"Username": username}))
return nil
}
log.WriteString(i18n.T("api.slackimport.slack_add_bot_user.email_pwd", map[string]any{"Email": botUser.Email, "Password": password}))
return mUser
}
func (si *SlackImporter) slackAddPosts(teamId string, channel *model.Channel, posts []slackPost, users map[string]*model.User, uploads map[string]*zip.File, botUser *model.User) {
sort.Slice(posts, func(i, j int) bool {
return slackConvertTimeStamp(posts[i].TimeStamp) < slackConvertTimeStamp(posts[j].TimeStamp)
})
threads := make(map[string]string)
for _, sPost := range posts {
switch {
case sPost.Type == "message" && (sPost.SubType == "" || sPost.SubType == "file_share"):
if sPost.User == "" {
mlog.Debug("Slack Import: Unable to import the message as the user field is missing.")
continue
}
if users[sPost.User] == nil {
mlog.Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User))
continue
}
newPost := model.Post{
UserId: users[sPost.User].Id,
ChannelId: channel.Id,
Message: sPost.Text,
CreateAt: slackConvertTimeStamp(sPost.TimeStamp),
}
if sPost.Upload {
if sPost.File != nil {
if fileInfo, ok := si.slackUploadFile(sPost.File, uploads, teamId, newPost.ChannelId, newPost.UserId, sPost.TimeStamp); ok {
newPost.FileIds = append(newPost.FileIds, fileInfo.Id)
}
} else if sPost.Files != nil {
for _, file := range sPost.Files {
if fileInfo, ok := si.slackUploadFile(file, uploads, teamId, newPost.ChannelId, newPost.UserId, sPost.TimeStamp); ok {
newPost.FileIds = append(newPost.FileIds, fileInfo.Id)
}
}
}
}
// If post in thread
if sPost.ThreadTS != "" && sPost.ThreadTS != sPost.TimeStamp {
newPost.RootId = threads[sPost.ThreadTS]
}
postId := si.oldImportPost(&newPost)
// If post is thread starter
if sPost.ThreadTS == sPost.TimeStamp {
threads[sPost.ThreadTS] = postId
}
case sPost.Type == "message" && sPost.SubType == "file_comment":
if sPost.Comment == nil {
mlog.Debug("Slack Import: Unable to import the message as it has no comments.")
continue
}
if sPost.Comment.User == "" {
mlog.Debug("Slack Import: Unable to import the message as the user field is missing.")
continue
}
if users[sPost.Comment.User] == nil {
mlog.Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User))
continue
}
newPost := model.Post{
UserId: users[sPost.Comment.User].Id,
ChannelId: channel.Id,
Message: sPost.Comment.Comment,
CreateAt: slackConvertTimeStamp(sPost.TimeStamp),
}
si.oldImportPost(&newPost)
case sPost.Type == "message" && sPost.SubType == "bot_message":
if botUser == nil {
mlog.Warn("Slack Import: Unable to import the bot message as the bot user does not exist.")
continue
}
if sPost.BotId == "" {
mlog.Warn("Slack Import: Unable to import bot message as the BotId field is missing.")
continue
}
props := make(model.StringInterface)
props["override_username"] = sPost.BotUsername
if len(sPost.Attachments) > 0 {
props["attachments"] = sPost.Attachments
}
post := &model.Post{
UserId: botUser.Id,
ChannelId: channel.Id,
CreateAt: slackConvertTimeStamp(sPost.TimeStamp),
Message: sPost.Text,
Type: model.PostTypeSlackAttachment,
}
postId := si.oldImportIncomingWebhookPost(post, props)
// If post is thread starter
if sPost.ThreadTS == sPost.TimeStamp {
threads[sPost.ThreadTS] = postId
}
case sPost.Type == "message" && (sPost.SubType == "channel_join" || sPost.SubType == "channel_leave"):
if sPost.User == "" {
mlog.Debug("Slack Import: Unable to import the message as the user field is missing.")
continue
}
if users[sPost.User] == nil {
mlog.Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User))
continue
}
var postType string
if sPost.SubType == "channel_join" {
postType = model.PostTypeJoinChannel
} else {
postType = model.PostTypeLeaveChannel
}
newPost := model.Post{
UserId: users[sPost.User].Id,
ChannelId: channel.Id,
Message: sPost.Text,
CreateAt: slackConvertTimeStamp(sPost.TimeStamp),
Type: postType,
Props: model.StringInterface{
"username": users[sPost.User].Username,
},
}
si.oldImportPost(&newPost)
case sPost.Type == "message" && sPost.SubType == "me_message":
if sPost.User == "" {
mlog.Debug("Slack Import: Unable to import the message as the user field is missing.")
continue
}
if users[sPost.User] == nil {
mlog.Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User))
continue
}
newPost := model.Post{
UserId: users[sPost.User].Id,
ChannelId: channel.Id,
Message: "*" + sPost.Text + "*",
CreateAt: slackConvertTimeStamp(sPost.TimeStamp),
}
postId := si.oldImportPost(&newPost)
// If post is thread starter
if sPost.ThreadTS == sPost.TimeStamp {
threads[sPost.ThreadTS] = postId
}
case sPost.Type == "message" && sPost.SubType == "channel_topic":
if sPost.User == "" {
mlog.Debug("Slack Import: Unable to import the message as the user field is missing.")
continue
}
if users[sPost.User] == nil {
mlog.Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User))
continue
}
newPost := model.Post{
UserId: users[sPost.User].Id,
ChannelId: channel.Id,
Message: sPost.Text,
CreateAt: slackConvertTimeStamp(sPost.TimeStamp),
Type: model.PostTypeHeaderChange,
}
si.oldImportPost(&newPost)
case sPost.Type == "message" && sPost.SubType == "channel_purpose":
if sPost.User == "" {
mlog.Debug("Slack Import: Unable to import the message as the user field is missing.")
continue
}
if users[sPost.User] == nil {
mlog.Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User))
continue
}
newPost := model.Post{
UserId: users[sPost.User].Id,
ChannelId: channel.Id,
Message: sPost.Text,
CreateAt: slackConvertTimeStamp(sPost.TimeStamp),
Type: model.PostTypePurposeChange,
}
si.oldImportPost(&newPost)
case sPost.Type == "message" && sPost.SubType == "channel_name":
if sPost.User == "" {
mlog.Debug("Slack Import: Unable to import the message as the user field is missing.")
continue
}
if users[sPost.User] == nil {
mlog.Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User))
continue
}
newPost := model.Post{
UserId: users[sPost.User].Id,
ChannelId: channel.Id,
Message: sPost.Text,
CreateAt: slackConvertTimeStamp(sPost.TimeStamp),
Type: model.PostTypeDisplaynameChange,
}
si.oldImportPost(&newPost)
default:
mlog.Warn(
"Slack Import: Unable to import the message as its type is not supported",
mlog.String("post_type", sPost.Type),
mlog.String("post_subtype", sPost.SubType),
)
}
}
}
func (si *SlackImporter) slackUploadFile(slackPostFile *slackFile, uploads map[string]*zip.File, teamId string, channelId string, userId string, slackTimestamp string) (*model.FileInfo, bool) {
if slackPostFile == nil {
mlog.Warn("Slack Import: Unable to attach the file to the post as the latter has no file section present in Slack export.")
return nil, false
}
file, ok := uploads[slackPostFile.Id]
if !ok {
mlog.Warn("Slack Import: Unable to import file as the file is missing from the Slack export zip file.", mlog.String("file_id", slackPostFile.Id))
return nil, false
}
openFile, err := file.Open()
if err != nil {
mlog.Warn("Slack Import: Unable to open the file from the Slack export.", mlog.String("file_id", slackPostFile.Id), mlog.Err(err))
return nil, false
}
defer openFile.Close()
timestamp := utils.TimeFromMillis(slackConvertTimeStamp(slackTimestamp))
uploadedFile, err := si.oldImportFile(timestamp, openFile, teamId, channelId, userId, filepath.Base(file.Name))
if err != nil {
mlog.Warn("Slack Import: An error occurred when uploading file.", mlog.String("file_id", slackPostFile.Id), mlog.Err(err))
return nil, false
}
return uploadedFile, true
}
func (si *SlackImporter) deactivateSlackBotUser(user *model.User) {
if _, err := si.actions.UpdateActive(user, false); err != nil {
mlog.Warn("Slack Import: Unable to deactivate the user account used for the bot.")
}
}
func (si *SlackImporter) addSlackUsersToChannel(c request.CTX, members []string, users map[string]*model.User, channel *model.Channel, log *bytes.Buffer) {
for _, member := range members {
user, ok := users[member]
if !ok {
log.WriteString(i18n.T("api.slackimport.slack_add_channels.failed_to_add_user", map[string]any{"Username": "?"}))
continue
}
if _, err := si.actions.AddUserToChannel(c, user, channel, false); err != nil {
log.WriteString(i18n.T("api.slackimport.slack_add_channels.failed_to_add_user", map[string]any{"Username": user.Username}))
}
}
}
func slackSanitiseChannelProperties(channel model.Channel) model.Channel {
if utf8.RuneCountInString(channel.DisplayName) > model.ChannelDisplayNameMaxRunes {
mlog.Warn("Slack Import: Channel display name exceeds the maximum length. It will be truncated when imported.", mlog.String("channel_display_name", channel.DisplayName))
channel.DisplayName = truncateRunes(channel.DisplayName, model.ChannelDisplayNameMaxRunes)
}
if len(channel.Name) > model.ChannelNameMaxLength {
mlog.Warn("Slack Import: Channel handle exceeds the maximum length. It will be truncated when imported.", mlog.String("channel_display_name", channel.DisplayName))
channel.Name = channel.Name[0:model.ChannelNameMaxLength]
}
if utf8.RuneCountInString(channel.Purpose) > model.ChannelPurposeMaxRunes {
mlog.Warn("Slack Import: Channel purpose exceeds the maximum length. It will be truncated when imported.", mlog.String("channel_display_name", channel.DisplayName))
channel.Purpose = truncateRunes(channel.Purpose, model.ChannelPurposeMaxRunes)
}
if utf8.RuneCountInString(channel.Header) > model.ChannelHeaderMaxRunes {
mlog.Warn("Slack Import: Channel header exceeds the maximum length. It will be truncated when imported.", mlog.String("channel_display_name", channel.DisplayName))
channel.Header = truncateRunes(channel.Header, model.ChannelHeaderMaxRunes)
}
return channel
}
func (si *SlackImporter) slackAddChannels(c request.CTX, teamId string, slackchannels []slackChannel, posts map[string][]slackPost, users map[string]*model.User, uploads map[string]*zip.File, botUser *model.User, importerLog *bytes.Buffer) map[string]*model.Channel {
// Write Header
importerLog.WriteString(i18n.T("api.slackimport.slack_add_channels.added"))
importerLog.WriteString("=================\r\n\r\n")
addedChannels := make(map[string]*model.Channel)
for _, sChannel := range slackchannels {
newChannel := model.Channel{
TeamId: teamId,
Type: sChannel.Type,
DisplayName: sChannel.Name,
Name: slackConvertChannelName(sChannel.Name, sChannel.Id),
Purpose: sChannel.Purpose.Value,
Header: sChannel.Topic.Value,
}
// Direct message channels in Slack don't have a name so we set the id as name or else the messages won't get imported.
if newChannel.Type == model.ChannelTypeDirect {
sChannel.Name = sChannel.Id
}
newChannel = slackSanitiseChannelProperties(newChannel)
var mChannel *model.Channel
var err error
if mChannel, err = si.store.Channel().GetByName(teamId, sChannel.Name, true); err == nil {
// The channel already exists as an active channel. Merge with the existing one.
importerLog.WriteString(i18n.T("api.slackimport.slack_add_channels.merge", map[string]any{"DisplayName": newChannel.DisplayName}))
} else if _, nErr := si.store.Channel().GetDeletedByName(teamId, sChannel.Name); nErr == nil {
// The channel already exists but has been deleted. Generate a random string for the handle instead.
newChannel.Name = model.NewId()
newChannel = slackSanitiseChannelProperties(newChannel)
}
if mChannel == nil {
// Haven't found an existing channel to merge with. Try importing it as a new one.
mChannel = si.oldImportChannel(c, &newChannel, sChannel, users)
if mChannel == nil {
mlog.Warn("Slack Import: Unable to import Slack channel.", mlog.String("channel_display_name", newChannel.DisplayName))
importerLog.WriteString(i18n.T("api.slackimport.slack_add_channels.import_failed", map[string]any{"DisplayName": newChannel.DisplayName}))
continue
}
}
// Members for direct and group channels are added during the creation of the channel in the oldImportChannel function
if sChannel.Type == model.ChannelTypeOpen || sChannel.Type == model.ChannelTypePrivate {
si.addSlackUsersToChannel(c, sChannel.Members, users, mChannel, importerLog)
}
importerLog.WriteString(newChannel.DisplayName + "\r\n")
addedChannels[sChannel.Id] = mChannel
si.slackAddPosts(teamId, mChannel, posts[sChannel.Name], users, uploads, botUser)
}
return addedChannels
}
//
// -- Old SlackImport Functions --
// Import functions are suitable for entering posts and users into the database without
// some of the usual checks. (IsValid is still run)
//
func (si *SlackImporter) oldImportPost(post *model.Post) string {
// Workaround for empty messages, which may be the case if they are webhook posts.
firstIteration := true
firstPostId := ""
if post.RootId != "" {
firstPostId = post.RootId
}
maxPostSize := si.actions.MaxPostSize()
for messageRuneCount := utf8.RuneCountInString(post.Message); messageRuneCount > 0 || firstIteration; messageRuneCount = utf8.RuneCountInString(post.Message) {
var remainder string
if messageRuneCount > maxPostSize {
remainder = string(([]rune(post.Message))[maxPostSize:])
post.Message = truncateRunes(post.Message, maxPostSize)
} else {
remainder = ""
}
post.Hashtags, _ = model.ParseHashtags(post.Message)
post.RootId = firstPostId
_, err := si.store.Post().Save(post)
if err != nil {
mlog.Debug("Error saving post.", mlog.String("user_id", post.UserId), mlog.String("message", post.Message))
}
if firstIteration {
if firstPostId == "" {
firstPostId = post.Id
}
for _, fileId := range post.FileIds {
if err := si.store.FileInfo().AttachToPost(fileId, post.Id, post.ChannelId, post.UserId); err != nil {
mlog.Error(
"Error attaching files to post.",
mlog.String("post_id", post.Id),
mlog.String("file_ids", strings.Join(post.FileIds, ",")),
mlog.String("user_id", post.UserId),
mlog.Err(err),
)
}
}
post.FileIds = nil
}
post.Id = ""
post.CreateAt++
post.Message = remainder
firstIteration = false
}
return firstPostId
}
func (si *SlackImporter) oldImportUser(team *model.Team, user *model.User) *model.User {
user.MakeNonNil()
user.Roles = model.SystemUserRoleId
ruser, nErr := si.store.User().Save(user)
if nErr != nil {
mlog.Debug("Error saving user.", mlog.Err(nErr))
return nil
}
if _, err := si.store.User().VerifyEmail(ruser.Id, ruser.Email); err != nil {
mlog.Warn("Failed to set email verified.", mlog.Err(err))
}
if _, err := si.actions.JoinUserToTeam(team, user, ""); err != nil {
mlog.Warn("Failed to join team when importing.", mlog.Err(err))
}
return ruser
}
func (si *SlackImporter) oldImportChannel(c request.CTX, channel *model.Channel, sChannel slackChannel, users map[string]*model.User) *model.Channel {
switch {
case channel.Type == model.ChannelTypeDirect:
if len(sChannel.Members) < 2 {
return nil
}
u1 := users[sChannel.Members[0]]
u2 := users[sChannel.Members[1]]
if u1 == nil || u2 == nil {
mlog.Warn("Either or both of user ids not found in users.json. Ignoring.", mlog.String("id1", sChannel.Members[0]), mlog.String("id2", sChannel.Members[1]))
return nil
}
sc, err := si.actions.CreateDirectChannel(c, u1.Id, u2.Id)
if err != nil {
return nil
}
return sc
// check if direct channel has less than 8 members and if not import as private channel instead
case channel.Type == model.ChannelTypeGroup && len(sChannel.Members) < 8:
members := make([]string, len(sChannel.Members))
for i := range sChannel.Members {
u := users[sChannel.Members[i]]
if u == nil {
mlog.Warn("User not found in users.json. Ignoring.", mlog.String("id", sChannel.Members[i]))
continue
}
members[i] = u.Id
}
creator := users[sChannel.Creator]
if creator == nil {
return nil
}
sc, err := si.actions.CreateGroupChannel(c, members)
if err != nil {
return nil
}
return sc
case channel.Type == model.ChannelTypeGroup:
channel.Type = model.ChannelTypePrivate
sc, err := si.actions.CreateChannel(channel, false)
if err != nil {
return nil
}
return sc
}
sc, err := si.store.Channel().Save(channel, *si.config.TeamSettings.MaxChannelsPerTeam)
if err != nil {
return nil
}
return sc
}
func (si *SlackImporter) oldImportFile(timestamp time.Time, file io.Reader, teamId string, channelId string, userId string, fileName string) (*model.FileInfo, error) {
buf := bytes.NewBuffer(nil)
io.Copy(buf, file)
data := buf.Bytes()
fileInfo, err := si.actions.DoUploadFile(timestamp, teamId, channelId, userId, fileName, data)
if err != nil {
return nil, err
}
if fileInfo.IsImage() && !fileInfo.IsSvg() {
img, imgType, release, err := si.actions.PrepareImage(data)
if err != nil {
return nil, err
}
defer release()
si.actions.GenerateThumbnailImage(img, imgType, fileInfo.ThumbnailPath)
si.actions.GeneratePreviewImage(img, imgType, fileInfo.PreviewPath)
}
return fileInfo, nil
}
func (si *SlackImporter) oldImportIncomingWebhookPost(post *model.Post, props model.StringInterface) string {
linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
post.Message = linkWithTextRegex.ReplaceAllString(post.Message, "[${2}](${1})")
post.AddProp("from_webhook", "true")
if _, ok := props["override_username"]; !ok {
post.AddProp("override_username", model.DefaultWebhookUsername)
}
if len(props) > 0 {
for key, val := range props {
if key == "attachments" {
if attachments, success := val.([]*model.SlackAttachment); success {
model.ParseSlackAttachment(post, attachments)
}
} else if key != "from_webhook" {
post.AddProp(key, val)
}
}
}
return si.oldImportPost(post)
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make telemetry-mocks`.
package mocks
import (
context "context"
httpservice "github.com/mattermost/mattermost-server/v6/server/platform/services/httpservice"
mock "github.com/stretchr/testify/mock"
model "github.com/mattermost/mattermost-server/v6/model"
plugin "github.com/mattermost/mattermost-server/v6/plugin"
product "github.com/mattermost/mattermost-server/v6/server/channels/product"
)
// ServerIface is an autogenerated mock type for the ServerIface type
type ServerIface struct {
mock.Mock
}
// Config provides a mock function with given fields:
func (_m *ServerIface) Config() *model.Config {
ret := _m.Called()
var r0 *model.Config
if rf, ok := ret.Get(0).(func() *model.Config); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Config)
}
}
return r0
}
// GetPluginsEnvironment provides a mock function with given fields:
func (_m *ServerIface) GetPluginsEnvironment() *plugin.Environment {
ret := _m.Called()
var r0 *plugin.Environment
if rf, ok := ret.Get(0).(func() *plugin.Environment); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*plugin.Environment)
}
}
return r0
}
// GetRoleByName provides a mock function with given fields: _a0, _a1
func (_m *ServerIface) GetRoleByName(_a0 context.Context, _a1 string) (*model.Role, *model.AppError) {
ret := _m.Called(_a0, _a1)
var r0 *model.Role
if rf, ok := ret.Get(0).(func(context.Context, string) *model.Role); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Role)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(context.Context, string) *model.AppError); ok {
r1 = rf(_a0, _a1)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// GetSchemes provides a mock function with given fields: _a0, _a1, _a2
func (_m *ServerIface) GetSchemes(_a0 string, _a1 int, _a2 int) ([]*model.Scheme, *model.AppError) {
ret := _m.Called(_a0, _a1, _a2)
var r0 []*model.Scheme
if rf, ok := ret.Get(0).(func(string, int, int) []*model.Scheme); ok {
r0 = rf(_a0, _a1, _a2)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Scheme)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func(string, int, int) *model.AppError); ok {
r1 = rf(_a0, _a1, _a2)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// HTTPService provides a mock function with given fields:
func (_m *ServerIface) HTTPService() httpservice.HTTPService {
ret := _m.Called()
var r0 httpservice.HTTPService
if rf, ok := ret.Get(0).(func() httpservice.HTTPService); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(httpservice.HTTPService)
}
}
return r0
}
// HooksManager provides a mock function with given fields:
func (_m *ServerIface) HooksManager() *product.HooksManager {
ret := _m.Called()
var r0 *product.HooksManager
if rf, ok := ret.Get(0).(func() *product.HooksManager); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*product.HooksManager)
}
}
return r0
}
// IsLeader provides a mock function with given fields:
func (_m *ServerIface) IsLeader() bool {
ret := _m.Called()
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// License provides a mock function with given fields:
func (_m *ServerIface) License() *model.License {
ret := _m.Called()
var r0 *model.License
if rf, ok := ret.Get(0).(func() *model.License); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.License)
}
}
return r0
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package telemetry
import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"time"
rudder "github.com/rudderlabs/analytics-go"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
"github.com/mattermost/mattermost-server/v6/server/channels/product"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/services/httpservice"
"github.com/mattermost/mattermost-server/v6/server/platform/services/marketplace"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
DayMilliseconds = 24 * 60 * 60 * 1000
MonthMilliseconds = 31 * DayMilliseconds
DBAccessAttempts = 3
DBAccessTimeoutSecs = 10
RudderKey = "placeholder_rudder_key"
RudderDataplaneURL = "placeholder_rudder_dataplane_url"
EnvVarInstallType = "MM_INSTALL_TYPE"
TrackConfigService = "config_service"
TrackConfigTeam = "config_team"
TrackConfigClientReq = "config_client_requirements"
TrackConfigSQL = "config_sql"
TrackConfigLog = "config_log"
TrackConfigAudit = "config_audit"
TrackConfigNotificationLog = "config_notifications_log"
TrackConfigFile = "config_file"
TrackConfigRate = "config_rate"
TrackConfigEmail = "config_email"
TrackConfigPrivacy = "config_privacy"
TrackConfigTheme = "config_theme"
TrackConfigOAuth = "config_oauth"
TrackConfigLDAP = "config_ldap"
TrackConfigCompliance = "config_compliance"
TrackConfigLocalization = "config_localization"
TrackConfigSAML = "config_saml"
TrackConfigPassword = "config_password"
TrackConfigCluster = "config_cluster"
TrackConfigMetrics = "config_metrics"
TrackConfigSupport = "config_support"
TrackConfigNativeApp = "config_nativeapp"
TrackConfigExperimental = "config_experimental"
TrackConfigAnalytics = "config_analytics"
TrackConfigAnnouncement = "config_announcement"
TrackConfigElasticsearch = "config_elasticsearch"
TrackConfigPlugin = "config_plugin"
TrackConfigDataRetention = "config_data_retention"
TrackConfigMessageExport = "config_message_export"
TrackConfigDisplay = "config_display"
TrackConfigGuestAccounts = "config_guest_accounts"
TrackConfigImageProxy = "config_image_proxy"
TrackConfigBleve = "config_bleve"
TrackConfigExport = "config_export"
TrackFeatureFlags = "config_feature_flags"
TrackConfigProducts = "products"
TrackPermissionsGeneral = "permissions_general"
TrackPermissionsSystemScheme = "permissions_system_scheme"
TrackPermissionsTeamSchemes = "permissions_team_schemes"
TrackPermissionsSystemRoles = "permissions_system_roles"
TrackElasticsearch = "elasticsearch"
TrackGroups = "groups"
TrackChannelModeration = "channel_moderation"
TrackWarnMetrics = "warn_metrics"
TrackActivity = "activity"
TrackLicense = "license"
TrackServer = "server"
TrackPlugins = "plugins"
)
type ServerIface interface {
Config() *model.Config
IsLeader() bool
HTTPService() httpservice.HTTPService
GetPluginsEnvironment() *plugin.Environment
License() *model.License
GetRoleByName(context.Context, string) (*model.Role, *model.AppError)
GetSchemes(string, int, int) ([]*model.Scheme, *model.AppError)
HooksManager() *product.HooksManager
}
type TelemetryService struct {
srv ServerIface
dbStore store.Store
searchEngine *searchengine.Broker
log *mlog.Logger
rudderClient rudder.Client
TelemetryID string
timestampLastTelemetrySent time.Time
verbose bool
}
type RudderConfig struct {
RudderKey string
DataplaneURL string
}
func New(srv ServerIface, dbStore store.Store, searchEngine *searchengine.Broker, log *mlog.Logger, verbose bool) (*TelemetryService, error) {
service := &TelemetryService{
srv: srv,
dbStore: dbStore,
searchEngine: searchEngine,
log: log,
verbose: verbose,
}
if err := service.ensureTelemetryID(); err != nil {
return nil, fmt.Errorf("unable to ensure telemetry ID: %w", err)
}
return service, nil
}
func (ts *TelemetryService) ensureTelemetryID() error {
if ts.TelemetryID != "" {
return nil
}
id := model.NewId()
var err error
for i := 0; i < DBAccessAttempts; i++ {
ts.log.Info("Ensuring the telemetry ID", mlog.String("id", id))
systemID := &model.System{Name: model.SystemTelemetryId, Value: id}
systemID, err = ts.dbStore.System().InsertIfExists(systemID)
if err != nil {
ts.log.Info("Unable to get/set the telemetry ID", mlog.Err(err))
time.Sleep(DBAccessTimeoutSecs * time.Second)
continue
}
ts.TelemetryID = systemID.Value
return nil
}
return fmt.Errorf("unable to get the telemetry ID: %w", err)
}
func (ts *TelemetryService) getRudderConfig() RudderConfig {
if !strings.Contains(RudderKey, "placeholder") && !strings.Contains(RudderDataplaneURL, "placeholder") {
return RudderConfig{RudderKey, RudderDataplaneURL}
} else if os.Getenv("RudderKey") != "" && os.Getenv("RudderDataplaneURL") != "" {
return RudderConfig{os.Getenv("RudderKey"), os.Getenv("RudderDataplaneURL")}
} else {
return RudderConfig{}
}
}
func (ts *TelemetryService) telemetryEnabled() bool {
return *ts.srv.Config().LogSettings.EnableDiagnostics && ts.srv.IsLeader()
}
func (ts *TelemetryService) sendDailyTelemetry(override bool) {
config := ts.getRudderConfig()
if ts.telemetryEnabled() && ((config.DataplaneURL != "" && config.RudderKey != "") || override) {
ts.initRudder(config.DataplaneURL, config.RudderKey)
ts.trackActivity()
ts.trackConfig()
ts.trackLicense()
ts.trackPlugins()
ts.trackServer()
ts.trackPermissions()
ts.trackElasticsearch()
ts.trackGroups()
ts.trackChannelModeration()
ts.trackWarnMetrics()
ts.trackProducts()
}
}
func (ts *TelemetryService) SendTelemetry(event string, properties map[string]any) {
if ts.rudderClient != nil {
var context *rudder.Context
// if we are part of a cloud installation, add it's ID to the tracked event's context
if installationId := os.Getenv("MM_CLOUD_INSTALLATION_ID"); installationId != "" {
context = &rudder.Context{Traits: map[string]any{"installationId": installationId}}
}
err := ts.rudderClient.Enqueue(rudder.Track{
Event: event,
UserId: ts.TelemetryID,
Properties: properties,
Context: context,
})
if err != nil {
ts.log.Warn("Error sending telemetry", mlog.Err(err))
}
}
}
func isDefaultArray(setting, defaultValue []string) bool {
if len(setting) != len(defaultValue) {
return false
}
for i := 0; i < len(setting); i++ {
if setting[i] != defaultValue[i] {
return false
}
}
return true
}
func isDefault(setting any, defaultValue any) bool {
return setting == defaultValue
}
func pluginSetting(pluginSettings *model.PluginSettings, plugin, key string, defaultValue any) any {
settings, ok := pluginSettings.Plugins[plugin]
if !ok {
return defaultValue
}
if value, ok := settings[key]; ok {
return value
}
return defaultValue
}
func pluginActivated(pluginStates map[string]*model.PluginState, pluginId string) bool {
state, ok := pluginStates[pluginId]
if !ok {
return false
}
return state.Enable
}
func pluginVersion(pluginsAvailable []*model.BundleInfo, pluginId string) string {
for _, plugin := range pluginsAvailable {
if plugin.Manifest != nil && plugin.Manifest.Id == pluginId {
return plugin.Manifest.Version
}
}
return ""
}
func (ts *TelemetryService) trackActivity() {
var userCount int64
var guestAccountsCount int64
var botAccountsCount int64
var inactiveUserCount int64
var publicChannelCount int64
var privateChannelCount int64
var directChannelCount int64
var deletedPublicChannelCount int64
var deletedPrivateChannelCount int64
var postsCount int64
var postsCountPreviousDay int64
var botPostsCountPreviousDay int64
var slashCommandsCount int64
var incomingWebhooksCount int64
var outgoingWebhooksCount int64
activeUsersDailyCountChan := make(chan store.StoreResult, 1)
go func() {
count, err := ts.dbStore.User().AnalyticsActiveCount(DayMilliseconds, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: false})
activeUsersDailyCountChan <- store.StoreResult{Data: count, NErr: err}
close(activeUsersDailyCountChan)
}()
activeUsersMonthlyCountChan := make(chan store.StoreResult, 1)
go func() {
count, err := ts.dbStore.User().AnalyticsActiveCount(MonthMilliseconds, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: false})
activeUsersMonthlyCountChan <- store.StoreResult{Data: count, NErr: err}
close(activeUsersMonthlyCountChan)
}()
if count, err := ts.dbStore.User().Count(model.UserCountOptions{IncludeDeleted: true}); err == nil {
userCount = count
}
if count, err := ts.dbStore.User().AnalyticsGetGuestCount(); err == nil {
guestAccountsCount = count
}
if count, err := ts.dbStore.User().Count(model.UserCountOptions{IncludeBotAccounts: true, ExcludeRegularUsers: true}); err == nil {
botAccountsCount = count
}
if iucr, err := ts.dbStore.User().AnalyticsGetInactiveUsersCount(); err == nil {
inactiveUserCount = iucr
}
teamCount, err := ts.dbStore.Team().AnalyticsTeamCount(nil)
if err != nil {
mlog.Info("Could not get team count", mlog.Err(err))
}
if ucc, err := ts.dbStore.Channel().AnalyticsTypeCount("", model.ChannelTypeOpen); err == nil {
publicChannelCount = ucc
}
if pcc, err := ts.dbStore.Channel().AnalyticsTypeCount("", model.ChannelTypePrivate); err == nil {
privateChannelCount = pcc
}
if dcc, err := ts.dbStore.Channel().AnalyticsTypeCount("", model.ChannelTypeDirect); err == nil {
directChannelCount = dcc
}
if duccr, err := ts.dbStore.Channel().AnalyticsDeletedTypeCount("", model.ChannelTypeOpen); err == nil {
deletedPublicChannelCount = duccr
}
if dpccr, err := ts.dbStore.Channel().AnalyticsDeletedTypeCount("", model.ChannelTypePrivate); err == nil {
deletedPrivateChannelCount = dpccr
}
postsCount, _ = ts.dbStore.Post().AnalyticsPostCount(&model.PostCountOptions{})
postCountsOptions := &model.AnalyticsPostCountsOptions{TeamId: "", BotsOnly: false, YesterdayOnly: true}
postCountsYesterday, _ := ts.dbStore.Post().AnalyticsPostCountsByDay(postCountsOptions)
postsCountPreviousDay = 0
if len(postCountsYesterday) > 0 {
postsCountPreviousDay = int64(postCountsYesterday[0].Value)
}
postCountsOptions = &model.AnalyticsPostCountsOptions{TeamId: "", BotsOnly: true, YesterdayOnly: true}
botPostCountsYesterday, _ := ts.dbStore.Post().AnalyticsPostCountsByDay(postCountsOptions)
botPostsCountPreviousDay = 0
if len(botPostCountsYesterday) > 0 {
botPostsCountPreviousDay = int64(botPostCountsYesterday[0].Value)
}
slashCommandsCount, _ = ts.dbStore.Command().AnalyticsCommandCount("")
if c, err := ts.dbStore.Webhook().AnalyticsIncomingCount(""); err == nil {
incomingWebhooksCount = c
}
outgoingWebhooksCount, _ = ts.dbStore.Webhook().AnalyticsOutgoingCount("")
var activeUsersDailyCount int64
if r := <-activeUsersDailyCountChan; r.NErr == nil {
activeUsersDailyCount = r.Data.(int64)
}
var activeUsersMonthlyCount int64
if r := <-activeUsersMonthlyCountChan; r.NErr == nil {
activeUsersMonthlyCount = r.Data.(int64)
}
activity := map[string]any{
"registered_users": userCount,
"bot_accounts": botAccountsCount,
"guest_accounts": guestAccountsCount,
"active_users_daily": activeUsersDailyCount,
"active_users_monthly": activeUsersMonthlyCount,
"registered_deactivated_users": inactiveUserCount,
"teams": teamCount,
"public_channels": publicChannelCount,
"private_channels": privateChannelCount,
"direct_message_channels": directChannelCount,
"public_channels_deleted": deletedPublicChannelCount,
"private_channels_deleted": deletedPrivateChannelCount,
"posts_previous_day": postsCountPreviousDay,
"bot_posts_previous_day": botPostsCountPreviousDay,
"posts": postsCount,
"slash_commands": slashCommandsCount,
"incoming_webhooks": incomingWebhooksCount,
"outgoing_webhooks": outgoingWebhooksCount,
}
if license := ts.srv.License(); license.IsCloud() {
var tmpStorage int64
if usage, err := ts.dbStore.FileInfo().GetStorageUsage(true, false); err == nil {
tmpStorage = usage
}
activity["storage_bytes"] = utils.RoundOffToZeroesResolution(float64(tmpStorage), 8)
}
ts.SendTelemetry(TrackActivity, activity)
}
func (ts *TelemetryService) trackConfig() {
cfg := ts.srv.Config()
ts.SendTelemetry(TrackConfigService, map[string]any{
"web_server_mode": *cfg.ServiceSettings.WebserverMode,
"enable_security_fix_alert": *cfg.ServiceSettings.EnableSecurityFixAlert,
"enable_insecure_outgoing_connections": *cfg.ServiceSettings.EnableInsecureOutgoingConnections,
"enable_incoming_webhooks": cfg.ServiceSettings.EnableIncomingWebhooks,
"enable_outgoing_webhooks": cfg.ServiceSettings.EnableOutgoingWebhooks,
"enable_commands": *cfg.ServiceSettings.EnableCommands,
"enable_post_username_override": cfg.ServiceSettings.EnablePostUsernameOverride,
"enable_post_icon_override": cfg.ServiceSettings.EnablePostIconOverride,
"enable_user_access_tokens": *cfg.ServiceSettings.EnableUserAccessTokens,
"enable_custom_emoji": *cfg.ServiceSettings.EnableCustomEmoji,
"enable_emoji_picker": *cfg.ServiceSettings.EnableEmojiPicker,
"enable_gif_picker": *cfg.ServiceSettings.EnableGifPicker,
"gfycat_api_key": isDefault(*cfg.ServiceSettings.GfycatAPIKey, model.ServiceSettingsDefaultGfycatAPIKey),
"gfycat_api_secret": isDefault(*cfg.ServiceSettings.GfycatAPISecret, model.ServiceSettingsDefaultGfycatAPISecret),
"experimental_enable_authentication_transfer": *cfg.ServiceSettings.ExperimentalEnableAuthenticationTransfer,
"enable_testing": cfg.ServiceSettings.EnableTesting,
"enable_developer": *cfg.ServiceSettings.EnableDeveloper,
"developer_flags": isDefault(*cfg.ServiceSettings.DeveloperFlags, model.ServiceSettingsDefaultDeveloperFlags),
"enable_client_performance_debugging": *cfg.ServiceSettings.EnableClientPerformanceDebugging,
"enable_multifactor_authentication": *cfg.ServiceSettings.EnableMultifactorAuthentication,
"enforce_multifactor_authentication": *cfg.ServiceSettings.EnforceMultifactorAuthentication,
"enable_oauth_service_provider": cfg.ServiceSettings.EnableOAuthServiceProvider,
"connection_security": *cfg.ServiceSettings.ConnectionSecurity,
"tls_strict_transport": *cfg.ServiceSettings.TLSStrictTransport,
"uses_letsencrypt": *cfg.ServiceSettings.UseLetsEncrypt,
"forward_80_to_443": *cfg.ServiceSettings.Forward80To443,
"maximum_login_attempts": *cfg.ServiceSettings.MaximumLoginAttempts,
"extend_session_length_with_activity": *cfg.ServiceSettings.ExtendSessionLengthWithActivity,
"session_length_web_in_hours": *cfg.ServiceSettings.SessionLengthWebInHours,
"session_length_mobile_in_hours": *cfg.ServiceSettings.SessionLengthMobileInHours,
"session_length_sso_in_hours": *cfg.ServiceSettings.SessionLengthSSOInHours,
"session_cache_in_minutes": *cfg.ServiceSettings.SessionCacheInMinutes,
"session_idle_timeout_in_minutes": *cfg.ServiceSettings.SessionIdleTimeoutInMinutes,
"isdefault_site_url": isDefault(*cfg.ServiceSettings.SiteURL, model.ServiceSettingsDefaultSiteURL),
"isdefault_tls_cert_file": isDefault(*cfg.ServiceSettings.TLSCertFile, model.ServiceSettingsDefaultTLSCertFile),
"isdefault_tls_key_file": isDefault(*cfg.ServiceSettings.TLSKeyFile, model.ServiceSettingsDefaultTLSKeyFile),
"isdefault_read_timeout": isDefault(*cfg.ServiceSettings.ReadTimeout, model.ServiceSettingsDefaultReadTimeout),
"isdefault_write_timeout": isDefault(*cfg.ServiceSettings.WriteTimeout, model.ServiceSettingsDefaultWriteTimeout),
"isdefault_idle_timeout": isDefault(*cfg.ServiceSettings.IdleTimeout, model.ServiceSettingsDefaultIdleTimeout),
"isdefault_google_developer_key": isDefault(cfg.ServiceSettings.GoogleDeveloperKey, ""),
"isdefault_allow_cors_from": isDefault(*cfg.ServiceSettings.AllowCorsFrom, model.ServiceSettingsDefaultAllowCorsFrom),
"isdefault_cors_exposed_headers": isDefault(cfg.ServiceSettings.CorsExposedHeaders, ""),
"cors_allow_credentials": *cfg.ServiceSettings.CorsAllowCredentials,
"cors_debug": *cfg.ServiceSettings.CorsDebug,
"isdefault_allowed_untrusted_internal_connections": isDefault(*cfg.ServiceSettings.AllowedUntrustedInternalConnections, ""),
"post_edit_time_limit": *cfg.ServiceSettings.PostEditTimeLimit,
"enable_user_typing_messages": *cfg.ServiceSettings.EnableUserTypingMessages,
"enable_channel_viewed_messages": *cfg.ServiceSettings.EnableChannelViewedMessages,
"time_between_user_typing_updates_milliseconds": *cfg.ServiceSettings.TimeBetweenUserTypingUpdatesMilliseconds,
"cluster_log_timeout_milliseconds": *cfg.ServiceSettings.ClusterLogTimeoutMilliseconds,
"enable_post_search": *cfg.ServiceSettings.EnablePostSearch,
"minimum_hashtag_length": *cfg.ServiceSettings.MinimumHashtagLength,
"enable_user_statuses": *cfg.ServiceSettings.EnableUserStatuses,
"enable_preview_features": *cfg.ServiceSettings.EnablePreviewFeatures,
"enable_tutorial": *cfg.ServiceSettings.EnableTutorial,
"enable_onboarding_flow": *cfg.ServiceSettings.EnableOnboardingFlow,
"experimental_enable_default_channel_leave_join_messages": *cfg.ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages,
"experimental_group_unread_channels": *cfg.ServiceSettings.ExperimentalGroupUnreadChannels,
"collapsed_threads": *cfg.ServiceSettings.CollapsedThreads,
"websocket_url": isDefault(*cfg.ServiceSettings.WebsocketURL, ""),
"allow_cookies_for_subdomains": *cfg.ServiceSettings.AllowCookiesForSubdomains,
"enable_api_team_deletion": *cfg.ServiceSettings.EnableAPITeamDeletion,
"enable_api_trigger_admin_notification": *cfg.ServiceSettings.EnableAPITriggerAdminNotifications,
"enable_api_user_deletion": *cfg.ServiceSettings.EnableAPIUserDeletion,
"enable_api_channel_deletion": *cfg.ServiceSettings.EnableAPIChannelDeletion,
"experimental_enable_hardened_mode": *cfg.ServiceSettings.ExperimentalEnableHardenedMode,
"experimental_strict_csrf_enforcement": *cfg.ServiceSettings.ExperimentalStrictCSRFEnforcement,
"enable_email_invitations": *cfg.ServiceSettings.EnableEmailInvitations,
"disable_bots_when_owner_is_deactivated": *cfg.ServiceSettings.DisableBotsWhenOwnerIsDeactivated,
"enable_bot_account_creation": *cfg.ServiceSettings.EnableBotAccountCreation,
"enable_svgs": *cfg.ServiceSettings.EnableSVGs,
"enable_latex": *cfg.ServiceSettings.EnableLatex,
"enable_inline_latex": *cfg.ServiceSettings.EnableInlineLatex,
"enable_opentracing": *cfg.ServiceSettings.EnableOpenTracing,
"enable_local_mode": *cfg.ServiceSettings.EnableLocalMode,
"managed_resource_paths": isDefault(*cfg.ServiceSettings.ManagedResourcePaths, ""),
"thread_auto_follow": *cfg.ServiceSettings.ThreadAutoFollow,
"enable_link_previews": *cfg.ServiceSettings.EnableLinkPreviews,
"enable_permalink_previews": *cfg.ServiceSettings.EnablePermalinkPreviews,
"enable_file_search": *cfg.ServiceSettings.EnableFileSearch,
"restrict_link_previews": isDefault(*cfg.ServiceSettings.RestrictLinkPreviews, ""),
"enable_custom_groups": *cfg.ServiceSettings.EnableCustomGroups,
"post_priority": *cfg.ServiceSettings.PostPriority,
"self_hosted_purchase": *cfg.ServiceSettings.SelfHostedPurchase,
"allow_synced_drafts": *cfg.ServiceSettings.AllowSyncedDrafts,
"self_hosted_expansion": *cfg.ServiceSettings.SelfHostedExpansion,
})
ts.SendTelemetry(TrackConfigTeam, map[string]any{
"enable_user_creation": cfg.TeamSettings.EnableUserCreation,
"enable_open_server": *cfg.TeamSettings.EnableOpenServer,
"enable_user_deactivation": *cfg.TeamSettings.EnableUserDeactivation,
"enable_custom_user_statuses": *cfg.TeamSettings.EnableCustomUserStatuses,
"enable_last_active_time": *cfg.TeamSettings.EnableLastActiveTime,
"enable_custom_brand": *cfg.TeamSettings.EnableCustomBrand,
"restrict_direct_message": *cfg.TeamSettings.RestrictDirectMessage,
"max_notifications_per_channel": *cfg.TeamSettings.MaxNotificationsPerChannel,
"enable_confirm_notifications_to_channel": *cfg.TeamSettings.EnableConfirmNotificationsToChannel,
"max_users_per_team": *cfg.TeamSettings.MaxUsersPerTeam,
"max_channels_per_team": *cfg.TeamSettings.MaxChannelsPerTeam,
"teammate_name_display": *cfg.TeamSettings.TeammateNameDisplay,
"experimental_view_archived_channels": *cfg.TeamSettings.ExperimentalViewArchivedChannels,
"lock_teammate_name_display": *cfg.TeamSettings.LockTeammateNameDisplay,
"isdefault_site_name": isDefault(cfg.TeamSettings.SiteName, "Mattermost"),
"isdefault_custom_brand_text": isDefault(*cfg.TeamSettings.CustomBrandText, model.TeamSettingsDefaultCustomBrandText),
"isdefault_custom_description_text": isDefault(*cfg.TeamSettings.CustomDescriptionText, model.TeamSettingsDefaultCustomDescriptionText),
"isdefault_user_status_away_timeout": isDefault(*cfg.TeamSettings.UserStatusAwayTimeout, model.TeamSettingsDefaultUserStatusAwayTimeout),
"experimental_enable_automatic_replies": *cfg.TeamSettings.ExperimentalEnableAutomaticReplies,
"experimental_primary_team": isDefault(*cfg.TeamSettings.ExperimentalPrimaryTeam, ""),
"experimental_default_channels": len(cfg.TeamSettings.ExperimentalDefaultChannels),
})
ts.SendTelemetry(TrackConfigClientReq, map[string]any{
"android_latest_version": cfg.ClientRequirements.AndroidLatestVersion,
"android_min_version": cfg.ClientRequirements.AndroidMinVersion,
"ios_latest_version": cfg.ClientRequirements.IosLatestVersion,
"ios_min_version": cfg.ClientRequirements.IosMinVersion,
})
ts.SendTelemetry(TrackConfigSQL, map[string]any{
"driver_name": *cfg.SqlSettings.DriverName,
"trace": cfg.SqlSettings.Trace,
"max_idle_conns": *cfg.SqlSettings.MaxIdleConns,
"conn_max_lifetime_milliseconds": *cfg.SqlSettings.ConnMaxLifetimeMilliseconds,
"conn_max_idletime_milliseconds": *cfg.SqlSettings.ConnMaxIdleTimeMilliseconds,
"max_open_conns": *cfg.SqlSettings.MaxOpenConns,
"data_source_replicas": len(cfg.SqlSettings.DataSourceReplicas),
"data_source_search_replicas": len(cfg.SqlSettings.DataSourceSearchReplicas),
"query_timeout": *cfg.SqlSettings.QueryTimeout,
"disable_database_search": *cfg.SqlSettings.DisableDatabaseSearch,
"migrations_statement_timeout_seconds": *cfg.SqlSettings.MigrationsStatementTimeoutSeconds,
})
ts.SendTelemetry(TrackConfigLog, map[string]any{
"enable_console": cfg.LogSettings.EnableConsole,
"console_level": cfg.LogSettings.ConsoleLevel,
"console_json": *cfg.LogSettings.ConsoleJson,
"enable_file": cfg.LogSettings.EnableFile,
"file_level": cfg.LogSettings.FileLevel,
"file_json": cfg.LogSettings.FileJson,
"enable_webhook_debugging": cfg.LogSettings.EnableWebhookDebugging,
"isdefault_file_location": isDefault(cfg.LogSettings.FileLocation, ""),
"advanced_logging_config": *cfg.LogSettings.AdvancedLoggingConfig != "",
})
ts.SendTelemetry(TrackConfigAudit, map[string]any{
"file_enabled": *cfg.ExperimentalAuditSettings.FileEnabled,
"file_max_size_mb": *cfg.ExperimentalAuditSettings.FileMaxSizeMB,
"file_max_age_days": *cfg.ExperimentalAuditSettings.FileMaxAgeDays,
"file_max_backups": *cfg.ExperimentalAuditSettings.FileMaxBackups,
"file_compress": *cfg.ExperimentalAuditSettings.FileCompress,
"file_max_queue_size": *cfg.ExperimentalAuditSettings.FileMaxQueueSize,
"advanced_logging_config": *cfg.ExperimentalAuditSettings.AdvancedLoggingConfig != "",
})
ts.SendTelemetry(TrackConfigNotificationLog, map[string]any{
"enable_console": *cfg.NotificationLogSettings.EnableConsole,
"console_level": *cfg.NotificationLogSettings.ConsoleLevel,
"console_json": *cfg.NotificationLogSettings.ConsoleJson,
"enable_file": *cfg.NotificationLogSettings.EnableFile,
"file_level": *cfg.NotificationLogSettings.FileLevel,
"file_json": *cfg.NotificationLogSettings.FileJson,
"isdefault_file_location": isDefault(*cfg.NotificationLogSettings.FileLocation, ""),
"advanced_logging_config": *cfg.NotificationLogSettings.AdvancedLoggingConfig != "",
})
ts.SendTelemetry(TrackConfigPassword, map[string]any{
"minimum_length": *cfg.PasswordSettings.MinimumLength,
"lowercase": *cfg.PasswordSettings.Lowercase,
"number": *cfg.PasswordSettings.Number,
"uppercase": *cfg.PasswordSettings.Uppercase,
"symbol": *cfg.PasswordSettings.Symbol,
})
ts.SendTelemetry(TrackConfigFile, map[string]any{
"enable_public_links": cfg.FileSettings.EnablePublicLink,
"driver_name": *cfg.FileSettings.DriverName,
"isdefault_directory": isDefault(*cfg.FileSettings.Directory, model.FileSettingsDefaultDirectory),
"isabsolute_directory": filepath.IsAbs(*cfg.FileSettings.Directory),
"extract_content": *cfg.FileSettings.ExtractContent,
"archive_recursion": *cfg.FileSettings.ArchiveRecursion,
"amazon_s3_ssl": *cfg.FileSettings.AmazonS3SSL,
"amazon_s3_sse": *cfg.FileSettings.AmazonS3SSE,
"amazon_s3_signv2": *cfg.FileSettings.AmazonS3SignV2,
"amazon_s3_trace": *cfg.FileSettings.AmazonS3Trace,
"max_file_size": *cfg.FileSettings.MaxFileSize,
"max_image_resolution": *cfg.FileSettings.MaxImageResolution,
"max_image_decoder_concurrency": *cfg.FileSettings.MaxImageDecoderConcurrency,
"enable_file_attachments": *cfg.FileSettings.EnableFileAttachments,
"enable_mobile_upload": *cfg.FileSettings.EnableMobileUpload,
"enable_mobile_download": *cfg.FileSettings.EnableMobileDownload,
})
ts.SendTelemetry(TrackConfigEmail, map[string]any{
"enable_sign_up_with_email": cfg.EmailSettings.EnableSignUpWithEmail,
"enable_sign_in_with_email": *cfg.EmailSettings.EnableSignInWithEmail,
"enable_sign_in_with_username": *cfg.EmailSettings.EnableSignInWithUsername,
"require_email_verification": cfg.EmailSettings.RequireEmailVerification,
"send_email_notifications": cfg.EmailSettings.SendEmailNotifications,
"use_channel_in_email_notifications": *cfg.EmailSettings.UseChannelInEmailNotifications,
"email_notification_contents_type": *cfg.EmailSettings.EmailNotificationContentsType,
"enable_smtp_auth": *cfg.EmailSettings.EnableSMTPAuth,
"connection_security": cfg.EmailSettings.ConnectionSecurity,
"send_push_notifications": *cfg.EmailSettings.SendPushNotifications,
"push_notification_contents": *cfg.EmailSettings.PushNotificationContents,
"enable_email_batching": *cfg.EmailSettings.EnableEmailBatching,
"email_batching_buffer_size": *cfg.EmailSettings.EmailBatchingBufferSize,
"email_batching_interval": *cfg.EmailSettings.EmailBatchingInterval,
"enable_preview_mode_banner": *cfg.EmailSettings.EnablePreviewModeBanner,
"isdefault_feedback_name": isDefault(cfg.EmailSettings.FeedbackName, ""),
"isdefault_feedback_email": isDefault(cfg.EmailSettings.FeedbackEmail, ""),
"isdefault_reply_to_address": isDefault(cfg.EmailSettings.ReplyToAddress, ""),
"isdefault_feedback_organization": isDefault(*cfg.EmailSettings.FeedbackOrganization, model.EmailSettingsDefaultFeedbackOrganization),
"skip_server_certificate_verification": *cfg.EmailSettings.SkipServerCertificateVerification,
"isdefault_login_button_color": isDefault(*cfg.EmailSettings.LoginButtonColor, ""),
"isdefault_login_button_border_color": isDefault(*cfg.EmailSettings.LoginButtonBorderColor, ""),
"isdefault_login_button_text_color": isDefault(*cfg.EmailSettings.LoginButtonTextColor, ""),
"smtp_server_timeout": *cfg.EmailSettings.SMTPServerTimeout,
})
ts.SendTelemetry(TrackConfigRate, map[string]any{
"enable_rate_limiter": *cfg.RateLimitSettings.Enable,
"vary_by_remote_address": *cfg.RateLimitSettings.VaryByRemoteAddr,
"vary_by_user": *cfg.RateLimitSettings.VaryByUser,
"per_sec": *cfg.RateLimitSettings.PerSec,
"max_burst": *cfg.RateLimitSettings.MaxBurst,
"memory_store_size": *cfg.RateLimitSettings.MemoryStoreSize,
"isdefault_vary_by_header": isDefault(cfg.RateLimitSettings.VaryByHeader, ""),
})
ts.SendTelemetry(TrackConfigPrivacy, map[string]any{
"show_email_address": cfg.PrivacySettings.ShowEmailAddress,
"show_full_name": cfg.PrivacySettings.ShowFullName,
})
ts.SendTelemetry(TrackConfigTheme, map[string]any{
"enable_theme_selection": *cfg.ThemeSettings.EnableThemeSelection,
"isdefault_default_theme": isDefault(*cfg.ThemeSettings.DefaultTheme, model.TeamSettingsDefaultTeamText),
"allow_custom_themes": *cfg.ThemeSettings.AllowCustomThemes,
"allowed_themes": len(cfg.ThemeSettings.AllowedThemes),
})
ts.SendTelemetry(TrackConfigOAuth, map[string]any{
"enable_gitlab": cfg.GitLabSettings.Enable,
"openid_gitlab": *cfg.GitLabSettings.Enable && strings.Contains(*cfg.GitLabSettings.Scope, model.ServiceOpenid),
"enable_google": cfg.GoogleSettings.Enable,
"openid_google": *cfg.GoogleSettings.Enable && strings.Contains(*cfg.GoogleSettings.Scope, model.ServiceOpenid),
"enable_office365": cfg.Office365Settings.Enable,
"openid_office365": *cfg.Office365Settings.Enable && strings.Contains(*cfg.Office365Settings.Scope, model.ServiceOpenid),
"enable_openid": cfg.OpenIdSettings.Enable,
})
ts.SendTelemetry(TrackConfigSupport, map[string]any{
"isdefault_terms_of_service_link": isDefault(*cfg.SupportSettings.TermsOfServiceLink, model.SupportSettingsDefaultTermsOfServiceLink),
"isdefault_privacy_policy_link": isDefault(*cfg.SupportSettings.PrivacyPolicyLink, model.SupportSettingsDefaultPrivacyPolicyLink),
"isdefault_about_link": isDefault(*cfg.SupportSettings.AboutLink, model.SupportSettingsDefaultAboutLink),
"isdefault_help_link": isDefault(*cfg.SupportSettings.HelpLink, model.SupportSettingsDefaultHelpLink),
"isdefault_report_a_problem_link": isDefault(*cfg.SupportSettings.ReportAProblemLink, model.SupportSettingsDefaultReportAProblemLink),
"isdefault_support_email": isDefault(*cfg.SupportSettings.SupportEmail, model.SupportSettingsDefaultSupportEmail),
"custom_terms_of_service_enabled": *cfg.SupportSettings.CustomTermsOfServiceEnabled,
"custom_terms_of_service_re_acceptance_period": *cfg.SupportSettings.CustomTermsOfServiceReAcceptancePeriod,
"enable_ask_community_link": *cfg.SupportSettings.EnableAskCommunityLink,
})
ts.SendTelemetry(TrackConfigLDAP, map[string]any{
"enable": *cfg.LdapSettings.Enable,
"enable_sync": *cfg.LdapSettings.EnableSync,
"enable_admin_filter": *cfg.LdapSettings.EnableAdminFilter,
"connection_security": *cfg.LdapSettings.ConnectionSecurity,
"skip_certificate_verification": *cfg.LdapSettings.SkipCertificateVerification,
"sync_interval_minutes": *cfg.LdapSettings.SyncIntervalMinutes,
"query_timeout": *cfg.LdapSettings.QueryTimeout,
"max_page_size": *cfg.LdapSettings.MaxPageSize,
"isdefault_first_name_attribute": isDefault(*cfg.LdapSettings.FirstNameAttribute, model.LdapSettingsDefaultFirstNameAttribute),
"isdefault_last_name_attribute": isDefault(*cfg.LdapSettings.LastNameAttribute, model.LdapSettingsDefaultLastNameAttribute),
"isdefault_email_attribute": isDefault(*cfg.LdapSettings.EmailAttribute, model.LdapSettingsDefaultEmailAttribute),
"isdefault_username_attribute": isDefault(*cfg.LdapSettings.UsernameAttribute, model.LdapSettingsDefaultUsernameAttribute),
"isdefault_nickname_attribute": isDefault(*cfg.LdapSettings.NicknameAttribute, model.LdapSettingsDefaultNicknameAttribute),
"isdefault_id_attribute": isDefault(*cfg.LdapSettings.IdAttribute, model.LdapSettingsDefaultIdAttribute),
"isdefault_position_attribute": isDefault(*cfg.LdapSettings.PositionAttribute, model.LdapSettingsDefaultPositionAttribute),
"isdefault_login_id_attribute": isDefault(*cfg.LdapSettings.LoginIdAttribute, ""),
"isdefault_login_field_name": isDefault(*cfg.LdapSettings.LoginFieldName, model.LdapSettingsDefaultLoginFieldName),
"isdefault_login_button_color": isDefault(*cfg.LdapSettings.LoginButtonColor, ""),
"isdefault_login_button_border_color": isDefault(*cfg.LdapSettings.LoginButtonBorderColor, ""),
"isdefault_login_button_text_color": isDefault(*cfg.LdapSettings.LoginButtonTextColor, ""),
"isempty_group_filter": isDefault(*cfg.LdapSettings.GroupFilter, ""),
"isdefault_group_display_name_attribute": isDefault(*cfg.LdapSettings.GroupDisplayNameAttribute, model.LdapSettingsDefaultGroupDisplayNameAttribute),
"isdefault_group_id_attribute": isDefault(*cfg.LdapSettings.GroupIdAttribute, model.LdapSettingsDefaultGroupIdAttribute),
"isempty_guest_filter": isDefault(*cfg.LdapSettings.GuestFilter, ""),
"isempty_admin_filter": isDefault(*cfg.LdapSettings.AdminFilter, ""),
"isnotempty_picture_attribute": !isDefault(*cfg.LdapSettings.PictureAttribute, ""),
"isnotempty_public_certificate": !isDefault(*cfg.LdapSettings.PublicCertificateFile, ""),
"isnotempty_private_key": !isDefault(*cfg.LdapSettings.PrivateKeyFile, ""),
})
ts.SendTelemetry(TrackConfigCompliance, map[string]any{
"enable": *cfg.ComplianceSettings.Enable,
"enable_daily": *cfg.ComplianceSettings.EnableDaily,
})
ts.SendTelemetry(TrackConfigLocalization, map[string]any{
"default_server_locale": *cfg.LocalizationSettings.DefaultServerLocale,
"default_client_locale": *cfg.LocalizationSettings.DefaultClientLocale,
"available_locales": *cfg.LocalizationSettings.AvailableLocales,
})
ts.SendTelemetry(TrackConfigSAML, map[string]any{
"enable": *cfg.SamlSettings.Enable,
"enable_sync_with_ldap": *cfg.SamlSettings.EnableSyncWithLdap,
"enable_sync_with_ldap_include_auth": *cfg.SamlSettings.EnableSyncWithLdapIncludeAuth,
"ignore_guests_ldap_sync": *cfg.SamlSettings.IgnoreGuestsLdapSync,
"enable_admin_attribute": *cfg.SamlSettings.EnableAdminAttribute,
"verify": *cfg.SamlSettings.Verify,
"encrypt": *cfg.SamlSettings.Encrypt,
"sign_request": *cfg.SamlSettings.SignRequest,
"isdefault_signature_algorithm": isDefault(*cfg.SamlSettings.SignatureAlgorithm, ""),
"isdefault_canonical_algorithm": isDefault(*cfg.SamlSettings.CanonicalAlgorithm, ""),
"isdefault_scoping_idp_provider_id": isDefault(*cfg.SamlSettings.ScopingIDPProviderId, ""),
"isdefault_scoping_idp_name": isDefault(*cfg.SamlSettings.ScopingIDPName, ""),
"isdefault_id_attribute": isDefault(*cfg.SamlSettings.IdAttribute, model.SamlSettingsDefaultIdAttribute),
"isdefault_guest_attribute": isDefault(*cfg.SamlSettings.GuestAttribute, model.SamlSettingsDefaultGuestAttribute),
"isdefault_admin_attribute": isDefault(*cfg.SamlSettings.AdminAttribute, model.SamlSettingsDefaultAdminAttribute),
"isdefault_first_name_attribute": isDefault(*cfg.SamlSettings.FirstNameAttribute, model.SamlSettingsDefaultFirstNameAttribute),
"isdefault_last_name_attribute": isDefault(*cfg.SamlSettings.LastNameAttribute, model.SamlSettingsDefaultLastNameAttribute),
"isdefault_email_attribute": isDefault(*cfg.SamlSettings.EmailAttribute, model.SamlSettingsDefaultEmailAttribute),
"isdefault_username_attribute": isDefault(*cfg.SamlSettings.UsernameAttribute, model.SamlSettingsDefaultUsernameAttribute),
"isdefault_nickname_attribute": isDefault(*cfg.SamlSettings.NicknameAttribute, model.SamlSettingsDefaultNicknameAttribute),
"isdefault_locale_attribute": isDefault(*cfg.SamlSettings.LocaleAttribute, model.SamlSettingsDefaultLocaleAttribute),
"isdefault_position_attribute": isDefault(*cfg.SamlSettings.PositionAttribute, model.SamlSettingsDefaultPositionAttribute),
"isdefault_login_button_text": isDefault(*cfg.SamlSettings.LoginButtonText, model.UserAuthServiceSamlText),
"isdefault_login_button_color": isDefault(*cfg.SamlSettings.LoginButtonColor, ""),
"isdefault_login_button_border_color": isDefault(*cfg.SamlSettings.LoginButtonBorderColor, ""),
"isdefault_login_button_text_color": isDefault(*cfg.SamlSettings.LoginButtonTextColor, ""),
})
ts.SendTelemetry(TrackConfigCluster, map[string]any{
"enable": *cfg.ClusterSettings.Enable,
"network_interface": isDefault(*cfg.ClusterSettings.NetworkInterface, ""),
"bind_address": isDefault(*cfg.ClusterSettings.BindAddress, ""),
"advertise_address": isDefault(*cfg.ClusterSettings.AdvertiseAddress, ""),
"use_ip_address": *cfg.ClusterSettings.UseIPAddress,
"enable_experimental_gossip_encryption": *cfg.ClusterSettings.EnableExperimentalGossipEncryption,
"enable_gossip_compression": *cfg.ClusterSettings.EnableGossipCompression,
"read_only_config": *cfg.ClusterSettings.ReadOnlyConfig,
})
ts.SendTelemetry(TrackConfigMetrics, map[string]any{
"enable": *cfg.MetricsSettings.Enable,
"block_profile_rate": *cfg.MetricsSettings.BlockProfileRate,
})
ts.SendTelemetry(TrackConfigNativeApp, map[string]any{
"isdefault_app_custom_url_schemes": isDefaultArray(cfg.NativeAppSettings.AppCustomURLSchemes, model.GetDefaultAppCustomURLSchemes()),
"isdefault_app_download_link": isDefault(*cfg.NativeAppSettings.AppDownloadLink, model.NativeappSettingsDefaultAppDownloadLink),
"isdefault_android_app_download_link": isDefault(*cfg.NativeAppSettings.AndroidAppDownloadLink, model.NativeappSettingsDefaultAndroidAppDownloadLink),
"isdefault_iosapp_download_link": isDefault(*cfg.NativeAppSettings.IosAppDownloadLink, model.NativeappSettingsDefaultIosAppDownloadLink),
})
ts.SendTelemetry(TrackConfigExperimental, map[string]any{
"client_side_cert_enable": *cfg.ExperimentalSettings.ClientSideCertEnable,
"isdefault_client_side_cert_check": isDefault(*cfg.ExperimentalSettings.ClientSideCertCheck, model.ClientSideCertCheckPrimaryAuth),
"link_metadata_timeout_milliseconds": *cfg.ExperimentalSettings.LinkMetadataTimeoutMilliseconds,
"restrict_system_admin": *cfg.ExperimentalSettings.RestrictSystemAdmin,
"use_new_saml_library": *cfg.ExperimentalSettings.UseNewSAMLLibrary,
"enable_shared_channels": *cfg.ExperimentalSettings.EnableSharedChannels,
"enable_remote_cluster_service": *cfg.ExperimentalSettings.EnableRemoteClusterService && cfg.FeatureFlags.EnableRemoteClusterService,
"enable_app_bar": *cfg.ExperimentalSettings.EnableAppBar,
"patch_plugins_react_dom": *cfg.ExperimentalSettings.PatchPluginsReactDOM,
})
ts.SendTelemetry(TrackConfigAnalytics, map[string]any{
"isdefault_max_users_for_statistics": isDefault(*cfg.AnalyticsSettings.MaxUsersForStatistics, model.AnalyticsSettingsDefaultMaxUsersForStatistics),
})
ts.SendTelemetry(TrackConfigAnnouncement, map[string]any{
"enable_banner": *cfg.AnnouncementSettings.EnableBanner,
"isdefault_banner_color": isDefault(*cfg.AnnouncementSettings.BannerColor, model.AnnouncementSettingsDefaultBannerColor),
"isdefault_banner_text_color": isDefault(*cfg.AnnouncementSettings.BannerTextColor, model.AnnouncementSettingsDefaultBannerTextColor),
"allow_banner_dismissal": *cfg.AnnouncementSettings.AllowBannerDismissal,
"admin_notices_enabled": *cfg.AnnouncementSettings.AdminNoticesEnabled,
"user_notices_enabled": *cfg.AnnouncementSettings.UserNoticesEnabled,
})
ts.SendTelemetry(TrackConfigElasticsearch, map[string]any{
"isdefault_connection_url": isDefault(*cfg.ElasticsearchSettings.ConnectionURL, model.ElasticsearchSettingsDefaultConnectionURL),
"isdefault_username": isDefault(*cfg.ElasticsearchSettings.Username, model.ElasticsearchSettingsDefaultUsername),
"isdefault_password": isDefault(*cfg.ElasticsearchSettings.Password, model.ElasticsearchSettingsDefaultPassword),
"enable_indexing": *cfg.ElasticsearchSettings.EnableIndexing,
"enable_searching": *cfg.ElasticsearchSettings.EnableSearching,
"enable_autocomplete": *cfg.ElasticsearchSettings.EnableAutocomplete,
"sniff": *cfg.ElasticsearchSettings.Sniff,
"post_index_replicas": *cfg.ElasticsearchSettings.PostIndexReplicas,
"post_index_shards": *cfg.ElasticsearchSettings.PostIndexShards,
"channel_index_replicas": *cfg.ElasticsearchSettings.ChannelIndexReplicas,
"channel_index_shards": *cfg.ElasticsearchSettings.ChannelIndexShards,
"user_index_replicas": *cfg.ElasticsearchSettings.UserIndexReplicas,
"user_index_shards": *cfg.ElasticsearchSettings.UserIndexShards,
"isdefault_index_prefix": isDefault(*cfg.ElasticsearchSettings.IndexPrefix, model.ElasticsearchSettingsDefaultIndexPrefix),
"live_indexing_batch_size": *cfg.ElasticsearchSettings.LiveIndexingBatchSize,
"bulk_indexing_batch_size": *cfg.ElasticsearchSettings.BatchSize,
"request_timeout_seconds": *cfg.ElasticsearchSettings.RequestTimeoutSeconds,
"skip_tls_verification": *cfg.ElasticsearchSettings.SkipTLSVerification,
"isdefault_ca": isDefault(*cfg.ElasticsearchSettings.CA, ""),
"isdefault_client_cert": isDefault(*cfg.ElasticsearchSettings.ClientCert, ""),
"isdefault_client_key": isDefault(*cfg.ElasticsearchSettings.ClientKey, ""),
"trace": *cfg.ElasticsearchSettings.Trace,
})
ts.trackPluginConfig(cfg, model.PluginSettingsDefaultMarketplaceURL)
ts.SendTelemetry(TrackConfigDataRetention, map[string]any{
"enable_message_deletion": *cfg.DataRetentionSettings.EnableMessageDeletion,
"enable_file_deletion": *cfg.DataRetentionSettings.EnableFileDeletion,
"enable_boards_deletion": *cfg.DataRetentionSettings.EnableBoardsDeletion,
"message_retention_days": *cfg.DataRetentionSettings.MessageRetentionDays,
"file_retention_days": *cfg.DataRetentionSettings.FileRetentionDays,
"boards_retention_days": *cfg.DataRetentionSettings.BoardsRetentionDays,
"deletion_job_start_time": *cfg.DataRetentionSettings.DeletionJobStartTime,
"batch_size": *cfg.DataRetentionSettings.BatchSize,
"cleanup_jobs_threshold_days": *cfg.JobSettings.CleanupJobsThresholdDays,
"cleanup_config_threshold_days": *cfg.JobSettings.CleanupConfigThresholdDays,
})
ts.SendTelemetry(TrackConfigMessageExport, map[string]any{
"enable_message_export": *cfg.MessageExportSettings.EnableExport,
"export_format": *cfg.MessageExportSettings.ExportFormat,
"daily_run_time": *cfg.MessageExportSettings.DailyRunTime,
"default_export_from_timestamp": *cfg.MessageExportSettings.ExportFromTimestamp,
"batch_size": *cfg.MessageExportSettings.BatchSize,
"global_relay_customer_type": *cfg.MessageExportSettings.GlobalRelaySettings.CustomerType,
"is_default_global_relay_smtp_username": isDefault(*cfg.MessageExportSettings.GlobalRelaySettings.SMTPUsername, ""),
"is_default_global_relay_smtp_password": isDefault(*cfg.MessageExportSettings.GlobalRelaySettings.SMTPPassword, ""),
"is_default_global_relay_email_address": isDefault(*cfg.MessageExportSettings.GlobalRelaySettings.EmailAddress, ""),
"global_relay_smtp_server_timeout": *cfg.MessageExportSettings.GlobalRelaySettings.SMTPServerTimeout,
"download_export_results": *cfg.MessageExportSettings.DownloadExportResults,
})
ts.SendTelemetry(TrackConfigDisplay, map[string]any{
"experimental_timezone": *cfg.DisplaySettings.ExperimentalTimezone,
"isdefault_custom_url_schemes": len(cfg.DisplaySettings.CustomURLSchemes) != 0,
})
ts.SendTelemetry(TrackConfigGuestAccounts, map[string]any{
"enable": *cfg.GuestAccountsSettings.Enable,
"allow_email_accounts": *cfg.GuestAccountsSettings.AllowEmailAccounts,
"enforce_multifactor_authentication": *cfg.GuestAccountsSettings.EnforceMultifactorAuthentication,
"isdefault_restrict_creation_to_domains": isDefault(*cfg.GuestAccountsSettings.RestrictCreationToDomains, ""),
})
ts.SendTelemetry(TrackConfigImageProxy, map[string]any{
"enable": *cfg.ImageProxySettings.Enable,
"image_proxy_type": *cfg.ImageProxySettings.ImageProxyType,
"isdefault_remote_image_proxy_url": isDefault(*cfg.ImageProxySettings.RemoteImageProxyURL, ""),
"isdefault_remote_image_proxy_options": isDefault(*cfg.ImageProxySettings.RemoteImageProxyOptions, ""),
})
ts.SendTelemetry(TrackConfigBleve, map[string]any{
"enable_indexing": *cfg.BleveSettings.EnableIndexing,
"enable_searching": *cfg.BleveSettings.EnableSearching,
"enable_autocomplete": *cfg.BleveSettings.EnableAutocomplete,
"bulk_indexing_batch_size": *cfg.BleveSettings.BatchSize,
})
ts.SendTelemetry(TrackConfigExport, map[string]any{
"retention_days": *cfg.ExportSettings.RetentionDays,
})
ts.SendTelemetry(TrackConfigProducts, map[string]any{
"enable_public_shared_boards": *cfg.ProductSettings.EnablePublicSharedBoards,
})
// Convert feature flags to map[string]any for sending
flags := cfg.FeatureFlags.ToMap()
interfaceFlags := make(map[string]any)
for k, v := range flags {
interfaceFlags[k] = v
}
ts.SendTelemetry(TrackFeatureFlags, interfaceFlags)
}
func (ts *TelemetryService) trackLicense() {
if license := ts.srv.License(); license != nil {
data := map[string]any{
"customer_id": license.Customer.Id,
"license_id": license.Id,
"issued": license.IssuedAt,
"start": license.StartsAt,
"expire": license.ExpiresAt,
"users": *license.Features.Users,
"edition": license.SkuShortName,
}
features := license.Features.ToMap()
for featureName, featureValue := range features {
data["feature_"+featureName] = featureValue
}
ts.SendTelemetry(TrackLicense, data)
}
}
func (ts *TelemetryService) trackPlugins() {
pluginsEnvironment := ts.srv.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return
}
totalEnabledCount := 0
webappEnabledCount := 0
backendEnabledCount := 0
totalDisabledCount := 0
totalCoreDisabledCount := 0
webappDisabledCount := 0
backendDisabledCount := 0
brokenManifestCount := 0
settingsCount := 0
pluginStates := ts.srv.Config().PluginSettings.PluginStates
plugins, _ := pluginsEnvironment.Available()
if pluginStates != nil && plugins != nil {
for _, plugin := range plugins {
if plugin.Manifest == nil {
brokenManifestCount += 1
continue
}
if state, ok := pluginStates[plugin.Manifest.Id]; ok && state.Enable {
totalEnabledCount += 1
if plugin.Manifest.HasServer() {
backendEnabledCount += 1
}
if plugin.Manifest.HasWebapp() {
webappEnabledCount += 1
}
} else {
totalDisabledCount += 1
if plugin.Manifest.HasServer() {
backendDisabledCount += 1
}
if plugin.Manifest.HasWebapp() {
webappDisabledCount += 1
}
if _, isCorePlugin := model.InstalledIntegrationsIgnoredPlugins[plugin.Manifest.Id]; isCorePlugin {
totalCoreDisabledCount += 1
}
}
if plugin.Manifest.SettingsSchema != nil {
settingsCount += 1
}
}
} else {
totalEnabledCount = -1 // -1 to indicate disabled or error
totalCoreDisabledCount = -1 // -1 to indicate disabled or error
totalDisabledCount = -1 // -1 to indicate disabled or error
}
ts.SendTelemetry(TrackPlugins, map[string]any{
"enabled_plugins": totalEnabledCount,
"enabled_webapp_plugins": webappEnabledCount,
"enabled_backend_plugins": backendEnabledCount,
"disabled_plugins": totalDisabledCount,
"disabled_default_plugins": totalCoreDisabledCount,
"disabled_webapp_plugins": webappDisabledCount,
"disabled_backend_plugins": backendDisabledCount,
"plugins_with_settings": settingsCount,
"plugins_with_broken_manifests": brokenManifestCount,
})
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
hooks.OnSendDailyTelemetry()
return true
}, plugin.OnSendDailyTelemetryID)
}
func (ts *TelemetryService) trackProducts() {
hm := ts.srv.HooksManager()
if hm == nil {
return
}
hm.RunMultiHook(func(hooks plugin.Hooks) bool {
hooks.OnSendDailyTelemetry()
return true
}, plugin.OnSendDailyTelemetryID)
}
func (ts *TelemetryService) trackServer() {
data := map[string]any{
"edition": model.BuildEnterpriseReady,
"version": model.CurrentVersion,
"database_type": *ts.srv.Config().SqlSettings.DriverName,
"operating_system": runtime.GOOS,
"installation_type": os.Getenv(EnvVarInstallType),
}
if scr, err := ts.dbStore.User().AnalyticsGetSystemAdminCount(); err == nil {
data["system_admins"] = scr
}
if scr, err := ts.dbStore.GetDbVersion(false); err == nil {
data["database_version"] = scr
}
ts.SendTelemetry(TrackServer, data)
}
func (ts *TelemetryService) trackPermissions() {
phase1Complete := false
if _, err := ts.dbStore.System().GetByName(model.AdvancedPermissionsMigrationKey); err == nil {
phase1Complete = true
}
phase2Complete := false
if _, err := ts.dbStore.System().GetByName(model.MigrationKeyAdvancedPermissionsPhase2); err == nil {
phase2Complete = true
}
ts.SendTelemetry(TrackPermissionsGeneral, map[string]any{
"phase_1_migration_complete": phase1Complete,
"phase_2_migration_complete": phase2Complete,
})
systemAdminPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), model.SystemAdminRoleId); err == nil {
systemAdminPermissions = strings.Join(role.Permissions, " ")
}
systemUserPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), model.SystemUserRoleId); err == nil {
systemUserPermissions = strings.Join(role.Permissions, " ")
}
teamAdminPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), model.TeamAdminRoleId); err == nil {
teamAdminPermissions = strings.Join(role.Permissions, " ")
}
teamUserPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), model.TeamUserRoleId); err == nil {
teamUserPermissions = strings.Join(role.Permissions, " ")
}
teamGuestPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), model.TeamGuestRoleId); err == nil {
teamGuestPermissions = strings.Join(role.Permissions, " ")
}
channelAdminPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), model.ChannelAdminRoleId); err == nil {
channelAdminPermissions = strings.Join(role.Permissions, " ")
}
channelUserPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), model.ChannelUserRoleId); err == nil {
channelUserPermissions = strings.Join(role.Permissions, " ")
}
channelGuestPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), model.ChannelGuestRoleId); err == nil {
channelGuestPermissions = strings.Join(role.Permissions, " ")
}
systemManagerPermissions := ""
systemManagerPermissionsModified := false
if role, err := ts.srv.GetRoleByName(context.Background(), model.SystemManagerRoleId); err == nil {
systemManagerPermissionsModified = len(model.PermissionsChangedByPatch(role, &model.RolePatch{Permissions: &model.SystemManagerDefaultPermissions})) > 0
systemManagerPermissions = strings.Join(role.Permissions, " ")
}
systemManagerCount, countErr := ts.dbStore.User().Count(model.UserCountOptions{Roles: []string{model.SystemManagerRoleId}})
if countErr != nil {
systemManagerCount = 0
}
systemUserManagerPermissions := ""
systemUserManagerPermissionsModified := false
if role, err := ts.srv.GetRoleByName(context.Background(), model.SystemUserManagerRoleId); err == nil {
systemUserManagerPermissionsModified = len(model.PermissionsChangedByPatch(role, &model.RolePatch{Permissions: &model.SystemUserManagerDefaultPermissions})) > 0
systemUserManagerPermissions = strings.Join(role.Permissions, " ")
}
systemUserManagerCount, countErr := ts.dbStore.User().Count(model.UserCountOptions{Roles: []string{model.SystemUserManagerRoleId}})
if countErr != nil {
systemManagerCount = 0
}
systemReadOnlyAdminPermissions := ""
systemReadOnlyAdminPermissionsModified := false
if role, err := ts.srv.GetRoleByName(context.Background(), model.SystemReadOnlyAdminRoleId); err == nil {
systemReadOnlyAdminPermissionsModified = len(model.PermissionsChangedByPatch(role, &model.RolePatch{Permissions: &model.SystemReadOnlyAdminDefaultPermissions})) > 0
systemReadOnlyAdminPermissions = strings.Join(role.Permissions, " ")
}
systemReadOnlyAdminCount, countErr := ts.dbStore.User().Count(model.UserCountOptions{Roles: []string{model.SystemReadOnlyAdminRoleId}})
if countErr != nil {
systemReadOnlyAdminCount = 0
}
systemCustomGroupAdminPermissions := ""
systemCustomGroupAdminPermissionsModified := false
if role, err := ts.srv.GetRoleByName(context.Background(), model.SystemCustomGroupAdminRoleId); err == nil {
systemCustomGroupAdminPermissionsModified = len(model.PermissionsChangedByPatch(role, &model.RolePatch{Permissions: &model.SystemReadOnlyAdminDefaultPermissions})) > 0
systemCustomGroupAdminPermissions = strings.Join(role.Permissions, " ")
}
systemCustomGroupAdminCount, countErr := ts.dbStore.User().Count(model.UserCountOptions{Roles: []string{model.SystemCustomGroupAdminRoleId}})
if countErr != nil {
systemCustomGroupAdminCount = 0
}
ts.SendTelemetry(TrackPermissionsSystemScheme, map[string]any{
"system_admin_permissions": systemAdminPermissions,
"system_user_permissions": systemUserPermissions,
"system_manager_permissions": systemManagerPermissions,
"system_user_manager_permissions": systemUserManagerPermissions,
"system_read_only_admin_permissions": systemReadOnlyAdminPermissions,
"team_admin_permissions": teamAdminPermissions,
"team_user_permissions": teamUserPermissions,
"team_guest_permissions": teamGuestPermissions,
"channel_admin_permissions": channelAdminPermissions,
"channel_user_permissions": channelUserPermissions,
"channel_guest_permissions": channelGuestPermissions,
"system_manager_permissions_modified": systemManagerPermissionsModified,
"system_manager_count": systemManagerCount,
"system_user_manager_permissions_modified": systemUserManagerPermissionsModified,
"system_user_manager_count": systemUserManagerCount,
"system_read_only_admin_permissions_modified": systemReadOnlyAdminPermissionsModified,
"system_read_only_admin_count": systemReadOnlyAdminCount,
"system_custom_group_admin_permissions": systemCustomGroupAdminPermissions,
"system_custom_group_admin_permissions_modified": systemCustomGroupAdminPermissionsModified,
"system_custom_group_admin_count": systemCustomGroupAdminCount,
})
if schemes, err := ts.srv.GetSchemes(model.SchemeScopeTeam, 0, 100); err == nil {
for _, scheme := range schemes {
teamAdminPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), scheme.DefaultTeamAdminRole); err == nil {
teamAdminPermissions = strings.Join(role.Permissions, " ")
}
teamUserPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), scheme.DefaultTeamUserRole); err == nil {
teamUserPermissions = strings.Join(role.Permissions, " ")
}
teamGuestPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), scheme.DefaultTeamGuestRole); err == nil {
teamGuestPermissions = strings.Join(role.Permissions, " ")
}
channelAdminPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), scheme.DefaultChannelAdminRole); err == nil {
channelAdminPermissions = strings.Join(role.Permissions, " ")
}
channelUserPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), scheme.DefaultChannelUserRole); err == nil {
channelUserPermissions = strings.Join(role.Permissions, " ")
}
channelGuestPermissions := ""
if role, err := ts.srv.GetRoleByName(context.Background(), scheme.DefaultChannelGuestRole); err == nil {
channelGuestPermissions = strings.Join(role.Permissions, " ")
}
count, _ := ts.dbStore.Team().AnalyticsGetTeamCountForScheme(scheme.Id)
ts.SendTelemetry(TrackPermissionsTeamSchemes, map[string]any{
"scheme_id": scheme.Id,
"team_admin_permissions": teamAdminPermissions,
"team_user_permissions": teamUserPermissions,
"team_guest_permissions": teamGuestPermissions,
"channel_admin_permissions": channelAdminPermissions,
"channel_user_permissions": channelUserPermissions,
"channel_guest_permissions": channelGuestPermissions,
"team_count": count,
})
}
}
}
func (ts *TelemetryService) trackElasticsearch() {
data := map[string]any{}
for _, engine := range ts.searchEngine.GetActiveEngines() {
if engine.GetVersion() != 0 && engine.GetName() == "elasticsearch" {
data["elasticsearch_server_version"] = engine.GetVersion()
}
}
ts.SendTelemetry(TrackElasticsearch, data)
}
func (ts *TelemetryService) trackGroups() {
groupCount, err := ts.dbStore.Group().GroupCount()
if err != nil {
ts.log.Debug("Could not get group_count", mlog.Err(err))
}
ldapGroupCount, err := ts.dbStore.Group().GroupCountBySource(model.GroupSourceLdap)
if err != nil {
ts.log.Debug("Could not get group_count", mlog.Err(err))
}
customGroupCount, err := ts.dbStore.Group().GroupCountBySource(model.GroupSourceCustom)
if err != nil {
ts.log.Debug("Could not get group_count", mlog.Err(err))
}
groupTeamCount, err := ts.dbStore.Group().GroupTeamCount()
if err != nil {
ts.log.Debug("Could not get group_team_count", mlog.Err(err))
}
groupChannelCount, err := ts.dbStore.Group().GroupChannelCount()
if err != nil {
ts.log.Debug("Could not get group_channel_count", mlog.Err(err))
}
groupSyncedTeamCount, nErr := ts.dbStore.Team().GroupSyncedTeamCount()
if nErr != nil {
ts.log.Debug("Could not get group_synced_team_count", mlog.Err(nErr))
}
groupSyncedChannelCount, nErr := ts.dbStore.Channel().GroupSyncedChannelCount()
if nErr != nil {
ts.log.Debug("Could not get group_synced_channel_count", mlog.Err(nErr))
}
groupMemberCount, err := ts.dbStore.Group().GroupMemberCount()
if err != nil {
ts.log.Debug("Could not get group_member_count", mlog.Err(err))
}
distinctGroupMemberCount, err := ts.dbStore.Group().DistinctGroupMemberCount()
if err != nil {
ts.log.Debug("Could not get distinct_group_member_count", mlog.Err(err))
}
distinctCustomGroupMemberCount, err := ts.dbStore.Group().DistinctGroupMemberCountForSource(model.GroupSourceCustom)
if err != nil {
ts.log.Debug("Could not get distinct_custom_group_member_count", mlog.Err(err))
}
distinctLdapGroupMemberCount, err := ts.dbStore.Group().DistinctGroupMemberCountForSource(model.GroupSourceLdap)
if err != nil {
ts.log.Debug("Could not get distinct_ldap_group_member_count", mlog.Err(err))
}
groupCountWithAllowReference, err := ts.dbStore.Group().GroupCountWithAllowReference()
if err != nil {
ts.log.Debug("Could not get group_count_with_allow_reference", mlog.Err(err))
}
ts.SendTelemetry(TrackGroups, map[string]any{
"group_count": groupCount,
"ldap_group_count": ldapGroupCount,
"custom_group_count": customGroupCount,
"group_team_count": groupTeamCount,
"group_channel_count": groupChannelCount,
"group_synced_team_count": groupSyncedTeamCount,
"group_synced_channel_count": groupSyncedChannelCount,
"group_member_count": groupMemberCount,
"distinct_group_member_count": distinctGroupMemberCount,
"distinct_custom_group_member_count": distinctCustomGroupMemberCount,
"distinct_ldap_group_member_count": distinctLdapGroupMemberCount,
"group_count_with_allow_reference": groupCountWithAllowReference,
})
}
func (ts *TelemetryService) trackChannelModeration() {
channelSchemeCount, err := ts.dbStore.Scheme().CountByScope(model.SchemeScopeChannel)
if err != nil {
ts.log.Debug("Could not get channel_scheme_count", mlog.Err(err))
}
createPostUser, err := ts.dbStore.Scheme().CountWithoutPermission(model.SchemeScopeChannel, model.PermissionCreatePost.Id, model.RoleScopeChannel, model.RoleTypeUser)
if err != nil {
ts.log.Debug("Could not get create_post_user_disabled_count", mlog.Err(err))
}
createPostGuest, err := ts.dbStore.Scheme().CountWithoutPermission(model.SchemeScopeChannel, model.PermissionCreatePost.Id, model.RoleScopeChannel, model.RoleTypeGuest)
if err != nil {
ts.log.Debug("Could not get create_post_guest_disabled_count", mlog.Err(err))
}
// only need to track one of 'add_reaction' or 'remove_reaction` because they're both toggled together by the channel moderation feature
postReactionsUser, err := ts.dbStore.Scheme().CountWithoutPermission(model.SchemeScopeChannel, model.PermissionAddReaction.Id, model.RoleScopeChannel, model.RoleTypeUser)
if err != nil {
ts.log.Debug("Could not get post_reactions_user_disabled_count", mlog.Err(err))
}
postReactionsGuest, err := ts.dbStore.Scheme().CountWithoutPermission(model.SchemeScopeChannel, model.PermissionAddReaction.Id, model.RoleScopeChannel, model.RoleTypeGuest)
if err != nil {
ts.log.Debug("Could not get post_reactions_guest_disabled_count", mlog.Err(err))
}
// only need to track one of 'manage_public_channel_members' or 'manage_private_channel_members` because they're both toggled together by the channel moderation feature
manageMembersUser, err := ts.dbStore.Scheme().CountWithoutPermission(model.SchemeScopeChannel, model.PermissionManagePublicChannelMembers.Id, model.RoleScopeChannel, model.RoleTypeUser)
if err != nil {
ts.log.Debug("Could not get manage_members_user_disabled_count", mlog.Err(err))
}
useChannelMentionsUser, err := ts.dbStore.Scheme().CountWithoutPermission(model.SchemeScopeChannel, model.PermissionUseChannelMentions.Id, model.RoleScopeChannel, model.RoleTypeUser)
if err != nil {
ts.log.Debug("Could not get use_channel_mentions_user_disabled_count", mlog.Err(err))
}
useChannelMentionsGuest, err := ts.dbStore.Scheme().CountWithoutPermission(model.SchemeScopeChannel, model.PermissionUseChannelMentions.Id, model.RoleScopeChannel, model.RoleTypeGuest)
if err != nil {
ts.log.Debug("Could not get use_channel_mentions_guest_disabled_count", mlog.Err(err))
}
ts.SendTelemetry(TrackChannelModeration, map[string]any{
"channel_scheme_count": channelSchemeCount,
"create_post_user_disabled_count": createPostUser,
"create_post_guest_disabled_count": createPostGuest,
"post_reactions_user_disabled_count": postReactionsUser,
"post_reactions_guest_disabled_count": postReactionsGuest,
"manage_members_user_disabled_count": manageMembersUser, // the UI does not allow this to be removed for guests
"use_channel_mentions_user_disabled_count": useChannelMentionsUser,
"use_channel_mentions_guest_disabled_count": useChannelMentionsGuest,
})
}
func (ts *TelemetryService) initRudder(endpoint string, rudderKey string) {
if ts.rudderClient == nil {
config := rudder.Config{}
config.Logger = rudder.StdLogger(ts.log.With(mlog.String("source", "rudder")).StdLogger(mlog.LvlDebug))
config.Endpoint = endpoint
config.Verbose = ts.verbose
// For testing
if endpoint != RudderDataplaneURL {
config.BatchSize = 1
}
client, err := rudder.NewWithConfig(rudderKey, endpoint, config)
if err != nil {
ts.log.Error("Failed to create Rudder instance", mlog.Err(err))
return
}
client.Enqueue(rudder.Identify{
UserId: ts.TelemetryID,
})
ts.rudderClient = client
}
}
func (ts *TelemetryService) doTelemetryIfNeeded(firstRun time.Time) {
hoursSinceFirstServerRun := time.Since(firstRun).Hours()
// Send once every 10 minutes for the first hour
// Send once every hour thereafter for the first 12 hours
// Send at the 24 hour mark and every 24 hours after
if hoursSinceFirstServerRun < 1 {
ts.doTelemetry()
} else if hoursSinceFirstServerRun <= 12 && time.Since(ts.timestampLastTelemetrySent) >= time.Hour {
ts.doTelemetry()
} else if hoursSinceFirstServerRun > 12 && time.Since(ts.timestampLastTelemetrySent) >= 24*time.Hour {
ts.doTelemetry()
}
}
func (ts *TelemetryService) RunTelemetryJob(firstRun int64) {
// Send on boot
ts.doTelemetry()
model.CreateRecurringTask("Telemetry", func() {
ts.doTelemetryIfNeeded(utils.TimeFromMillis(firstRun))
}, time.Minute*10)
}
func (ts *TelemetryService) doTelemetry() {
if *ts.srv.Config().LogSettings.EnableDiagnostics {
ts.timestampLastTelemetrySent = time.Now()
ts.sendDailyTelemetry(false)
}
}
// Shutdown closes the telemetry client.
func (ts *TelemetryService) Shutdown() error {
if ts.rudderClient != nil {
return ts.rudderClient.Close()
}
return nil
}
func (ts *TelemetryService) trackWarnMetrics() {
systemDataList, nErr := ts.dbStore.System().Get()
if nErr != nil {
return
}
for key, value := range systemDataList {
if strings.HasPrefix(key, model.WarnMetricStatusStorePrefix) {
if _, ok := model.WarnMetricsTable[key]; ok {
ts.SendTelemetry(TrackWarnMetrics, map[string]any{
key: value != "false",
})
}
}
}
}
func (ts *TelemetryService) trackPluginConfig(cfg *model.Config, marketplaceURL string) {
pluginConfigData := map[string]any{
"enable_nps_survey": pluginSetting(&cfg.PluginSettings, model.PluginIdNPS, "enablesurvey", true),
"enable": *cfg.PluginSettings.Enable,
"enable_uploads": *cfg.PluginSettings.EnableUploads,
"allow_insecure_download_url": *cfg.PluginSettings.AllowInsecureDownloadURL,
"enable_health_check": *cfg.PluginSettings.EnableHealthCheck,
"enable_marketplace": *cfg.PluginSettings.EnableMarketplace,
"require_pluginSignature": *cfg.PluginSettings.RequirePluginSignature,
"enable_remote_marketplace": *cfg.PluginSettings.EnableRemoteMarketplace,
"automatic_prepackaged_plugins": *cfg.PluginSettings.AutomaticPrepackagedPlugins,
"is_default_marketplace_url": isDefault(*cfg.PluginSettings.MarketplaceURL, model.PluginSettingsDefaultMarketplaceURL),
"signature_public_key_files": len(cfg.PluginSettings.SignaturePublicKeyFiles),
"chimera_oauth_proxy_url": *cfg.PluginSettings.ChimeraOAuthProxyURL,
}
// knownPluginIDs lists all known plugin IDs in the Marketplace
knownPluginIDs := []string{
"antivirus",
"com.github.manland.mattermost-plugin-gitlab",
"com.github.moussetc.mattermost.plugin.giphy",
"com.github.phillipahereza.mattermost-plugin-digitalocean",
"com.mattermost.aws-sns",
"com.mattermost.confluence",
"com.mattermost.custom-attributes",
"com.mattermost.mscalendar",
"com.mattermost.nps",
"com.mattermost.plugin-channel-export",
"com.mattermost.plugin-incident-management",
"playbooks",
"com.mattermost.plugin-todo",
"com.mattermost.webex",
"com.mattermost.welcomebot",
"github",
"jenkins",
"jira",
"jitsi",
"mattermost-autolink",
"memes",
"skype4business",
"zoom",
"focalboard",
}
marketplacePlugins, err := ts.GetAllMarketplacePlugins(marketplaceURL)
if err != nil {
mlog.Info("Failed to fetch marketplace plugins for telemetry. Using predefined list.", mlog.Err(err))
for _, id := range knownPluginIDs {
pluginConfigData["enable_"+id] = pluginActivated(cfg.PluginSettings.PluginStates, id)
}
} else {
for _, p := range marketplacePlugins {
id := p.Manifest.Id
pluginConfigData["enable_"+id] = pluginActivated(cfg.PluginSettings.PluginStates, id)
}
}
pluginsEnvironment := ts.srv.GetPluginsEnvironment()
if pluginsEnvironment != nil {
if plugins, appErr := pluginsEnvironment.Available(); appErr != nil {
ts.log.Warn("Unable to add plugin versions to telemetry", mlog.Err(appErr))
} else {
// If marketplace request failed, use predefined list
if marketplacePlugins == nil {
for _, id := range knownPluginIDs {
pluginConfigData["version_"+id] = pluginVersion(plugins, id)
}
} else {
for _, p := range marketplacePlugins {
id := p.Manifest.Id
pluginConfigData["version_"+id] = pluginVersion(plugins, id)
}
}
}
}
ts.SendTelemetry(TrackConfigPlugin, pluginConfigData)
}
func (ts *TelemetryService) GetAllMarketplacePlugins(marketplaceURL string) ([]*model.BaseMarketplacePlugin, error) {
marketplaceClient, err := marketplace.NewClient(
marketplaceURL,
ts.srv.HTTPService(),
)
if err != nil {
return nil, err
}
// Fetch all plugins from marketplace.
filter := &model.MarketplacePluginFilter{
PerPage: -1,
ServerVersion: model.CurrentVersion,
}
license := ts.srv.License()
if license != nil && *license.Features.EnterprisePlugins {
filter.EnterprisePlugins = true
}
if model.BuildEnterpriseReady == "true" {
filter.BuildEnterpriseReady = true
}
return marketplaceClient.GetPlugins(filter)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package timezones
type Timezones struct {
supportedZones []string
}
func New() *Timezones {
timezones := Timezones{}
timezones.supportedZones = DefaultSupportedTimezones
return &timezones
}
func (t *Timezones) GetSupported() []string {
return t.supportedZones
}
func DefaultUserTimezone() map[string]string {
defaultTimezone := make(map[string]string)
defaultTimezone["useAutomaticTimezone"] = "true"
defaultTimezone["automaticTimezone"] = ""
defaultTimezone["manualTimezone"] = ""
return defaultTimezone
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package tracing
import (
"context"
"io"
"time"
opentracing "github.com/opentracing/opentracing-go"
"github.com/uber/jaeger-client-go"
jaegercfg "github.com/uber/jaeger-client-go/config"
"github.com/uber/jaeger-client-go/zipkin"
"github.com/uber/jaeger-lib/metrics"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// Tracer is a wrapper around Jaeger OpenTracing client, used to properly de-initialize jaeger on exit
type Tracer struct {
closer io.Closer
}
type LogrusAdapter struct {
}
// Error - logrus adapter for span errors
func (LogrusAdapter) Error(msg string) {
mlog.Error(msg)
}
// Infof - logrus adapter for span info logging
func (LogrusAdapter) Infof(msg string, args ...any) {
// we ignore Info messages from opentracing
}
// New instantiates Jaeger opentracing client with default options
// To override the defaults use environment variables listed here: https://github.com/jaegertracing/jaeger-client-go/blob/master/config/config.go
func New() (*Tracer, error) {
cfg := jaegercfg.Configuration{
Sampler: &jaegercfg.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
Reporter: &jaegercfg.ReporterConfig{
LogSpans: true,
},
}
zipkinPropagator := zipkin.NewZipkinB3HTTPHeaderPropagator()
closer, err := cfg.InitGlobalTracer(
"mattermost",
jaegercfg.Logger(LogrusAdapter{}),
jaegercfg.Metrics(metrics.NullFactory),
jaegercfg.Tag("serverStartTime", time.Now().UTC().Format(time.RFC3339)),
jaegercfg.Injector(opentracing.HTTPHeaders, zipkinPropagator),
jaegercfg.Extractor(opentracing.HTTPHeaders, zipkinPropagator),
jaegercfg.ZipkinSharedRPCSpan(true),
)
if err != nil {
return nil, err
}
mlog.Info("Opentracing initialized")
return &Tracer{
closer: closer,
}, nil
}
func (t *Tracer) Close() error {
return t.closer.Close()
}
func StartRootSpanByContext(ctx context.Context, operationName string) (opentracing.Span, context.Context) {
return opentracing.StartSpanFromContext(ctx, operationName)
}
func StartSpanWithParentByContext(ctx context.Context, operationName string) (opentracing.Span, context.Context) {
parentSpan := opentracing.SpanFromContext(ctx)
if parentSpan == nil {
return StartRootSpanByContext(ctx, operationName)
}
return opentracing.StartSpanFromContext(ctx, operationName, opentracing.ChildOf(parentSpan.Context()))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package upgrader
import (
"fmt"
)
// InvalidArch indicates that the current operating system or cpu architecture doesn't support upgrades
type InvalidArch struct{}
func NewInvalidArch() *InvalidArch {
return &InvalidArch{}
}
func (e *InvalidArch) Error() string {
return "invalid operating system or processor architecture"
}
// InvalidSignature indicates that the downloaded file doesn't have a valid signature.
type InvalidSignature struct{}
func NewInvalidSignature() *InvalidSignature {
return &InvalidSignature{}
}
func (e *InvalidSignature) Error() string {
return "invalid file signature"
}
// InvalidPermissions indicates that the file permissions doesn't allow to upgrade
type InvalidPermissions struct {
ErrType string
Path string
FileUsername string
MattermostUsername string
}
func NewInvalidPermissions(errType string, path string, mattermostUsername string, fileUsername string) *InvalidPermissions {
return &InvalidPermissions{
ErrType: errType,
Path: path,
FileUsername: fileUsername,
MattermostUsername: mattermostUsername,
}
}
func (e *InvalidPermissions) Error() string {
return fmt.Sprintf("the user %s is unable to update the %s file", e.MattermostUsername, e.Path)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:build !linux
// +build !linux
package upgrader
func CanIUpgradeToE0() error {
return &InvalidArch{}
}
func UpgradeToE0() error {
return &InvalidArch{}
}
func UpgradeToE0Status() (int64, error) {
return 0, &InvalidArch{}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package filestore
import (
"context"
"io"
"time"
"github.com/pkg/errors"
)
const (
driverS3 = "amazons3"
driverLocal = "local"
)
type ReadCloseSeeker interface {
io.ReadCloser
io.Seeker
}
type FileBackend interface {
TestConnection() error
Reader(path string) (ReadCloseSeeker, error)
ReadFile(path string) ([]byte, error)
FileExists(path string) (bool, error)
FileSize(path string) (int64, error)
CopyFile(oldPath, newPath string) error
MoveFile(oldPath, newPath string) error
WriteFile(fr io.Reader, path string) (int64, error)
AppendFile(fr io.Reader, path string) (int64, error)
RemoveFile(path string) error
FileModTime(path string) (time.Time, error)
ListDirectory(path string) ([]string, error)
ListDirectoryRecursively(path string) ([]string, error)
RemoveDirectory(path string) error
}
type FileBackendSettings struct {
DriverName string
Directory string
AmazonS3AccessKeyId string
AmazonS3SecretAccessKey string
AmazonS3Bucket string
AmazonS3PathPrefix string
AmazonS3Region string
AmazonS3Endpoint string
AmazonS3SSL bool
AmazonS3SignV2 bool
AmazonS3SSE bool
AmazonS3Trace bool
SkipVerify bool
AmazonS3RequestTimeoutMilliseconds int64
}
func (settings *FileBackendSettings) CheckMandatoryS3Fields() error {
if settings.AmazonS3Bucket == "" {
return errors.New("missing s3 bucket settings")
}
// if S3 endpoint is not set call the set defaults to set that
if settings.AmazonS3Endpoint == "" {
settings.AmazonS3Endpoint = "s3.amazonaws.com"
}
return nil
}
func NewFileBackend(settings FileBackendSettings) (FileBackend, error) {
switch settings.DriverName {
case driverS3:
backend, err := NewS3FileBackend(settings)
if err != nil {
return nil, errors.Wrap(err, "unable to connect to the s3 backend")
}
return backend, nil
case driverLocal:
return &LocalFileBackend{
directory: settings.Directory,
}, nil
}
return nil, errors.New("no valid filestorage driver found")
}
// TryWriteFileContext checks if the file backend supports context writes and passes the context in that case.
// Should the file backend not support contexts, it just calls WriteFile instead. This can be used to disable
// the timeouts for long writes (like exports).
func TryWriteFileContext(fb FileBackend, ctx context.Context, fr io.Reader, path string) (int64, error) {
type ContextWriter interface {
WriteFileContext(context.Context, io.Reader, string) (int64, error)
}
if cw, ok := fb.(ContextWriter); ok {
return cw.WriteFileContext(ctx, fr, path)
}
return fb.WriteFile(fr, path)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package filestore
import (
"bytes"
"io"
"os"
"path/filepath"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
TestFilePath = "/testfile"
)
type LocalFileBackend struct {
directory string
}
// copyFile will copy a file from src path to dst path.
// Overwrites any existing files at dst.
// Permissions are copied from file at src to the new file at dst.
func copyFile(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return
}
defer in.Close()
if err = os.MkdirAll(filepath.Dir(dst), os.ModePerm); err != nil {
return
}
out, err := os.Create(dst)
if err != nil {
return
}
defer func() {
if e := out.Close(); e != nil {
err = e
}
}()
_, err = io.Copy(out, in)
if err != nil {
return
}
err = out.Sync()
if err != nil {
return
}
stat, err := os.Stat(src)
if err != nil {
return
}
err = os.Chmod(dst, stat.Mode())
if err != nil {
return
}
return
}
func (b *LocalFileBackend) TestConnection() error {
f := bytes.NewReader([]byte("testingwrite"))
if _, err := writeFileLocally(f, filepath.Join(b.directory, TestFilePath)); err != nil {
return errors.Wrap(err, "unable to write to the local filesystem storage")
}
os.Remove(filepath.Join(b.directory, TestFilePath))
mlog.Debug("Able to write files to local storage.")
return nil
}
func (b *LocalFileBackend) Reader(path string) (ReadCloseSeeker, error) {
f, err := os.Open(filepath.Join(b.directory, path))
if err != nil {
return nil, errors.Wrapf(err, "unable to open file %s", path)
}
return f, nil
}
func (b *LocalFileBackend) ReadFile(path string) ([]byte, error) {
f, err := os.ReadFile(filepath.Join(b.directory, path))
if err != nil {
return nil, errors.Wrapf(err, "unable to read file %s", path)
}
return f, nil
}
func (b *LocalFileBackend) FileExists(path string) (bool, error) {
_, err := os.Stat(filepath.Join(b.directory, path))
if os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, errors.Wrapf(err, "unable to know if file %s exists", path)
}
return true, nil
}
func (b *LocalFileBackend) FileSize(path string) (int64, error) {
info, err := os.Stat(filepath.Join(b.directory, path))
if err != nil {
return 0, errors.Wrapf(err, "unable to get file size for %s", path)
}
return info.Size(), nil
}
func (b *LocalFileBackend) FileModTime(path string) (time.Time, error) {
info, err := os.Stat(filepath.Join(b.directory, path))
if err != nil {
return time.Time{}, errors.Wrapf(err, "unable to get modification time for file %s", path)
}
return info.ModTime(), nil
}
func (b *LocalFileBackend) CopyFile(oldPath, newPath string) error {
if err := copyFile(filepath.Join(b.directory, oldPath), filepath.Join(b.directory, newPath)); err != nil {
return errors.Wrapf(err, "unable to copy file from %s to %s", oldPath, newPath)
}
return nil
}
func (b *LocalFileBackend) MoveFile(oldPath, newPath string) error {
if err := os.MkdirAll(filepath.Dir(filepath.Join(b.directory, newPath)), 0750); err != nil {
return errors.Wrapf(err, "unable to create the new destination directory %s", filepath.Dir(newPath))
}
if err := os.Rename(filepath.Join(b.directory, oldPath), filepath.Join(b.directory, newPath)); err != nil {
return errors.Wrapf(err, "unable to move the file to %s to the destination directory", newPath)
}
return nil
}
func (b *LocalFileBackend) WriteFile(fr io.Reader, path string) (int64, error) {
return writeFileLocally(fr, filepath.Join(b.directory, path))
}
func writeFileLocally(fr io.Reader, path string) (int64, error) {
if err := os.MkdirAll(filepath.Dir(path), 0750); err != nil {
directory, _ := filepath.Abs(filepath.Dir(path))
return 0, errors.Wrapf(err, "unable to create the directory %s for the file %s", directory, path)
}
fw, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return 0, errors.Wrapf(err, "unable to open the file %s to write the data", path)
}
defer fw.Close()
written, err := io.Copy(fw, fr)
if err != nil {
return written, errors.Wrapf(err, "unable write the data in the file %s", path)
}
return written, nil
}
func (b *LocalFileBackend) AppendFile(fr io.Reader, path string) (int64, error) {
fp := filepath.Join(b.directory, path)
if _, err := os.Stat(fp); err != nil {
return 0, errors.Wrapf(err, "unable to find the file %s to append the data", path)
}
fw, err := os.OpenFile(fp, os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
return 0, errors.Wrapf(err, "unable to open the file %s to append the data", path)
}
defer fw.Close()
written, err := io.Copy(fw, fr)
if err != nil {
return written, errors.Wrapf(err, "unable append the data in the file %s", path)
}
return written, nil
}
func (b *LocalFileBackend) RemoveFile(path string) error {
if err := os.Remove(filepath.Join(b.directory, path)); err != nil {
return errors.Wrapf(err, "unable to remove the file %s", path)
}
return nil
}
// basePath: path to get to the file but won't be added to the end result
// path: basePath+path current directory we are looking at
// maxDepth: parameter to prevent infinite recursion, once this is reached we won't look any further
func appendRecursively(basePath, path string, maxDepth int) ([]string, error) {
results := []string{}
dirEntries, err := os.ReadDir(filepath.Join(basePath, path))
if err != nil {
if os.IsNotExist(err) {
return results, nil
}
return results, errors.Wrapf(err, "unable to list the directory %s", path)
}
for _, dirEntry := range dirEntries {
entryName := dirEntry.Name()
entryPath := filepath.Join(path, entryName)
if entryName == "." || entryName == ".." || entryPath == path {
continue
}
if dirEntry.IsDir() {
if maxDepth <= 0 {
mlog.Warn("Max Depth reached", mlog.String("path", entryPath))
results = append(results, entryPath)
continue // we'll ignore it if max depth is reached.
}
nestedResults, err := appendRecursively(basePath, entryPath, maxDepth-1)
if err != nil {
return results, err
}
results = append(results, nestedResults...)
} else {
results = append(results, entryPath)
}
}
return results, nil
}
func (b *LocalFileBackend) ListDirectory(path string) ([]string, error) {
return appendRecursively(b.directory, path, 0)
}
func (b *LocalFileBackend) ListDirectoryRecursively(path string) ([]string, error) {
return appendRecursively(b.directory, path, 10)
}
func (b *LocalFileBackend) RemoveDirectory(path string) error {
if err := os.RemoveAll(filepath.Join(b.directory, path)); err != nil {
return errors.Wrapf(err, "unable to remove the directory %s", path)
}
return nil
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make filestore-mocks`.
package mocks
import (
io "io"
filestore "github.com/mattermost/mattermost-server/v6/server/platform/shared/filestore"
mock "github.com/stretchr/testify/mock"
time "time"
)
// FileBackend is an autogenerated mock type for the FileBackend type
type FileBackend struct {
mock.Mock
}
// AppendFile provides a mock function with given fields: fr, path
func (_m *FileBackend) AppendFile(fr io.Reader, path string) (int64, error) {
ret := _m.Called(fr, path)
var r0 int64
if rf, ok := ret.Get(0).(func(io.Reader, string) int64); ok {
r0 = rf(fr, path)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(io.Reader, string) error); ok {
r1 = rf(fr, path)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CopyFile provides a mock function with given fields: oldPath, newPath
func (_m *FileBackend) CopyFile(oldPath string, newPath string) error {
ret := _m.Called(oldPath, newPath)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(oldPath, newPath)
} else {
r0 = ret.Error(0)
}
return r0
}
// FileExists provides a mock function with given fields: path
func (_m *FileBackend) FileExists(path string) (bool, error) {
ret := _m.Called(path)
var r0 bool
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(path)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(path)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FileModTime provides a mock function with given fields: path
func (_m *FileBackend) FileModTime(path string) (time.Time, error) {
ret := _m.Called(path)
var r0 time.Time
if rf, ok := ret.Get(0).(func(string) time.Time); ok {
r0 = rf(path)
} else {
r0 = ret.Get(0).(time.Time)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(path)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FileSize provides a mock function with given fields: path
func (_m *FileBackend) FileSize(path string) (int64, error) {
ret := _m.Called(path)
var r0 int64
if rf, ok := ret.Get(0).(func(string) int64); ok {
r0 = rf(path)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(path)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ListDirectory provides a mock function with given fields: path
func (_m *FileBackend) ListDirectory(path string) ([]string, error) {
ret := _m.Called(path)
var r0 []string
if rf, ok := ret.Get(0).(func(string) []string); ok {
r0 = rf(path)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(path)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ListDirectoryRecursively provides a mock function with given fields: path
func (_m *FileBackend) ListDirectoryRecursively(path string) ([]string, error) {
ret := _m.Called(path)
var r0 []string
if rf, ok := ret.Get(0).(func(string) []string); ok {
r0 = rf(path)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(path)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MoveFile provides a mock function with given fields: oldPath, newPath
func (_m *FileBackend) MoveFile(oldPath string, newPath string) error {
ret := _m.Called(oldPath, newPath)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(oldPath, newPath)
} else {
r0 = ret.Error(0)
}
return r0
}
// ReadFile provides a mock function with given fields: path
func (_m *FileBackend) ReadFile(path string) ([]byte, error) {
ret := _m.Called(path)
var r0 []byte
if rf, ok := ret.Get(0).(func(string) []byte); ok {
r0 = rf(path)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(path)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Reader provides a mock function with given fields: path
func (_m *FileBackend) Reader(path string) (filestore.ReadCloseSeeker, error) {
ret := _m.Called(path)
var r0 filestore.ReadCloseSeeker
if rf, ok := ret.Get(0).(func(string) filestore.ReadCloseSeeker); ok {
r0 = rf(path)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(filestore.ReadCloseSeeker)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(path)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RemoveDirectory provides a mock function with given fields: path
func (_m *FileBackend) RemoveDirectory(path string) error {
ret := _m.Called(path)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(path)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveFile provides a mock function with given fields: path
func (_m *FileBackend) RemoveFile(path string) error {
ret := _m.Called(path)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(path)
} else {
r0 = ret.Error(0)
}
return r0
}
// TestConnection provides a mock function with given fields:
func (_m *FileBackend) TestConnection() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// WriteFile provides a mock function with given fields: fr, path
func (_m *FileBackend) WriteFile(fr io.Reader, path string) (int64, error) {
ret := _m.Called(fr, path)
var r0 int64
if rf, ok := ret.Get(0).(func(io.Reader, string) int64); ok {
r0 = rf(fr, path)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(io.Reader, string) error); ok {
r1 = rf(fr, path)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make filestore-mocks`.
package mocks
import mock "github.com/stretchr/testify/mock"
// ReadCloseSeeker is an autogenerated mock type for the ReadCloseSeeker type
type ReadCloseSeeker struct {
mock.Mock
}
// Close provides a mock function with given fields:
func (_m *ReadCloseSeeker) Close() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Read provides a mock function with given fields: p
func (_m *ReadCloseSeeker) Read(p []byte) (int, error) {
ret := _m.Called(p)
var r0 int
if rf, ok := ret.Get(0).(func([]byte) int); ok {
r0 = rf(p)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func([]byte) error); ok {
r1 = rf(p)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Seek provides a mock function with given fields: offset, whence
func (_m *ReadCloseSeeker) Seek(offset int64, whence int) (int64, error) {
ret := _m.Called(offset, whence)
var r0 int64
if rf, ok := ret.Get(0).(func(int64, int) int64); ok {
r0 = rf(offset, whence)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(int64, int) error); ok {
r1 = rf(offset, whence)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package filestore
import (
"context"
"net/http"
"github.com/minio/minio-go/v7/pkg/credentials"
)
// customTransport is used to point the request to a different server.
// This is helpful in situations where a different service is handling AWS S3 requests
// from multiple Mattermost applications, and the Mattermost service itself does not
// have any S3 credentials.
type customTransport struct {
host string
scheme string
client http.Client
}
// RoundTrip implements the http.Roundtripper interface.
func (t *customTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Roundtrippers should not modify the original request.
newReq := req.Clone(context.Background())
*newReq.URL = *req.URL
req.URL.Scheme = t.scheme
req.URL.Host = t.host
return t.client.Do(req)
}
// customProvider is a dummy credentials provider for the minio client to work
// without actually providing credentials. This is needed with a custom transport
// in cases where the minio client does not actually have credentials with itself,
// rather needs responses from another entity.
//
// It satisfies the credentials.Provider interface.
type customProvider struct {
isSignV2 bool
}
// Retrieve just returns empty credentials.
func (cp customProvider) Retrieve() (credentials.Value, error) {
sign := credentials.SignatureV4
if cp.isSignV2 {
sign = credentials.SignatureV2
}
return credentials.Value{
SignerType: sign,
}, nil
}
// IsExpired always returns false.
func (cp customProvider) IsExpired() bool { return false }
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package filestore
import (
"bytes"
"context"
"crypto/tls"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
s3 "github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/minio/minio-go/v7/pkg/encrypt"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// S3FileBackend contains all necessary information to communicate with
// an AWS S3 compatible API backend.
type S3FileBackend struct {
endpoint string
accessKey string
secretKey string
secure bool
signV2 bool
region string
bucket string
pathPrefix string
encrypt bool
trace bool
client *s3.Client
skipVerify bool
timeout time.Duration
}
type S3FileBackendAuthError struct {
DetailedError string
}
// S3FileBackendNoBucketError is returned when testing a connection and no S3 bucket is found
type S3FileBackendNoBucketError struct{}
const (
// This is not exported by minio. See: https://github.com/minio/minio-go/issues/1339
bucketNotFound = "NoSuchBucket"
)
var (
imageExtensions = map[string]bool{".jpg": true, ".jpeg": true, ".gif": true, ".bmp": true, ".png": true, ".tiff": true, "tif": true}
imageMimeTypes = map[string]string{".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".bmp": "image/bmp", ".png": "image/png", ".tiff": "image/tiff", ".tif": "image/tif"}
)
var (
// Ensure that the ReaderAt interface is implemented.
_ io.ReaderAt = (*s3WithCancel)(nil)
)
func isFileExtImage(ext string) bool {
ext = strings.ToLower(ext)
return imageExtensions[ext]
}
func getImageMimeType(ext string) string {
ext = strings.ToLower(ext)
if imageMimeTypes[ext] == "" {
return "image"
}
return imageMimeTypes[ext]
}
func (s *S3FileBackendAuthError) Error() string {
return s.DetailedError
}
func (s *S3FileBackendNoBucketError) Error() string {
return "no such bucket"
}
// NewS3FileBackend returns an instance of an S3FileBackend.
func NewS3FileBackend(settings FileBackendSettings) (*S3FileBackend, error) {
timeout := time.Duration(settings.AmazonS3RequestTimeoutMilliseconds) * time.Millisecond
backend := &S3FileBackend{
endpoint: settings.AmazonS3Endpoint,
accessKey: settings.AmazonS3AccessKeyId,
secretKey: settings.AmazonS3SecretAccessKey,
secure: settings.AmazonS3SSL,
signV2: settings.AmazonS3SignV2,
region: settings.AmazonS3Region,
bucket: settings.AmazonS3Bucket,
pathPrefix: settings.AmazonS3PathPrefix,
encrypt: settings.AmazonS3SSE,
trace: settings.AmazonS3Trace,
skipVerify: settings.SkipVerify,
timeout: timeout,
}
cli, err := backend.s3New()
if err != nil {
return nil, err
}
backend.client = cli
return backend, nil
}
// Similar to s3.New() but allows initialization of signature v2 or signature v4 client.
// If signV2 input is false, function always returns signature v4.
//
// Additionally this function also takes a user defined region, if set
// disables automatic region lookup.
func (b *S3FileBackend) s3New() (*s3.Client, error) {
var creds *credentials.Credentials
isCloud := os.Getenv("MM_CLOUD_FILESTORE_BIFROST") != ""
if isCloud {
creds = credentials.New(customProvider{isSignV2: b.signV2})
} else if b.accessKey == "" && b.secretKey == "" {
creds = credentials.NewIAM("")
} else if b.signV2 {
creds = credentials.NewStatic(b.accessKey, b.secretKey, "", credentials.SignatureV2)
} else {
creds = credentials.NewStatic(b.accessKey, b.secretKey, "", credentials.SignatureV4)
}
opts := s3.Options{
Creds: creds,
Secure: b.secure,
Region: b.region,
}
tr, err := s3.DefaultTransport(b.secure)
if err != nil {
return nil, err
}
if b.skipVerify {
tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
opts.Transport = tr
// If this is a cloud installation, we override the default transport.
if isCloud {
scheme := "http"
if b.secure {
scheme = "https"
}
newTransport := http.DefaultTransport.(*http.Transport).Clone()
newTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: b.skipVerify}
opts.Transport = &customTransport{
host: b.endpoint,
scheme: scheme,
client: http.Client{Transport: newTransport},
}
}
s3Clnt, err := s3.New(b.endpoint, &opts)
if err != nil {
return nil, err
}
if b.trace {
s3Clnt.TraceOn(os.Stdout)
}
return s3Clnt, nil
}
func (b *S3FileBackend) TestConnection() error {
exists := true
var err error
// If a path prefix is present, we attempt to test the bucket by listing objects under the path
// and just checking the first response. This is because the BucketExists call is only at a bucket level
// and sometimes the user might only be allowed access to the specified path prefix.
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
if b.pathPrefix != "" {
obj := <-b.client.ListObjects(ctx, b.bucket, s3.ListObjectsOptions{Prefix: b.pathPrefix})
if obj.Err != nil {
typedErr := s3.ToErrorResponse(obj.Err)
if typedErr.Code != bucketNotFound {
return &S3FileBackendAuthError{DetailedError: "unable to list objects in the S3 bucket"}
}
exists = false
}
} else {
exists, err = b.client.BucketExists(ctx, b.bucket)
if err != nil {
return &S3FileBackendAuthError{DetailedError: "unable to check if the S3 bucket exists"}
}
}
if !exists {
return &S3FileBackendNoBucketError{}
}
mlog.Debug("Connection to S3 or minio is good. Bucket exists.")
return nil
}
func (b *S3FileBackend) MakeBucket() error {
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
err := b.client.MakeBucket(ctx, b.bucket, s3.MakeBucketOptions{Region: b.region})
if err != nil {
return errors.Wrap(err, "unable to create the s3 bucket")
}
return nil
}
// s3WithCancel is a wrapper struct which cancels the context
// when the object is closed.
type s3WithCancel struct {
*s3.Object
timer *time.Timer
cancel context.CancelFunc
}
func (sc *s3WithCancel) Close() error {
sc.timer.Stop()
sc.cancel()
return sc.Object.Close()
}
// CancelTimeout attempts to cancel the timeout for this reader. It allows calling
// code to ignore the timeout in case of longer running operations. The methods returns
// false if the timeout has already fired.
func (sc *s3WithCancel) CancelTimeout() bool {
return sc.timer.Stop()
}
// Caller must close the first return value
func (b *S3FileBackend) Reader(path string) (ReadCloseSeeker, error) {
path = filepath.Join(b.pathPrefix, path)
ctx, cancel := context.WithCancel(context.Background())
minioObject, err := b.client.GetObject(ctx, b.bucket, path, s3.GetObjectOptions{})
if err != nil {
cancel()
return nil, errors.Wrapf(err, "unable to open file %s", path)
}
sc := &s3WithCancel{
Object: minioObject,
timer: time.AfterFunc(b.timeout, cancel),
cancel: cancel,
}
return sc, nil
}
func (b *S3FileBackend) ReadFile(path string) ([]byte, error) {
path = filepath.Join(b.pathPrefix, path)
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
minioObject, err := b.client.GetObject(ctx, b.bucket, path, s3.GetObjectOptions{})
if err != nil {
return nil, errors.Wrapf(err, "unable to open file %s", path)
}
defer minioObject.Close()
f, err := io.ReadAll(minioObject)
if err != nil {
return nil, errors.Wrapf(err, "unable to read file %s", path)
}
return f, nil
}
func (b *S3FileBackend) FileExists(path string) (bool, error) {
path = filepath.Join(b.pathPrefix, path)
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
_, err := b.client.StatObject(ctx, b.bucket, path, s3.StatObjectOptions{})
if err == nil {
return true, nil
}
var s3Err s3.ErrorResponse
if errors.As(err, &s3Err); s3Err.Code == "NoSuchKey" {
return false, nil
}
return false, errors.Wrapf(err, "unable to know if file %s exists", path)
}
func (b *S3FileBackend) FileSize(path string) (int64, error) {
path = filepath.Join(b.pathPrefix, path)
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
info, err := b.client.StatObject(ctx, b.bucket, path, s3.StatObjectOptions{})
if err != nil {
return 0, errors.Wrapf(err, "unable to get file size for %s", path)
}
return info.Size, nil
}
func (b *S3FileBackend) FileModTime(path string) (time.Time, error) {
path = filepath.Join(b.pathPrefix, path)
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
info, err := b.client.StatObject(ctx, b.bucket, path, s3.StatObjectOptions{})
if err != nil {
return time.Time{}, errors.Wrapf(err, "unable to get modification time for file %s", path)
}
return info.LastModified, nil
}
func (b *S3FileBackend) CopyFile(oldPath, newPath string) error {
oldPath = filepath.Join(b.pathPrefix, oldPath)
newPath = filepath.Join(b.pathPrefix, newPath)
srcOpts := s3.CopySrcOptions{
Bucket: b.bucket,
Object: oldPath,
}
if b.encrypt {
srcOpts.Encryption = encrypt.NewSSE()
}
dstOpts := s3.CopyDestOptions{
Bucket: b.bucket,
Object: newPath,
}
if b.encrypt {
dstOpts.Encryption = encrypt.NewSSE()
}
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
if _, err := b.client.CopyObject(ctx, dstOpts, srcOpts); err != nil {
return errors.Wrapf(err, "unable to copy file from %s to %s", oldPath, newPath)
}
return nil
}
func (b *S3FileBackend) MoveFile(oldPath, newPath string) error {
oldPath = filepath.Join(b.pathPrefix, oldPath)
newPath = filepath.Join(b.pathPrefix, newPath)
srcOpts := s3.CopySrcOptions{
Bucket: b.bucket,
Object: oldPath,
}
if b.encrypt {
srcOpts.Encryption = encrypt.NewSSE()
}
dstOpts := s3.CopyDestOptions{
Bucket: b.bucket,
Object: newPath,
}
if b.encrypt {
dstOpts.Encryption = encrypt.NewSSE()
}
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
if _, err := b.client.CopyObject(ctx, dstOpts, srcOpts); err != nil {
return errors.Wrapf(err, "unable to copy the file to %s to the new destination", newPath)
}
ctx2, cancel2 := context.WithTimeout(context.Background(), b.timeout)
defer cancel2()
if err := b.client.RemoveObject(ctx2, b.bucket, oldPath, s3.RemoveObjectOptions{}); err != nil {
return errors.Wrapf(err, "unable to remove the file old file %s", oldPath)
}
return nil
}
func (b *S3FileBackend) WriteFile(fr io.Reader, path string) (int64, error) {
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
return b.WriteFileContext(ctx, fr, path)
}
func (b *S3FileBackend) WriteFileContext(ctx context.Context, fr io.Reader, path string) (int64, error) {
var contentType string
path = filepath.Join(b.pathPrefix, path)
if ext := filepath.Ext(path); isFileExtImage(ext) {
contentType = getImageMimeType(ext)
} else {
contentType = "binary/octet-stream"
}
options := s3PutOptions(b.encrypt, contentType)
objSize := int64(-1)
isCloud := os.Getenv("MM_CLOUD_FILESTORE_BIFROST") != ""
if isCloud {
options.DisableContentSha256 = true
} else {
// We pass an object size only in situations where bifrost is not
// used. Bifrost needs to run in HTTPS, which is not yet deployed.
switch t := fr.(type) {
case *bytes.Buffer:
objSize = int64(t.Len())
case *os.File:
if s, err := t.Stat(); err == nil {
objSize = s.Size()
}
}
}
info, err := b.client.PutObject(ctx, b.bucket, path, fr, objSize, options)
if err != nil {
return info.Size, errors.Wrapf(err, "unable write the data in the file %s", path)
}
return info.Size, nil
}
func (b *S3FileBackend) AppendFile(fr io.Reader, path string) (int64, error) {
fp := filepath.Join(b.pathPrefix, path)
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
if _, err := b.client.StatObject(ctx, b.bucket, fp, s3.StatObjectOptions{}); err != nil {
return 0, errors.Wrapf(err, "unable to find the file %s to append the data", path)
}
var contentType string
if ext := filepath.Ext(fp); isFileExtImage(ext) {
contentType = getImageMimeType(ext)
} else {
contentType = "binary/octet-stream"
}
options := s3PutOptions(b.encrypt, contentType)
sse := options.ServerSideEncryption
partName := fp + ".part"
ctx2, cancel2 := context.WithTimeout(context.Background(), b.timeout)
defer cancel2()
objSize := -1
isCloud := os.Getenv("MM_CLOUD_FILESTORE_BIFROST") != ""
if isCloud {
options.DisableContentSha256 = true
}
// We pass an object size only in situations where bifrost is not
// used. Bifrost needs to run in HTTPS, which is not yet deployed.
if buf, ok := fr.(*bytes.Buffer); ok && !isCloud {
objSize = buf.Len()
}
info, err := b.client.PutObject(ctx2, b.bucket, partName, fr, int64(objSize), options)
if err != nil {
return 0, errors.Wrapf(err, "unable append the data in the file %s", path)
}
defer func() {
ctx4, cancel4 := context.WithTimeout(context.Background(), b.timeout)
defer cancel4()
b.client.RemoveObject(ctx4, b.bucket, partName, s3.RemoveObjectOptions{})
}()
src1Opts := s3.CopySrcOptions{
Bucket: b.bucket,
Object: fp,
}
src2Opts := s3.CopySrcOptions{
Bucket: b.bucket,
Object: partName,
}
dstOpts := s3.CopyDestOptions{
Bucket: b.bucket,
Object: fp,
Encryption: sse,
}
ctx3, cancel3 := context.WithTimeout(context.Background(), b.timeout)
defer cancel3()
_, err = b.client.ComposeObject(ctx3, dstOpts, src1Opts, src2Opts)
if err != nil {
return 0, errors.Wrapf(err, "unable append the data in the file %s", path)
}
return info.Size, nil
}
func (b *S3FileBackend) RemoveFile(path string) error {
path = filepath.Join(b.pathPrefix, path)
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
if err := b.client.RemoveObject(ctx, b.bucket, path, s3.RemoveObjectOptions{}); err != nil {
return errors.Wrapf(err, "unable to remove the file %s", path)
}
return nil
}
func getPathsFromObjectInfos(in <-chan s3.ObjectInfo) <-chan s3.ObjectInfo {
out := make(chan s3.ObjectInfo, 1)
go func() {
defer close(out)
for {
info, done := <-in
if !done {
break
}
out <- info
}
}()
return out
}
func (b *S3FileBackend) listDirectory(path string, recursion bool) ([]string, error) {
path = filepath.Join(b.pathPrefix, path)
if !strings.HasSuffix(path, "/") && path != "" {
// s3Clnt returns only the path itself when "/" is not present
// appending "/" to make it consistent across all filestores
path = path + "/"
}
opts := s3.ListObjectsOptions{
Prefix: path,
Recursive: recursion,
}
var paths []string
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
for object := range b.client.ListObjects(ctx, b.bucket, opts) {
if object.Err != nil {
return nil, errors.Wrapf(object.Err, "unable to list the directory %s", path)
}
// We strip the path prefix that gets applied,
// so that it remains transparent to the application.
object.Key = strings.TrimPrefix(object.Key, b.pathPrefix)
trimmed := strings.Trim(object.Key, "/")
if trimmed != "" {
paths = append(paths, trimmed)
}
}
return paths, nil
}
func (b *S3FileBackend) ListDirectory(path string) ([]string, error) {
return b.listDirectory(path, false)
}
func (b *S3FileBackend) ListDirectoryRecursively(path string) ([]string, error) {
return b.listDirectory(path, true)
}
func (b *S3FileBackend) RemoveDirectory(path string) error {
opts := s3.ListObjectsOptions{
Prefix: filepath.Join(b.pathPrefix, path),
Recursive: true,
}
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
list := b.client.ListObjects(ctx, b.bucket, opts)
ctx2, cancel2 := context.WithTimeout(context.Background(), b.timeout)
defer cancel2()
objectsCh := b.client.RemoveObjects(ctx2, b.bucket, getPathsFromObjectInfos(list), s3.RemoveObjectsOptions{})
for err := range objectsCh {
if err.Err != nil {
return errors.Wrapf(err.Err, "unable to remove the directory %s", path)
}
}
return nil
}
func s3PutOptions(encrypted bool, contentType string) s3.PutObjectOptions {
options := s3.PutObjectOptions{}
if encrypted {
options.ServerSideEncryption = encrypt.NewSSE()
}
options.ContentType = contentType
// We set the part size to the minimum allowed value of 5MBs
// to avoid an excessive allocation in minio.PutObject implementation.
options.PartSize = 1024 * 1024 * 5
return options
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package i18n
import (
"fmt"
"html/template"
"net/http"
"os"
"path/filepath"
"reflect"
"strings"
"github.com/mattermost/go-i18n/i18n"
"github.com/mattermost/go-i18n/i18n/bundle"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const defaultLocale = "en"
// TranslateFunc is the type of the translate functions
type TranslateFunc func(translationID string, args ...any) string
// TranslationFuncByLocal is the type of function that takes local as a string and returns the translation function
type TranslationFuncByLocal func(locale string) TranslateFunc
// T is the translate function using the default server language as fallback language
var T TranslateFunc
// TDefault is the translate function using english as fallback language
var TDefault TranslateFunc
var locales map[string]string = make(map[string]string)
var defaultServerLocale string
var defaultClientLocale string
// TranslationsPreInit loads translations from filesystem if they are not
// loaded already and assigns english while loading server config
func TranslationsPreInit(translationsDir string) error {
if T != nil {
return nil
}
// Set T even if we fail to load the translations. Lots of shutdown handling code will
// segfault trying to handle the error, and the untranslated IDs are strictly better.
T = tfuncWithFallback(defaultLocale)
TDefault = tfuncWithFallback(defaultLocale)
return initTranslationsWithDir(translationsDir)
}
// InitTranslations set the defaults configured in the server and initialize
// the T function using the server default as fallback language
func InitTranslations(serverLocale, clientLocale string) error {
defaultServerLocale = serverLocale
defaultClientLocale = clientLocale
var err error
T, err = getTranslationsBySystemLocale()
return err
}
func initTranslationsWithDir(dir string) error {
files, _ := os.ReadDir(dir)
for _, f := range files {
if filepath.Ext(f.Name()) == ".json" {
filename := f.Name()
locales[strings.Split(filename, ".")[0]] = filepath.Join(dir, filename)
if err := i18n.LoadTranslationFile(filepath.Join(dir, filename)); err != nil {
return err
}
}
}
return nil
}
// GetTranslationFuncForDir loads translations from the filesystem into a new instance of the bundle.
// It returns a function to access loaded translations.
func GetTranslationFuncForDir(dir string) (TranslationFuncByLocal, error) {
var availableLocals map[string]string = make(map[string]string)
bundle := bundle.New()
files, _ := os.ReadDir(dir)
for _, f := range files {
if filepath.Ext(f.Name()) != ".json" {
continue
}
filename := f.Name()
availableLocals[strings.Split(filename, ".")[0]] = filepath.Join(dir, filename)
if err := bundle.LoadTranslationFile(filepath.Join(dir, filename)); err != nil {
return nil, err
}
}
return func(locale string) TranslateFunc {
if _, ok := availableLocals[locale]; !ok {
locale = defaultLocale
}
t, _ := bundle.Tfunc(locale)
return func(translationID string, args ...any) string {
if translated := t(translationID, args...); translated != translationID {
return translated
}
t, _ := bundle.Tfunc(defaultLocale)
return t(translationID, args...)
}
}, nil
}
func getTranslationsBySystemLocale() (TranslateFunc, error) {
locale := defaultServerLocale
if _, ok := locales[locale]; !ok {
mlog.Warn("Failed to load system translations for", mlog.String("locale", locale), mlog.String("attempting to fall back to default locale", defaultLocale))
locale = defaultLocale
}
if locales[locale] == "" {
return nil, fmt.Errorf("failed to load system translations for '%v'", defaultLocale)
}
translations := tfuncWithFallback(locale)
if translations == nil {
return nil, fmt.Errorf("failed to load system translations")
}
mlog.Info("Loaded system translations", mlog.String("for locale", locale), mlog.String("from locale", locales[locale]))
return translations, nil
}
// GetUserTranslations get the translation function for an specific locale
func GetUserTranslations(locale string) TranslateFunc {
if _, ok := locales[locale]; !ok {
locale = defaultLocale
}
translations := tfuncWithFallback(locale)
return translations
}
// GetTranslationsAndLocaleFromRequest return the translation function and the
// locale based on a request headers
func GetTranslationsAndLocaleFromRequest(r *http.Request) (TranslateFunc, string) {
// This is for checking against locales like pt_BR or zn_CN
headerLocaleFull := strings.Split(r.Header.Get("Accept-Language"), ",")[0]
// This is for checking against locales like en, es
headerLocale := strings.Split(strings.Split(r.Header.Get("Accept-Language"), ",")[0], "-")[0]
defaultLocale := defaultClientLocale
if locales[headerLocaleFull] != "" {
translations := tfuncWithFallback(headerLocaleFull)
return translations, headerLocaleFull
} else if locales[headerLocale] != "" {
translations := tfuncWithFallback(headerLocale)
return translations, headerLocale
} else if locales[defaultLocale] != "" {
translations := tfuncWithFallback(defaultLocale)
return translations, headerLocale
}
translations := tfuncWithFallback(defaultLocale)
return translations, defaultLocale
}
// GetSupportedLocales return a map of locale code and the file path with the
// translations
func GetSupportedLocales() map[string]string {
return locales
}
func tfuncWithFallback(pref string) TranslateFunc {
t, _ := i18n.Tfunc(pref)
return func(translationID string, args ...any) string {
if translated := t(translationID, args...); translated != translationID {
return translated
}
t, _ := i18n.Tfunc(defaultLocale)
return t(translationID, args...)
}
}
// TranslateAsHTML translates the translationID provided and return a
// template.HTML object
func TranslateAsHTML(t TranslateFunc, translationID string, args map[string]any) template.HTML {
message := t(translationID, escapeForHTML(args))
message = strings.Replace(message, "[[", "<strong>", -1)
message = strings.Replace(message, "]]", "</strong>", -1)
return template.HTML(message)
}
func escapeForHTML(arg any) any {
switch typedArg := arg.(type) {
case string:
return template.HTMLEscapeString(typedArg)
case *string:
return template.HTMLEscapeString(*typedArg)
case map[string]any:
safeArg := make(map[string]any, len(typedArg))
for key, value := range typedArg {
safeArg[key] = escapeForHTML(value)
}
return safeArg
default:
mlog.Warn(
"Unable to escape value for HTML template",
mlog.Any("html_template", arg),
mlog.String("template_type", reflect.ValueOf(arg).Type().String()),
)
return ""
}
}
// IdentityTfunc returns a translation function that don't translate, only
// returns the same id
func IdentityTfunc() TranslateFunc {
return func(translationID string, args ...any) string {
return translationID
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mail
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
const (
InbucketAPI = "/api/v1/mailbox/"
)
// OutputJSONHeader holds the received Header to test sending emails (inbucket)
type JSONMessageHeaderInbucket []struct {
Mailbox string
ID string `json:"Id"`
From, Subject, Date string
To []string
Size int
}
// OutputJSONMessage holds the received Message fto test sending emails (inbucket)
type JSONMessageInbucket struct {
Mailbox string
ID string `json:"Id"`
From, Subject, Date string
Size int
Header map[string][]string
Body struct {
Text string
HTML string `json:"Html"`
}
Attachments []struct {
Filename string
ContentType string `json:"content-type"`
DownloadLink string `json:"download-link"`
Bytes []byte `json:"-"`
}
}
func ParseEmail(email string) string {
pos := strings.Index(email, "@")
parsedEmail := email[0:pos]
return parsedEmail
}
func GetMailBox(email string) (results JSONMessageHeaderInbucket, err error) {
parsedEmail := ParseEmail(email)
url := fmt.Sprintf("%s%s%s", getInbucketHost(), InbucketAPI, parsedEmail)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer func() {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}()
if resp.Body == nil {
return nil, fmt.Errorf("no mailbox")
}
var record JSONMessageHeaderInbucket
err = json.NewDecoder(resp.Body).Decode(&record)
if err != nil {
return nil, fmt.Errorf("error: %w", err)
}
if len(record) == 0 {
return nil, fmt.Errorf("no mailbox")
}
return record, nil
}
func GetMessageFromMailbox(email, id string) (JSONMessageInbucket, error) {
parsedEmail := ParseEmail(email)
var record JSONMessageInbucket
url := fmt.Sprintf("%s%s%s/%s", getInbucketHost(), InbucketAPI, parsedEmail, id)
emailResponse, err := http.Get(url)
if err != nil {
return record, err
}
defer func() {
io.Copy(io.Discard, emailResponse.Body)
emailResponse.Body.Close()
}()
if err = json.NewDecoder(emailResponse.Body).Decode(&record); err != nil {
return record, err
}
// download attachments
if record.Attachments != nil && len(record.Attachments) > 0 {
for i := range record.Attachments {
var bytes []byte
bytes, err = downloadAttachment(record.Attachments[i].DownloadLink)
if err != nil {
return record, err
}
record.Attachments[i].Bytes = make([]byte, len(bytes))
copy(record.Attachments[i].Bytes, bytes)
}
}
return record, err
}
func downloadAttachment(url string) ([]byte, error) {
attachmentResponse, err := http.Get(url)
if err != nil {
return nil, err
}
defer attachmentResponse.Body.Close()
buf := new(bytes.Buffer)
io.Copy(buf, attachmentResponse.Body)
return buf.Bytes(), nil
}
func DeleteMailBox(email string) (err error) {
parsedEmail := ParseEmail(email)
url := fmt.Sprintf("%s%s%s", getInbucketHost(), InbucketAPI, parsedEmail)
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return err
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
func RetryInbucket(attempts int, callback func() error) (err error) {
for i := 0; ; i++ {
err = callback()
if err == nil {
return nil
}
if i >= (attempts - 1) {
break
}
time.Sleep(5 * time.Second)
fmt.Println("retrying...")
}
return fmt.Errorf("after %d attempts, last error: %s", attempts, err)
}
func getInbucketHost() (host string) {
inbucket_host := os.Getenv("CI_INBUCKET_HOST")
if inbucket_host == "" {
inbucket_host = "localhost"
}
inbucket_port := os.Getenv("CI_INBUCKET_PORT")
if inbucket_port == "" {
inbucket_port = "9001"
}
return fmt.Sprintf("http://%s:%s", inbucket_host, inbucket_port)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mail
import (
"context"
"crypto/tls"
"fmt"
"io"
"mime"
"net"
"net/mail"
"net/smtp"
"time"
"github.com/jaytaylor/html2text"
"github.com/pkg/errors"
gomail "gopkg.in/mail.v2"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
TLS = "TLS"
StartTLS = "STARTTLS"
)
type SMTPConfig struct {
ConnectionSecurity string
SkipServerCertificateVerification bool
Hostname string
ServerName string
Server string
Port string
ServerTimeout int
Username string
Password string
EnableSMTPAuth bool
SendEmailNotifications bool
FeedbackName string
FeedbackEmail string
ReplyToAddress string
}
type mailData struct {
mimeTo string
smtpTo string
from mail.Address
cc string
replyTo mail.Address
subject string
htmlBody string
embeddedFiles map[string]io.Reader
mimeHeaders map[string]string
messageID string
inReplyTo string
references string
category string
}
// smtpClient is implemented by an smtp.Client. See https://golang.org/pkg/net/smtp/#Client.
type smtpClient interface {
Mail(string) error
Rcpt(string) error
Data() (io.WriteCloser, error)
}
func encodeRFC2047Word(s string) string {
return mime.BEncoding.Encode("utf-8", s)
}
type authChooser struct {
smtp.Auth
config *SMTPConfig
}
func (a *authChooser) Start(server *smtp.ServerInfo) (string, []byte, error) {
smtpAddress := a.config.ServerName + ":" + a.config.Port
a.Auth = LoginAuth(a.config.Username, a.config.Password, smtpAddress)
for _, method := range server.Auth {
if method == "PLAIN" {
a.Auth = smtp.PlainAuth("", a.config.Username, a.config.Password, a.config.ServerName+":"+a.config.Port)
break
}
}
return a.Auth.Start(server)
}
type loginAuth struct {
username, password, host string
}
func LoginAuth(username, password, host string) smtp.Auth {
return &loginAuth{username, password, host}
}
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
if !server.TLS {
return "", nil, errors.New("unencrypted connection")
}
if server.Name != a.host {
return "", nil, errors.New("wrong host name")
}
return "LOGIN", []byte{}, nil
}
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if more {
switch string(fromServer) {
case "Username:":
return []byte(a.username), nil
case "Password:":
return []byte(a.password), nil
default:
return nil, errors.New("Unknown fromServer")
}
}
return nil, nil
}
func ConnectToSMTPServerAdvanced(config *SMTPConfig) (net.Conn, error) {
var conn net.Conn
var err error
smtpAddress := config.Server + ":" + config.Port
dialer := &net.Dialer{
Timeout: time.Duration(config.ServerTimeout) * time.Second,
}
if config.ConnectionSecurity == TLS {
tlsconfig := &tls.Config{
InsecureSkipVerify: config.SkipServerCertificateVerification,
ServerName: config.ServerName,
}
conn, err = tls.DialWithDialer(dialer, "tcp", smtpAddress, tlsconfig)
if err != nil {
return nil, errors.Wrap(err, "unable to connect to the SMTP server through TLS")
}
} else {
conn, err = dialer.Dial("tcp", smtpAddress)
if err != nil {
return nil, errors.Wrap(err, "unable to connect to the SMTP server")
}
}
return conn, nil
}
func ConnectToSMTPServer(config *SMTPConfig) (net.Conn, error) {
return ConnectToSMTPServerAdvanced(config)
}
func NewSMTPClientAdvanced(ctx context.Context, conn net.Conn, config *SMTPConfig) (*smtp.Client, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var c *smtp.Client
ec := make(chan error)
go func() {
var err error
c, err = smtp.NewClient(conn, config.ServerName+":"+config.Port)
if err != nil {
ec <- err
return
}
cancel()
}()
select {
case <-ctx.Done():
err := ctx.Err()
if err != nil && err.Error() != "context canceled" {
return nil, errors.Wrap(err, "unable to connect to the SMTP server")
}
case err := <-ec:
return nil, errors.Wrap(err, "unable to connect to the SMTP server")
}
if config.Hostname != "" {
err := c.Hello(config.Hostname)
if err != nil {
return nil, errors.Wrap(err, "unable to send hello message")
}
}
if config.ConnectionSecurity == StartTLS {
tlsconfig := &tls.Config{
InsecureSkipVerify: config.SkipServerCertificateVerification,
ServerName: config.ServerName,
}
c.StartTLS(tlsconfig)
}
if config.EnableSMTPAuth {
if err := c.Auth(&authChooser{config: config}); err != nil {
return nil, errors.Wrap(err, "authentication failed")
}
}
return c, nil
}
func NewSMTPClient(ctx context.Context, conn net.Conn, config *SMTPConfig) (*smtp.Client, error) {
return NewSMTPClientAdvanced(
ctx,
conn,
config,
)
}
func TestConnection(config *SMTPConfig) error {
conn, err := ConnectToSMTPServer(config)
if err != nil {
return errors.Wrap(err, "unable to connect")
}
defer conn.Close()
sec := config.ServerTimeout
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, time.Duration(sec)*time.Second)
defer cancel()
c, err := NewSMTPClient(ctx, conn, config)
if err != nil {
return errors.Wrap(err, "unable to connect")
}
c.Close()
c.Quit()
return nil
}
func SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody string, embeddedFiles map[string]io.Reader, config *SMTPConfig, enableComplianceFeatures bool, messageID string, inReplyTo string, references string, ccMail string, category string) error {
fromMail := mail.Address{Name: config.FeedbackName, Address: config.FeedbackEmail}
replyTo := mail.Address{Name: config.FeedbackName, Address: config.ReplyToAddress}
mail := mailData{
mimeTo: to,
smtpTo: to,
from: fromMail,
cc: ccMail,
replyTo: replyTo,
subject: subject,
htmlBody: htmlBody,
embeddedFiles: embeddedFiles,
messageID: messageID,
inReplyTo: inReplyTo,
references: references,
category: category,
}
return sendMailUsingConfigAdvanced(mail, config)
}
func SendMailUsingConfig(to, subject, htmlBody string, config *SMTPConfig, enableComplianceFeatures bool, messageID string, inReplyTo string, references string, ccMail, category string) error {
return SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody, nil, config, enableComplianceFeatures, messageID, inReplyTo, references, ccMail, category)
}
// allows for sending an email with differing MIME/SMTP recipients
func sendMailUsingConfigAdvanced(mail mailData, config *SMTPConfig) error {
if config.Server == "" {
return nil
}
conn, err := ConnectToSMTPServer(config)
if err != nil {
return err
}
defer conn.Close()
sec := config.ServerTimeout
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, time.Duration(sec)*time.Second)
defer cancel()
c, err := NewSMTPClient(ctx, conn, config)
if err != nil {
return err
}
defer c.Quit()
defer c.Close()
return sendMail(c, mail, time.Now(), config)
}
const SendGridXSMTPAPIHeader = "X-SMTPAPI"
func sendMail(c smtpClient, mail mailData, date time.Time, config *SMTPConfig) error {
mlog.Debug("sending mail", mlog.String("to", mail.smtpTo), mlog.String("subject", mail.subject))
htmlMessage := mail.htmlBody
txtBody, err := html2text.FromString(mail.htmlBody)
if err != nil {
mlog.Warn("Unable to convert html body to text", mlog.Err(err))
txtBody = ""
}
headers := map[string][]string{
"From": {mail.from.String()},
"To": {mail.mimeTo},
"Subject": {encodeRFC2047Word(mail.subject)},
"Content-Transfer-Encoding": {"8bit"},
"Auto-Submitted": {"auto-generated"},
"Precedence": {"bulk"},
}
if mail.category != "" {
sendgridHeader := fmt.Sprintf(`{"category": %q}`, mail.category)
headers[SendGridXSMTPAPIHeader] = []string{sendgridHeader}
}
if mail.replyTo.Address != "" {
headers["Reply-To"] = []string{mail.replyTo.String()}
}
if mail.cc != "" {
headers["CC"] = []string{mail.cc}
}
if mail.messageID != "" {
headers["Message-ID"] = []string{mail.messageID}
} else {
randomStringLength := 16
msgID := fmt.Sprintf("<%s-%d@%s>", model.NewRandomString(randomStringLength), time.Now().Unix(), config.Hostname)
headers["Message-ID"] = []string{msgID}
}
if mail.inReplyTo != "" {
headers["In-Reply-To"] = []string{mail.inReplyTo}
}
if mail.references != "" {
headers["References"] = []string{mail.references}
}
for k, v := range mail.mimeHeaders {
headers[k] = []string{encodeRFC2047Word(v)}
}
m := gomail.NewMessage(gomail.SetCharset("UTF-8"))
m.SetHeaders(headers)
m.SetDateHeader("Date", date)
m.SetBody("text/plain", txtBody)
m.AddAlternative("text/html", htmlMessage)
for name, reader := range mail.embeddedFiles {
m.EmbedReader(name, reader)
}
if err = c.Mail(mail.from.Address); err != nil {
return errors.Wrap(err, "failed to set the from address")
}
if err = c.Rcpt(mail.smtpTo); err != nil {
return errors.Wrap(err, "failed to set the to address")
}
w, err := c.Data()
if err != nil {
return errors.Wrap(err, "failed to add email message data")
}
_, err = m.WriteTo(w)
if err != nil {
return errors.Wrap(err, "failed to write the email message")
}
err = w.Close()
if err != nil {
return errors.Wrap(err, "failed to close connection to the SMTP server")
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
import (
"regexp"
"strings"
"unicode"
"unicode/utf8"
)
// Based off of extensions/autolink.c from https://github.com/github/cmark
var (
DefaultURLSchemes = []string{"http", "https", "ftp", "mailto", "tel"}
wwwAutoLinkRegex = regexp.MustCompile(`^www\d{0,3}\.`)
)
// Given a string with a w at the given position, tries to parse and return a range containing a www link.
// if one exists. If the text at the given position isn't a link, returns an empty string. Equivalent to
// www_match from the reference code.
func parseWWWAutolink(data string, position int) (Range, bool) {
// Check that this isn't part of another word
if position > 1 {
prevChar := data[position-1]
if !isWhitespaceByte(prevChar) && !isAllowedBeforeWWWLink(prevChar) {
return Range{}, false
}
}
// Check that this starts with www
if len(data)-position < 4 || !wwwAutoLinkRegex.MatchString(data[position:]) {
return Range{}, false
}
end := checkDomain(data[position:], false)
if end == 0 {
return Range{}, false
}
end += position
// Grab all text until the end of the string or the next whitespace character
for end < len(data) && !isWhitespaceByte(data[end]) {
end += 1
}
// Trim trailing punctuation
end = trimTrailingCharactersFromLink(data, position, end)
if position == end {
return Range{}, false
}
return Range{position, end}, true
}
func isAllowedBeforeWWWLink(c byte) bool {
switch c {
case '*', '_', '~', ')':
return true
}
return false
}
// Given a string with a : at the given position, tried to parse and return a range containing a URL scheme
// if one exists. If the text around the given position isn't a link, returns an empty string. Equivalent to
// url_match from the reference code.
func parseURLAutolink(data string, position int) (Range, bool) {
// Check that a :// exists. This doesn't match the clients that treat the slashes as optional.
if len(data)-position < 4 || data[position+1] != '/' || data[position+2] != '/' {
return Range{}, false
}
start := position - 1
for start > 0 && isAlphanumericByte(data[start-1]) {
start -= 1
}
if start < 0 || position >= len(data) {
return Range{}, false
}
// Ensure that the URL scheme is allowed and that at least one character after the scheme is valid.
scheme := data[start:position]
if !isSchemeAllowed(scheme) || !isValidHostCharacter(data[position+3:]) {
return Range{}, false
}
end := checkDomain(data[position+3:], true)
if end == 0 {
return Range{}, false
}
end += position
// Grab all text until the end of the string or the next whitespace character
for end < len(data) && !isWhitespaceByte(data[end]) {
end += 1
}
// Trim trailing punctuation
end = trimTrailingCharactersFromLink(data, start, end)
if start == end {
return Range{}, false
}
return Range{start, end}, true
}
func isSchemeAllowed(scheme string) bool {
// Note that this doesn't support the custom URL schemes implemented by the client
for _, allowed := range DefaultURLSchemes {
if strings.EqualFold(allowed, scheme) {
return true
}
}
return false
}
// Given a string starting with a URL, returns the number of valid characters that make up the URL's domain.
// Returns 0 if the string doesn't start with a domain name. allowShort determines whether or not the domain
// needs to contain a period to be considered valid. Equivalent to check_domain from the reference code.
func checkDomain(data string, allowShort bool) int {
foundUnderscore := false
foundPeriod := false
i := 1
for ; i < len(data)-1; i++ {
if data[i] == '_' {
foundUnderscore = true
break
} else if data[i] == '.' {
foundPeriod = true
} else if !isValidHostCharacter(data[i:]) && data[i] != '-' {
break
}
}
if foundUnderscore {
return 0
}
if allowShort {
// If allowShort is set, accept any string of valid domain characters
return i
}
// If allowShort isn't set, a valid domain just requires at least a single period. Note that this
// logic isn't entirely necessary because we already know the string starts with "www." when
// this is called from parseWWWAutolink
if foundPeriod {
return i
}
return 0
}
// Returns true if the provided link starts with a valid character for a domain name. Equivalent to
// is_valid_hostchar from the reference code.
func isValidHostCharacter(link string) bool {
c, _ := utf8.DecodeRuneInString(link)
if c == utf8.RuneError {
return false
}
return !unicode.IsSpace(c) && !unicode.IsPunct(c)
}
// Removes any trailing characters such as punctuation or stray brackets that shouldn't be part of the link.
// Returns a new end position for the link. Equivalent to autolink_delim from the reference code.
func trimTrailingCharactersFromLink(markdown string, start int, end int) int {
runes := []rune(markdown[start:end])
linkEnd := len(runes)
// Cut off the link before an open angle bracket if it contains one
for i, c := range runes {
if c == '<' {
linkEnd = i
break
}
}
for linkEnd > 0 {
c := runes[linkEnd-1]
if !canEndAutolink(c) {
// Trim trailing quotes, periods, etc
linkEnd = linkEnd - 1
} else if c == ';' {
// Trim a trailing HTML entity
newEnd := linkEnd - 2
for newEnd > 0 && ((runes[newEnd] >= 'a' && runes[newEnd] <= 'z') || (runes[newEnd] >= 'A' && runes[newEnd] <= 'Z')) {
newEnd -= 1
}
if newEnd < linkEnd-2 && runes[newEnd] == '&' {
linkEnd = newEnd
} else {
// This isn't actually an HTML entity, so just trim the semicolon
linkEnd = linkEnd - 1
}
} else if c == ')' {
// Only allow an autolink ending with a bracket if that bracket is part of a matching pair of brackets.
// If there are more closing brackets than opening ones, remove the extra bracket
numClosing := 0
numOpening := 0
// Examples (input text => output linked portion):
//
// http://www.pokemon.com/Pikachu_(Electric)
// => http://www.pokemon.com/Pikachu_(Electric)
//
// http://www.pokemon.com/Pikachu_((Electric)
// => http://www.pokemon.com/Pikachu_((Electric)
//
// http://www.pokemon.com/Pikachu_(Electric))
// => http://www.pokemon.com/Pikachu_(Electric)
//
// http://www.pokemon.com/Pikachu_((Electric))
// => http://www.pokemon.com/Pikachu_((Electric))
for i := 0; i < linkEnd; i++ {
if runes[i] == '(' {
numOpening += 1
} else if runes[i] == ')' {
numClosing += 1
}
}
if numClosing <= numOpening {
// There's fewer or equal closing brackets, so we've found the end of the link
break
}
linkEnd -= 1
} else {
// There's no special characters at the end of the link, so we're at the end
break
}
}
return start + len(string(runes[:linkEnd]))
}
func canEndAutolink(c rune) bool {
switch c {
case '?', '!', '.', ',', ':', '*', '_', '~', '\'', '"':
return false
}
return true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
type BlockQuote struct {
blockBase
markdown string
Children []Block
}
func (b *BlockQuote) Continuation(indentation int, r Range) *continuation {
if indentation > 3 {
return nil
}
s := b.markdown[r.Position:r.End]
if s == "" || s[0] != '>' {
return nil
}
remaining := Range{r.Position + 1, r.End}
indentation, indentationBytes := countIndentation(b.markdown, remaining)
if indentation > 0 {
indentation--
}
return &continuation{
Indentation: indentation,
Remaining: Range{remaining.Position + indentationBytes, remaining.End},
}
}
func (b *BlockQuote) AddChild(openBlocks []Block) []Block {
b.Children = append(b.Children, openBlocks[0])
return openBlocks
}
func blockQuoteStart(markdown string, indent int, r Range) []Block {
if indent > 3 {
return nil
}
s := markdown[r.Position:r.End]
if s == "" || s[0] != '>' {
return nil
}
block := &BlockQuote{
markdown: markdown,
}
r.Position++
if len(s) > 1 && s[1] == ' ' {
r.Position++
}
indent, bytes := countIndentation(markdown, r)
ret := []Block{block}
if descendants := blockStartOrParagraph(markdown, indent, Range{r.Position + bytes, r.End}, nil, nil); descendants != nil {
block.Children = append(block.Children, descendants[0])
ret = append(ret, descendants...)
}
return ret
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
import (
"strings"
)
type continuation struct {
Indentation int
Remaining Range
}
type Block interface {
Continuation(indentation int, r Range) *continuation
AddLine(indentation int, r Range) bool
Close()
AllowsBlockStarts() bool
HasTrailingBlankLine() bool
}
type blockBase struct{}
func (*blockBase) AddLine(indentation int, r Range) bool { return false }
func (*blockBase) Close() {}
func (*blockBase) AllowsBlockStarts() bool { return true }
func (*blockBase) HasTrailingBlankLine() bool { return false }
type ContainerBlock interface {
Block
AddChild(openBlocks []Block) []Block
}
type Range struct {
Position int
End int
}
func closeBlocks(blocks []Block, referenceDefinitions []*ReferenceDefinition) []*ReferenceDefinition {
for _, block := range blocks {
block.Close()
if p, ok := block.(*Paragraph); ok && len(p.ReferenceDefinitions) > 0 {
referenceDefinitions = append(referenceDefinitions, p.ReferenceDefinitions...)
}
}
return referenceDefinitions
}
func ParseBlocks(markdown string, lines []Line) (*Document, []*ReferenceDefinition) {
document := &Document{}
var referenceDefinitions []*ReferenceDefinition
openBlocks := []Block{document}
for _, line := range lines {
r := line.Range
lastMatchIndex := 0
indentation, indentationBytes := countIndentation(markdown, r)
r = Range{r.Position + indentationBytes, r.End}
for i, block := range openBlocks {
if continuation := block.Continuation(indentation, r); continuation != nil {
indentation = continuation.Indentation
r = continuation.Remaining
additionalIndentation, additionalIndentationBytes := countIndentation(markdown, r)
r = Range{r.Position + additionalIndentationBytes, r.End}
indentation += additionalIndentation
lastMatchIndex = i
} else {
break
}
}
if openBlocks[lastMatchIndex].AllowsBlockStarts() {
if newBlocks := blockStart(markdown, indentation, r, openBlocks[:lastMatchIndex+1], openBlocks[lastMatchIndex+1:]); newBlocks != nil {
didAdd := false
for i := lastMatchIndex; i >= 0; i-- {
if container, ok := openBlocks[i].(ContainerBlock); ok {
if addedBlocks := container.AddChild(newBlocks); addedBlocks != nil {
referenceDefinitions = closeBlocks(openBlocks[i+1:], referenceDefinitions)
openBlocks = openBlocks[:i+1]
openBlocks = append(openBlocks, addedBlocks...)
didAdd = true
break
}
}
}
if didAdd {
continue
}
}
}
isBlank := strings.TrimSpace(markdown[r.Position:r.End]) == ""
if paragraph, ok := openBlocks[len(openBlocks)-1].(*Paragraph); ok && !isBlank {
paragraph.Text = append(paragraph.Text, r)
continue
}
referenceDefinitions = closeBlocks(openBlocks[lastMatchIndex+1:], referenceDefinitions)
openBlocks = openBlocks[:lastMatchIndex+1]
if openBlocks[lastMatchIndex].AddLine(indentation, r) {
continue
}
if paragraph := newParagraph(markdown, r); paragraph != nil {
for i := lastMatchIndex; i >= 0; i-- {
if container, ok := openBlocks[i].(ContainerBlock); ok {
if newBlocks := container.AddChild([]Block{paragraph}); newBlocks != nil {
referenceDefinitions = closeBlocks(openBlocks[i+1:], referenceDefinitions)
openBlocks = openBlocks[:i+1]
openBlocks = append(openBlocks, newBlocks...)
break
}
}
}
}
}
referenceDefinitions = closeBlocks(openBlocks, referenceDefinitions)
return document, referenceDefinitions
}
func blockStart(markdown string, indentation int, r Range, matchedBlocks, unmatchedBlocks []Block) []Block {
if r.Position >= r.End {
return nil
}
if start := blockQuoteStart(markdown, indentation, r); start != nil {
return start
} else if start := listStart(markdown, indentation, r, matchedBlocks, unmatchedBlocks); start != nil {
return start
} else if start := indentedCodeStart(markdown, indentation, r, matchedBlocks, unmatchedBlocks); start != nil {
return start
} else if start := fencedCodeStart(markdown, indentation, r); start != nil {
return start
}
return nil
}
func blockStartOrParagraph(markdown string, indentation int, r Range, matchedBlocks, unmatchedBlocks []Block) []Block {
if start := blockStart(markdown, indentation, r, matchedBlocks, unmatchedBlocks); start != nil {
return start
}
if paragraph := newParagraph(markdown, r); paragraph != nil {
return []Block{paragraph}
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
type Document struct {
blockBase
Children []Block
}
func (b *Document) Continuation(indentation int, r Range) *continuation {
return &continuation{
Indentation: indentation,
Remaining: r,
}
}
func (b *Document) AddChild(openBlocks []Block) []Block {
b.Children = append(b.Children, openBlocks[0])
return openBlocks
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
import (
"strings"
)
type FencedCodeLine struct {
Indentation int
Range Range
}
type FencedCode struct {
blockBase
markdown string
didSeeClosingFence bool
Indentation int
OpeningFence Range
RawInfo Range
RawCode []FencedCodeLine
}
func (b *FencedCode) Code() (result string) {
for _, code := range b.RawCode {
result += strings.Repeat(" ", code.Indentation) + b.markdown[code.Range.Position:code.Range.End]
}
return
}
func (b *FencedCode) Info() string {
return Unescape(b.markdown[b.RawInfo.Position:b.RawInfo.End])
}
func (b *FencedCode) Continuation(indentation int, r Range) *continuation {
if b.didSeeClosingFence {
return nil
}
return &continuation{
Indentation: indentation,
Remaining: r,
}
}
func (b *FencedCode) AddLine(indentation int, r Range) bool {
s := b.markdown[r.Position:r.End]
if indentation <= 3 && strings.HasPrefix(s, b.markdown[b.OpeningFence.Position:b.OpeningFence.End]) {
suffix := strings.TrimSpace(s[b.OpeningFence.End-b.OpeningFence.Position:])
isClosingFence := true
for _, c := range suffix {
if c != rune(s[0]) {
isClosingFence = false
break
}
}
if isClosingFence {
b.didSeeClosingFence = true
return true
}
}
if indentation >= b.Indentation {
indentation -= b.Indentation
} else {
indentation = 0
}
b.RawCode = append(b.RawCode, FencedCodeLine{
Indentation: indentation,
Range: r,
})
return true
}
func (b *FencedCode) AllowsBlockStarts() bool {
return false
}
func fencedCodeStart(markdown string, indentation int, r Range) []Block {
s := markdown[r.Position:r.End]
if !strings.HasPrefix(s, "```") && !strings.HasPrefix(s, "~~~") {
return nil
}
fenceCharacter := rune(s[0])
fenceLength := 3
for _, c := range s[3:] {
if c == fenceCharacter {
fenceLength++
} else {
break
}
}
for i := r.Position + fenceLength; i < r.End; i++ {
if markdown[i] == '`' {
return nil
}
}
return []Block{
&FencedCode{
markdown: markdown,
Indentation: indentation,
RawInfo: trimRightSpace(markdown, Range{r.Position + fenceLength, r.End}),
OpeningFence: Range{r.Position, r.Position + fenceLength},
},
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
import (
"fmt"
"strings"
)
var htmlEscaper = strings.NewReplacer(
`&`, "&",
`<`, "<",
`>`, ">",
`"`, """,
)
// RenderHTML produces HTML with the same behavior as the example renderer used in the CommonMark
// reference materials except for one slight difference: for brevity, no unnecessary whitespace is
// inserted between elements. The output is not defined by the CommonMark spec, and it exists
// primarily as an aid in testing.
func RenderHTML(markdown string) string {
return RenderBlockHTML(Parse(markdown))
}
func RenderBlockHTML(block Block, referenceDefinitions []*ReferenceDefinition) (result string) {
return renderBlockHTML(block, referenceDefinitions, false)
}
func renderBlockHTML(block Block, referenceDefinitions []*ReferenceDefinition, isTightList bool) (result string) {
switch v := block.(type) {
case *Document:
for _, block := range v.Children {
result += RenderBlockHTML(block, referenceDefinitions)
}
case *Paragraph:
if len(v.Text) == 0 {
return
}
if !isTightList {
result += "<p>"
}
for _, inline := range v.ParseInlines(referenceDefinitions) {
result += RenderInlineHTML(inline)
}
if !isTightList {
result += "</p>"
}
case *List:
if v.IsOrdered {
if v.OrderedStart != 1 {
result += fmt.Sprintf(`<ol start="%v">`, v.OrderedStart)
} else {
result += "<ol>"
}
} else {
result += "<ul>"
}
for _, block := range v.Children {
result += renderBlockHTML(block, referenceDefinitions, !v.IsLoose)
}
if v.IsOrdered {
result += "</ol>"
} else {
result += "</ul>"
}
case *ListItem:
result += "<li>"
for _, block := range v.Children {
result += renderBlockHTML(block, referenceDefinitions, isTightList)
}
result += "</li>"
case *BlockQuote:
result += "<blockquote>"
for _, block := range v.Children {
result += RenderBlockHTML(block, referenceDefinitions)
}
result += "</blockquote>"
case *FencedCode:
if info := v.Info(); info != "" {
language := strings.Fields(info)[0]
result += `<pre><code class="language-` + htmlEscaper.Replace(language) + `">`
} else {
result += "<pre><code>"
}
result += htmlEscaper.Replace(v.Code()) + "</code></pre>"
case *IndentedCode:
result += "<pre><code>" + htmlEscaper.Replace(v.Code()) + "</code></pre>"
default:
panic(fmt.Sprintf("missing case for type %T", v))
}
return
}
func escapeURL(url string) (result string) {
for i := 0; i < len(url); {
switch b := url[i]; b {
case ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '-', '_', '.', '!', '~', '*', '\'', '(', ')', '#':
result += string(b)
i++
default:
if b == '%' && i+2 < len(url) && isHexByte(url[i+1]) && isHexByte(url[i+2]) {
result += url[i : i+3]
i += 3
} else if (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') {
result += string(b)
i++
} else {
result += fmt.Sprintf("%%%0X", b)
i++
}
}
}
return
}
func RenderInlineHTML(inline Inline) (result string) {
switch v := inline.(type) {
case *Text:
return htmlEscaper.Replace(v.Text)
case *HardLineBreak:
return "<br />"
case *SoftLineBreak:
return "\n"
case *CodeSpan:
return "<code>" + htmlEscaper.Replace(v.Code) + "</code>"
case *InlineImage:
result += `<img src="` + htmlEscaper.Replace(escapeURL(v.Destination())) + `" alt="` + htmlEscaper.Replace(renderImageAltText(v.Children)) + `"`
if title := v.Title(); title != "" {
result += ` title="` + htmlEscaper.Replace(title) + `"`
}
result += ` />`
case *ReferenceImage:
result += `<img src="` + htmlEscaper.Replace(escapeURL(v.Destination())) + `" alt="` + htmlEscaper.Replace(renderImageAltText(v.Children)) + `"`
if title := v.Title(); title != "" {
result += ` title="` + htmlEscaper.Replace(title) + `"`
}
result += ` />`
case *InlineLink:
result += `<a href="` + htmlEscaper.Replace(escapeURL(v.Destination())) + `"`
if title := v.Title(); title != "" {
result += ` title="` + htmlEscaper.Replace(title) + `"`
}
result += `>`
for _, inline := range v.Children {
result += RenderInlineHTML(inline)
}
result += "</a>"
case *ReferenceLink:
result += `<a href="` + htmlEscaper.Replace(escapeURL(v.Destination())) + `"`
if title := v.Title(); title != "" {
result += ` title="` + htmlEscaper.Replace(title) + `"`
}
result += `>`
for _, inline := range v.Children {
result += RenderInlineHTML(inline)
}
result += "</a>"
case *Autolink:
result += `<a href="` + htmlEscaper.Replace(escapeURL(v.Destination())) + `">`
for _, inline := range v.Children {
result += RenderInlineHTML(inline)
}
result += "</a>"
default:
panic(fmt.Sprintf("missing case for type %T", v))
}
return
}
func renderImageAltText(children []Inline) (result string) {
for _, inline := range children {
result += renderImageChildAltText(inline)
}
return
}
func renderImageChildAltText(inline Inline) (result string) {
switch v := inline.(type) {
case *Text:
return v.Text
case *InlineImage:
for _, inline := range v.Children {
result += renderImageChildAltText(inline)
}
case *InlineLink:
for _, inline := range v.Children {
result += renderImageChildAltText(inline)
}
}
return
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
import (
"strings"
)
type IndentedCodeLine struct {
Indentation int
Range Range
}
type IndentedCode struct {
blockBase
markdown string
RawCode []IndentedCodeLine
}
func (b *IndentedCode) Code() (result string) {
for _, code := range b.RawCode {
result += strings.Repeat(" ", code.Indentation) + b.markdown[code.Range.Position:code.Range.End]
}
return
}
func (b *IndentedCode) Continuation(indentation int, r Range) *continuation {
if indentation >= 4 {
return &continuation{
Indentation: indentation - 4,
Remaining: r,
}
}
s := b.markdown[r.Position:r.End]
if strings.TrimSpace(s) == "" {
return &continuation{
Remaining: r,
}
}
return nil
}
func (b *IndentedCode) AddLine(indentation int, r Range) bool {
b.RawCode = append(b.RawCode, IndentedCodeLine{
Indentation: indentation,
Range: r,
})
return true
}
func (b *IndentedCode) Close() {
for {
last := b.RawCode[len(b.RawCode)-1]
s := b.markdown[last.Range.Position:last.Range.End]
if strings.TrimRight(s, "\r\n") == "" {
b.RawCode = b.RawCode[:len(b.RawCode)-1]
} else {
break
}
}
}
func (b *IndentedCode) AllowsBlockStarts() bool {
return false
}
func indentedCodeStart(markdown string, indentation int, r Range, matchedBlocks, unmatchedBlocks []Block) []Block {
if len(unmatchedBlocks) > 0 {
if _, ok := unmatchedBlocks[len(unmatchedBlocks)-1].(*Paragraph); ok {
return nil
}
} else if len(matchedBlocks) > 0 {
if _, ok := matchedBlocks[len(matchedBlocks)-1].(*Paragraph); ok {
return nil
}
}
if indentation < 4 {
return nil
}
s := markdown[r.Position:r.End]
if strings.TrimSpace(s) == "" {
return nil
}
return []Block{
&IndentedCode{
markdown: markdown,
RawCode: []IndentedCodeLine{{
Indentation: indentation - 4,
Range: r,
}},
},
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
import (
"container/list"
"strings"
"unicode"
"unicode/utf8"
)
type Inline interface {
IsInline() bool
}
type inlineBase struct{}
func (inlineBase) IsInline() bool { return true }
type Text struct {
inlineBase
Text string
Range Range
}
type CodeSpan struct {
inlineBase
Code string
}
type HardLineBreak struct {
inlineBase
}
type SoftLineBreak struct {
inlineBase
}
type InlineLinkOrImage struct {
inlineBase
Children []Inline
RawDestination Range
markdown string
rawTitle string
}
func (i *InlineLinkOrImage) Destination() string {
return Unescape(i.markdown[i.RawDestination.Position:i.RawDestination.End])
}
func (i *InlineLinkOrImage) Title() string {
return Unescape(i.rawTitle)
}
type InlineLink struct {
InlineLinkOrImage
}
type InlineImage struct {
InlineLinkOrImage
}
type ReferenceLinkOrImage struct {
inlineBase
*ReferenceDefinition
Children []Inline
}
type ReferenceLink struct {
ReferenceLinkOrImage
}
type ReferenceImage struct {
ReferenceLinkOrImage
}
type Autolink struct {
inlineBase
Children []Inline
RawDestination Range
markdown string
}
func (i *Autolink) Destination() string {
destination := Unescape(i.markdown[i.RawDestination.Position:i.RawDestination.End])
if strings.HasPrefix(destination, "www") {
destination = "http://" + destination
}
return destination
}
type delimiterType int
const (
linkOpeningDelimiter delimiterType = iota
imageOpeningDelimiter
)
type delimiter struct {
Type delimiterType
IsInactive bool
TextNode int
Range Range
}
type inlineParser struct {
markdown string
ranges []Range
referenceDefinitions []*ReferenceDefinition
raw string
position int
inlines []Inline
delimiterStack *list.List
}
func newInlineParser(markdown string, ranges []Range, referenceDefinitions []*ReferenceDefinition) *inlineParser {
return &inlineParser{
markdown: markdown,
ranges: ranges,
referenceDefinitions: referenceDefinitions,
delimiterStack: list.New(),
}
}
func (p *inlineParser) parseBackticks() {
count := 1
for i := p.position + 1; i < len(p.raw) && p.raw[i] == '`'; i++ {
count++
}
opening := p.raw[p.position : p.position+count]
search := p.position + count
for search < len(p.raw) {
end := strings.Index(p.raw[search:], opening)
if end == -1 {
break
}
if search+end+count < len(p.raw) && p.raw[search+end+count] == '`' {
search += end + count
for search < len(p.raw) && p.raw[search] == '`' {
search++
}
continue
}
code := strings.Join(strings.Fields(p.raw[p.position+count:search+end]), " ")
p.position = search + end + count
p.inlines = append(p.inlines, &CodeSpan{
Code: code,
})
return
}
p.position += len(opening)
absPos := relativeToAbsolutePosition(p.ranges, p.position-len(opening))
p.inlines = append(p.inlines, &Text{
Text: opening,
Range: Range{absPos, absPos + len(opening)},
})
}
func (p *inlineParser) parseLineEnding() {
if p.position >= 1 && p.raw[p.position-1] == '\t' {
p.inlines = append(p.inlines, &HardLineBreak{})
} else if p.position >= 2 && p.raw[p.position-1] == ' ' && (p.raw[p.position-2] == '\t' || p.raw[p.position-1] == ' ') {
p.inlines = append(p.inlines, &HardLineBreak{})
} else {
p.inlines = append(p.inlines, &SoftLineBreak{})
}
p.position++
if p.position < len(p.raw) && p.raw[p.position] == '\n' {
p.position++
}
}
func (p *inlineParser) parseEscapeCharacter() {
if p.position+1 < len(p.raw) && isEscapableByte(p.raw[p.position+1]) {
absPos := relativeToAbsolutePosition(p.ranges, p.position+1)
p.inlines = append(p.inlines, &Text{
Text: string(p.raw[p.position+1]),
Range: Range{absPos, absPos + len(string(p.raw[p.position+1]))},
})
p.position += 2
} else {
absPos := relativeToAbsolutePosition(p.ranges, p.position)
p.inlines = append(p.inlines, &Text{
Text: `\`,
Range: Range{absPos, absPos + 1},
})
p.position++
}
}
func (p *inlineParser) parseText() {
if next := strings.IndexAny(p.raw[p.position:], "\r\n\\`&![]wW:"); next == -1 {
absPos := relativeToAbsolutePosition(p.ranges, p.position)
p.inlines = append(p.inlines, &Text{
Text: strings.TrimRightFunc(p.raw[p.position:], isWhitespace),
Range: Range{absPos, absPos + len(p.raw[p.position:])},
})
p.position = len(p.raw)
} else {
absPos := relativeToAbsolutePosition(p.ranges, p.position)
if p.raw[p.position+next] == '\r' || p.raw[p.position+next] == '\n' {
s := strings.TrimRightFunc(p.raw[p.position:p.position+next], isWhitespace)
p.inlines = append(p.inlines, &Text{
Text: s,
Range: Range{absPos, absPos + len(s)},
})
} else {
if next == 0 {
// Always read at least one character since 'w', 'W', and ':' may not actually match another
// type of node
next = 1
}
p.inlines = append(p.inlines, &Text{
Text: p.raw[p.position : p.position+next],
Range: Range{absPos, absPos + next},
})
}
p.position += next
}
}
func (p *inlineParser) parseLinkOrImageDelimiter() {
absPos := relativeToAbsolutePosition(p.ranges, p.position)
if p.raw[p.position] == '[' {
p.inlines = append(p.inlines, &Text{
Text: "[",
Range: Range{absPos, absPos + 1},
})
p.delimiterStack.PushBack(&delimiter{
Type: linkOpeningDelimiter,
TextNode: len(p.inlines) - 1,
Range: Range{p.position, p.position + 1},
})
p.position++
} else if p.raw[p.position] == '!' && p.position+1 < len(p.raw) && p.raw[p.position+1] == '[' {
p.inlines = append(p.inlines, &Text{
Text: "![",
Range: Range{absPos, absPos + 2},
})
p.delimiterStack.PushBack(&delimiter{
Type: imageOpeningDelimiter,
TextNode: len(p.inlines) - 1,
Range: Range{p.position, p.position + 2},
})
p.position += 2
} else {
p.inlines = append(p.inlines, &Text{
Text: "!",
Range: Range{absPos, absPos + 1},
})
p.position++
}
}
func (p *inlineParser) peekAtInlineLinkDestinationAndTitle(position int, isImage bool) (destination, title Range, end int, ok bool) {
if position >= len(p.raw) || p.raw[position] != '(' {
return
}
position++
destinationStart := nextNonWhitespace(p.raw, position)
if destinationStart >= len(p.raw) {
return
} else if p.raw[destinationStart] == ')' {
return Range{destinationStart, destinationStart}, Range{destinationStart, destinationStart}, destinationStart + 1, true
}
destination, end, ok = parseLinkDestination(p.raw, destinationStart)
if !ok {
return
}
position = end
if isImage && position < len(p.raw) && isWhitespaceByte(p.raw[position]) {
dimensionsStart := nextNonWhitespace(p.raw, position)
if dimensionsStart >= len(p.raw) {
return
}
if p.raw[dimensionsStart] == '=' {
// Read optional image dimensions even if we don't use them
_, end, ok = parseImageDimensions(p.raw, dimensionsStart)
if !ok {
return
}
position = end
}
}
if position < len(p.raw) && isWhitespaceByte(p.raw[position]) {
titleStart := nextNonWhitespace(p.raw, position)
if titleStart >= len(p.raw) {
return
} else if p.raw[titleStart] == ')' {
return destination, Range{titleStart, titleStart}, titleStart + 1, true
}
if p.raw[titleStart] == '"' || p.raw[titleStart] == '\'' || p.raw[titleStart] == '(' {
title, end, ok = parseLinkTitle(p.raw, titleStart)
if !ok {
return
}
position = end
}
}
closingPosition := nextNonWhitespace(p.raw, position)
if closingPosition >= len(p.raw) || p.raw[closingPosition] != ')' {
return Range{}, Range{}, 0, false
}
return destination, title, closingPosition + 1, true
}
func (p *inlineParser) referenceDefinition(label string) *ReferenceDefinition {
clean := strings.Join(strings.Fields(label), " ")
for _, d := range p.referenceDefinitions {
if strings.EqualFold(clean, strings.Join(strings.Fields(d.Label()), " ")) {
return d
}
}
return nil
}
func (p *inlineParser) lookForLinkOrImage() {
for element := p.delimiterStack.Back(); element != nil; element = element.Prev() {
d := element.Value.(*delimiter)
if d.Type != imageOpeningDelimiter && d.Type != linkOpeningDelimiter {
continue
}
if d.IsInactive {
p.delimiterStack.Remove(element)
break
}
isImage := d.Type == imageOpeningDelimiter
var inline Inline
if destination, title, next, ok := p.peekAtInlineLinkDestinationAndTitle(p.position+1, isImage); ok {
destinationMarkdownPosition := relativeToAbsolutePosition(p.ranges, destination.Position)
linkOrImage := InlineLinkOrImage{
Children: append([]Inline(nil), p.inlines[d.TextNode+1:]...),
RawDestination: Range{destinationMarkdownPosition, destinationMarkdownPosition + destination.End - destination.Position},
markdown: p.markdown,
rawTitle: p.raw[title.Position:title.End],
}
if d.Type == imageOpeningDelimiter {
inline = &InlineImage{linkOrImage}
} else {
inline = &InlineLink{linkOrImage}
}
p.position = next
} else {
referenceLabel := ""
label, next, hasLinkLabel := parseLinkLabel(p.raw, p.position+1)
if hasLinkLabel && label.End > label.Position {
referenceLabel = p.raw[label.Position:label.End]
} else {
referenceLabel = p.raw[d.Range.End:p.position]
if !hasLinkLabel {
next = p.position + 1
}
}
if referenceLabel != "" {
if reference := p.referenceDefinition(referenceLabel); reference != nil {
linkOrImage := ReferenceLinkOrImage{
ReferenceDefinition: reference,
Children: append([]Inline(nil), p.inlines[d.TextNode+1:]...),
}
if d.Type == imageOpeningDelimiter {
inline = &ReferenceImage{linkOrImage}
} else {
inline = &ReferenceLink{linkOrImage}
}
p.position = next
}
}
}
if inline != nil {
if d.Type == imageOpeningDelimiter {
p.inlines = append(p.inlines[:d.TextNode], inline)
} else {
p.inlines = append(p.inlines[:d.TextNode], inline)
for inlineElement := element.Prev(); inlineElement != nil; inlineElement = inlineElement.Prev() {
if d := inlineElement.Value.(*delimiter); d.Type == linkOpeningDelimiter {
d.IsInactive = true
}
}
}
p.delimiterStack.Remove(element)
return
}
p.delimiterStack.Remove(element)
break
}
absPos := relativeToAbsolutePosition(p.ranges, p.position)
p.inlines = append(p.inlines, &Text{
Text: "]",
Range: Range{absPos, absPos + 1},
})
p.position++
}
func CharacterReference(ref string) string {
if ref == "" {
return ""
}
if ref[0] == '#' {
if len(ref) < 2 {
return ""
}
n := 0
if ref[1] == 'X' || ref[1] == 'x' {
if len(ref) < 3 {
return ""
}
for i := 2; i < len(ref); i++ {
if i > 9 {
return ""
}
d := ref[i]
switch {
case d >= '0' && d <= '9':
n = n*16 + int(d-'0')
case d >= 'a' && d <= 'f':
n = n*16 + 10 + int(d-'a')
case d >= 'A' && d <= 'F':
n = n*16 + 10 + int(d-'A')
default:
return ""
}
}
} else {
for i := 1; i < len(ref); i++ {
if i > 8 || ref[i] < '0' || ref[i] > '9' {
return ""
}
n = n*10 + int(ref[i]-'0')
}
}
c := rune(n)
if c == '\u0000' || !utf8.ValidRune(c) {
return string(unicode.ReplacementChar)
}
return string(c)
}
if entity, ok := htmlEntities[ref]; ok {
return entity
}
return ""
}
func (p *inlineParser) parseCharacterReference() {
absPos := relativeToAbsolutePosition(p.ranges, p.position)
p.position++
if semicolon := strings.IndexByte(p.raw[p.position:], ';'); semicolon == -1 {
p.inlines = append(p.inlines, &Text{
Text: "&",
Range: Range{absPos, absPos + 1},
})
} else if s := CharacterReference(p.raw[p.position : p.position+semicolon]); s != "" {
p.position += semicolon + 1
p.inlines = append(p.inlines, &Text{
Text: s,
Range: Range{absPos, absPos + len(s)},
})
} else {
p.inlines = append(p.inlines, &Text{
Text: "&",
Range: Range{absPos, absPos + 1},
})
}
}
func (p *inlineParser) parseAutolink(c rune) bool {
for element := p.delimiterStack.Back(); element != nil; element = element.Prev() {
d := element.Value.(*delimiter)
if !d.IsInactive {
return false
}
}
var link Range
if c == ':' {
var ok bool
link, ok = parseURLAutolink(p.raw, p.position)
if !ok {
return false
}
// Since the current position is at the colon, we have to rewind the parsing slightly so that
// we don't duplicate the URL scheme
rewind := strings.Index(p.raw[link.Position:link.End], ":")
if rewind != -1 {
lastInline := p.inlines[len(p.inlines)-1]
lastText, ok := lastInline.(*Text)
if !ok {
// This should never occur since parseURLAutolink will only return a non-empty value
// when the previous text ends in a valid URL protocol which would mean that the previous
// node is a Text node
return false
}
p.inlines = p.inlines[0 : len(p.inlines)-1]
p.inlines = append(p.inlines, &Text{
Text: lastText.Text[:len(lastText.Text)-rewind],
Range: Range{lastText.Range.Position, lastText.Range.End - rewind},
})
p.position -= rewind
}
} else if c == 'w' || c == 'W' {
var ok bool
link, ok = parseWWWAutolink(p.raw, p.position)
if !ok {
return false
}
}
linkMarkdownPosition := relativeToAbsolutePosition(p.ranges, link.Position)
linkRange := Range{linkMarkdownPosition, linkMarkdownPosition + link.End - link.Position}
p.inlines = append(p.inlines, &Autolink{
Children: []Inline{
&Text{
Text: p.raw[link.Position:link.End],
Range: linkRange,
},
},
RawDestination: linkRange,
markdown: p.markdown,
})
p.position += (link.End - link.Position)
return true
}
func (p *inlineParser) Parse() []Inline {
for _, r := range p.ranges {
p.raw += p.markdown[r.Position:r.End]
}
for p.position < len(p.raw) {
c, _ := utf8.DecodeRuneInString(p.raw[p.position:])
switch c {
case '\r', '\n':
p.parseLineEnding()
case '\\':
p.parseEscapeCharacter()
case '`':
p.parseBackticks()
case '&':
p.parseCharacterReference()
case '!', '[':
p.parseLinkOrImageDelimiter()
case ']':
p.lookForLinkOrImage()
case 'w', 'W', ':':
matched := p.parseAutolink(c)
if !matched {
p.parseText()
}
default:
p.parseText()
}
}
return p.inlines
}
func ParseInlines(markdown string, ranges []Range, referenceDefinitions []*ReferenceDefinition) (inlines []Inline) {
return newInlineParser(markdown, ranges, referenceDefinitions).Parse()
}
func MergeInlineText(inlines []Inline) []Inline {
ret := inlines[:0]
for i, v := range inlines {
// always add first node
if i == 0 {
ret = append(ret, v)
continue
}
// not a text node? nothing to merge
text, ok := v.(*Text)
if !ok {
ret = append(ret, v)
continue
}
// previous node is not a text node? nothing to merge
prevText, ok := ret[len(ret)-1].(*Text)
if !ok {
ret = append(ret, v)
continue
}
// previous node is not right before this one
if prevText.Range.End != text.Range.Position {
ret = append(ret, v)
continue
}
// we have two consecutive text nodes
ret[len(ret)-1] = &Text{
Text: prevText.Text + text.Text,
Range: Range{prevText.Range.Position, text.Range.End},
}
}
return ret
}
func Unescape(markdown string) string {
ret := ""
position := 0
for position < len(markdown) {
c, cSize := utf8.DecodeRuneInString(markdown[position:])
switch c {
case '\\':
if position+1 < len(markdown) && isEscapableByte(markdown[position+1]) {
ret += string(markdown[position+1])
position += 2
} else {
ret += `\`
position++
}
case '&':
position++
if semicolon := strings.IndexByte(markdown[position:], ';'); semicolon == -1 {
ret += "&"
} else if s := CharacterReference(markdown[position : position+semicolon]); s != "" {
position += semicolon + 1
ret += s
} else {
ret += "&"
}
default:
ret += string(c)
position += cSize
}
}
return ret
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
// Inspect traverses the markdown tree in depth-first order. If f returns true, Inspect invokes f
// recursively for each child of the block or inline, followed by a call of f(nil).
func Inspect(markdown string, f func(any) bool) {
document, referenceDefinitions := Parse(markdown)
InspectBlock(document, func(block Block) bool {
if !f(block) {
return false
}
switch v := block.(type) {
case *Paragraph:
for _, inline := range MergeInlineText(v.ParseInlines(referenceDefinitions)) {
InspectInline(inline, func(inline Inline) bool {
return f(inline)
})
}
}
return true
})
}
// InspectBlock traverses the blocks in depth-first order, starting with block. If f returns true,
// InspectBlock invokes f recursively for each child of the block, followed by a call of f(nil).
func InspectBlock(block Block, f func(Block) bool) {
if !f(block) {
return
}
switch v := block.(type) {
case *Document:
for _, child := range v.Children {
InspectBlock(child, f)
}
case *List:
for _, child := range v.Children {
InspectBlock(child, f)
}
case *ListItem:
for _, child := range v.Children {
InspectBlock(child, f)
}
case *BlockQuote:
for _, child := range v.Children {
InspectBlock(child, f)
}
}
f(nil)
}
// InspectInline traverses the blocks in depth-first order, starting with block. If f returns true,
// InspectInline invokes f recursively for each child of the block, followed by a call of f(nil).
func InspectInline(inline Inline, f func(Inline) bool) {
if !f(inline) {
return
}
switch v := inline.(type) {
case *InlineImage:
for _, child := range v.Children {
InspectInline(child, f)
}
case *InlineLink:
for _, child := range v.Children {
InspectInline(child, f)
}
case *ReferenceImage:
for _, child := range v.Children {
InspectInline(child, f)
}
case *ReferenceLink:
for _, child := range v.Children {
InspectInline(child, f)
}
}
f(nil)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
import (
"strings"
)
type Line struct {
Range
}
func ParseLines(markdown string) []Line {
lineStartPosition := 0
isAfterCarriageReturn := false
lines := make([]Line, 0, strings.Count(markdown, "\n"))
for position, r := range markdown {
if r == '\n' {
lines = append(lines, Line{Range{lineStartPosition, position + 1}})
lineStartPosition = position + 1
} else if isAfterCarriageReturn {
lines = append(lines, Line{Range{lineStartPosition, position}})
lineStartPosition = position
}
isAfterCarriageReturn = r == '\r'
}
if lineStartPosition < len(markdown) {
lines = append(lines, Line{Range{lineStartPosition, len(markdown)}})
}
return lines
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
import (
"unicode/utf8"
)
func parseLinkDestination(markdown string, position int) (raw Range, next int, ok bool) {
if position >= len(markdown) {
return
}
if markdown[position] == '<' {
isEscaped := false
for offset, c := range []byte(markdown[position+1:]) {
if isEscaped {
isEscaped = false
if isEscapableByte(c) {
continue
}
}
if c == '\\' {
isEscaped = true
} else if c == '<' {
break
} else if c == '>' {
return Range{position + 1, position + 1 + offset}, position + 1 + offset + 1, true
} else if isWhitespaceByte(c) {
break
}
}
}
openCount := 0
isEscaped := false
for offset, c := range []byte(markdown[position:]) {
if isEscaped {
isEscaped = false
if isEscapableByte(c) {
continue
}
}
switch c {
case '\\':
isEscaped = true
case '(':
openCount++
case ')':
if openCount < 1 {
return Range{position, position + offset}, position + offset, true
}
openCount--
default:
if isWhitespaceByte(c) {
return Range{position, position + offset}, position + offset, true
}
}
}
return Range{position, len(markdown)}, len(markdown), true
}
func parseLinkTitle(markdown string, position int) (raw Range, next int, ok bool) {
if position >= len(markdown) {
return
}
originalPosition := position
var closer byte
switch markdown[position] {
case '"', '\'':
closer = markdown[position]
case '(':
closer = ')'
default:
return
}
position++
for position < len(markdown) {
switch markdown[position] {
case '\\':
position++
if position < len(markdown) && isEscapableByte(markdown[position]) {
position++
}
case closer:
return Range{originalPosition + 1, position}, position + 1, true
default:
position++
}
}
return
}
func parseLinkLabel(markdown string, position int) (raw Range, next int, ok bool) {
if position >= len(markdown) || markdown[position] != '[' {
return
}
originalPosition := position
position++
for position < len(markdown) {
switch markdown[position] {
case '\\':
position++
if position < len(markdown) && isEscapableByte(markdown[position]) {
position++
}
case '[':
return
case ']':
if position-originalPosition >= 1000 && utf8.RuneCountInString(markdown[originalPosition:position]) >= 1000 {
return
}
return Range{originalPosition + 1, position}, position + 1, true
default:
position++
}
}
return
}
// As a non-standard feature, we allow image links to specify dimensions of the image by adding "=WIDTHxHEIGHT"
// after the image destination but before the image title like .
// Both width and height are optional, but at least one of them must be specified.
func parseImageDimensions(markdown string, position int) (raw Range, next int, ok bool) {
if position >= len(markdown) {
return
}
originalPosition := position
// Read =
position += 1
if position >= len(markdown) {
return
}
// Read width
hasWidth := false
for position < len(markdown)-1 && isNumericByte(markdown[position]) {
hasWidth = true
position += 1
}
// Look for early end of dimensions
if isWhitespaceByte(markdown[position]) || markdown[position] == ')' {
return Range{originalPosition, position - 1}, position, true
}
// Read the x
if (markdown[position] != 'x' && markdown[position] != 'X') || position == len(markdown)-1 {
return
}
position += 1
// Read height
hasHeight := false
for position < len(markdown)-1 && isNumericByte(markdown[position]) {
hasHeight = true
position += 1
}
// Make sure the there's no trailing characters
if !isWhitespaceByte(markdown[position]) && markdown[position] != ')' {
return
}
if !hasWidth && !hasHeight {
// At least one of width or height is required
return
}
return Range{originalPosition, position - 1}, position, true
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
import (
"strings"
)
type ListItem struct {
blockBase
markdown string
hasTrailingBlankLine bool
hasBlankLineBetweenChildren bool
Indentation int
Children []Block
}
func (b *ListItem) Continuation(indentation int, r Range) *continuation {
s := b.markdown[r.Position:r.End]
if strings.TrimSpace(s) == "" {
if b.Children == nil {
return nil
}
return &continuation{
Remaining: r,
}
}
if indentation < b.Indentation {
return nil
}
return &continuation{
Indentation: indentation - b.Indentation,
Remaining: r,
}
}
func (b *ListItem) AddChild(openBlocks []Block) []Block {
b.Children = append(b.Children, openBlocks[0])
if b.hasTrailingBlankLine {
b.hasBlankLineBetweenChildren = true
}
b.hasTrailingBlankLine = false
return openBlocks
}
func (b *ListItem) AddLine(indentation int, r Range) bool {
isBlank := strings.TrimSpace(b.markdown[r.Position:r.End]) == ""
if isBlank {
b.hasTrailingBlankLine = true
}
return false
}
func (b *ListItem) HasTrailingBlankLine() bool {
return b.hasTrailingBlankLine || (len(b.Children) > 0 && b.Children[len(b.Children)-1].HasTrailingBlankLine())
}
func (b *ListItem) isLoose() bool {
if b.hasBlankLineBetweenChildren {
return true
}
for i, child := range b.Children {
if i < len(b.Children)-1 && child.HasTrailingBlankLine() {
return true
}
}
return false
}
type List struct {
blockBase
markdown string
hasTrailingBlankLine bool
hasBlankLineBetweenChildren bool
IsLoose bool
IsOrdered bool
OrderedStart int
BulletOrDelimiter byte
Children []*ListItem
}
func (b *List) Continuation(indentation int, r Range) *continuation {
s := b.markdown[r.Position:r.End]
if strings.TrimSpace(s) == "" {
return &continuation{
Remaining: r,
}
}
return &continuation{
Indentation: indentation,
Remaining: r,
}
}
func (b *List) AddChild(openBlocks []Block) []Block {
if item, ok := openBlocks[0].(*ListItem); ok {
b.Children = append(b.Children, item)
if b.hasTrailingBlankLine {
b.hasBlankLineBetweenChildren = true
}
b.hasTrailingBlankLine = false
return openBlocks
} else if list, ok := openBlocks[0].(*List); ok {
if len(list.Children) == 1 && list.IsOrdered == b.IsOrdered && list.BulletOrDelimiter == b.BulletOrDelimiter {
return b.AddChild(openBlocks[1:])
}
}
return nil
}
func (b *List) AddLine(indentation int, r Range) bool {
isBlank := strings.TrimSpace(b.markdown[r.Position:r.End]) == ""
if isBlank {
b.hasTrailingBlankLine = true
}
return false
}
func (b *List) HasTrailingBlankLine() bool {
return b.hasTrailingBlankLine || (len(b.Children) > 0 && b.Children[len(b.Children)-1].HasTrailingBlankLine())
}
func (b *List) isLoose() bool {
if b.hasBlankLineBetweenChildren {
return true
}
for i, child := range b.Children {
if child.isLoose() || (i < len(b.Children)-1 && child.HasTrailingBlankLine()) {
return true
}
}
return false
}
func (b *List) Close() {
b.IsLoose = b.isLoose()
}
func parseListMarker(markdown string, r Range) (success, isOrdered bool, orderedStart int, bulletOrDelimiter byte, markerWidth int, remaining Range) {
digits := 0
n := 0
for i := r.Position; i < r.End && markdown[i] >= '0' && markdown[i] <= '9'; i++ {
digits++
n = n*10 + int(markdown[i]-'0')
}
if digits > 0 {
if digits > 9 || r.Position+digits >= r.End {
return
}
next := markdown[r.Position+digits]
if next != '.' && next != ')' {
return
}
return true, true, n, next, digits + 1, Range{r.Position + digits + 1, r.End}
}
if r.Position >= r.End {
return
}
next := markdown[r.Position]
if next != '-' && next != '+' && next != '*' {
return
}
return true, false, 0, next, 1, Range{r.Position + 1, r.End}
}
func listStart(markdown string, indent int, r Range, matchedBlocks, unmatchedBlocks []Block) []Block {
afterList := false
if len(matchedBlocks) > 0 {
_, afterList = matchedBlocks[len(matchedBlocks)-1].(*List)
}
if !afterList && indent > 3 {
return nil
}
success, isOrdered, orderedStart, bulletOrDelimiter, markerWidth, remaining := parseListMarker(markdown, r)
if !success {
return nil
}
isBlank := strings.TrimSpace(markdown[remaining.Position:remaining.End]) == ""
if len(matchedBlocks) > 0 && len(unmatchedBlocks) == 0 {
if _, ok := matchedBlocks[len(matchedBlocks)-1].(*Paragraph); ok {
if isBlank || (isOrdered && orderedStart != 1) {
return nil
}
}
}
indentAfterMarker, indentBytesAfterMarker := countIndentation(markdown, remaining)
if !isBlank && indentAfterMarker < 1 {
return nil
}
remaining = Range{remaining.Position + indentBytesAfterMarker, remaining.End}
consumedIndentAfterMarker := indentAfterMarker
if isBlank || indentAfterMarker >= 5 {
consumedIndentAfterMarker = 1
}
listItem := &ListItem{
markdown: markdown,
Indentation: indent + markerWidth + consumedIndentAfterMarker,
}
list := &List{
markdown: markdown,
IsOrdered: isOrdered,
OrderedStart: orderedStart,
BulletOrDelimiter: bulletOrDelimiter,
Children: []*ListItem{listItem},
}
ret := []Block{list, listItem}
if descendants := blockStartOrParagraph(markdown, indentAfterMarker-consumedIndentAfterMarker, remaining, nil, nil); descendants != nil {
listItem.Children = append(listItem.Children, descendants[0])
ret = append(ret, descendants...)
}
return ret
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// This package implements a parser for the subset of the CommonMark spec necessary for us to do
// server-side processing. It is not a full implementation and lacks many features. But it is
// complete enough to efficiently and accurately allow us to do what we need to like rewrite image
// URLs for proxying.
package markdown
import (
"strings"
)
func isEscapable(c rune) bool {
return c > ' ' && (c < '0' || (c > '9' && (c < 'A' || (c > 'Z' && (c < 'a' || (c > 'z' && c <= '~'))))))
}
func isEscapableByte(c byte) bool {
return isEscapable(rune(c))
}
func isWhitespace(c rune) bool {
switch c {
case ' ', '\t', '\n', '\u000b', '\u000c', '\r':
return true
}
return false
}
func isWhitespaceByte(c byte) bool {
return isWhitespace(rune(c))
}
func isNumeric(c rune) bool {
return c >= '0' && c <= '9'
}
func isNumericByte(c byte) bool {
return isNumeric(rune(c))
}
func isHex(c rune) bool {
return isNumeric(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
}
func isHexByte(c byte) bool {
return isHex(rune(c))
}
func isAlphanumeric(c rune) bool {
return isNumeric(c) || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
func isAlphanumericByte(c byte) bool {
return isAlphanumeric(rune(c))
}
func nextNonWhitespace(markdown string, position int) int {
for offset, c := range []byte(markdown[position:]) {
if !isWhitespaceByte(c) {
return position + offset
}
}
return len(markdown)
}
func nextLine(markdown string, position int) (linePosition int, skippedNonWhitespace bool) {
for i := position; i < len(markdown); i++ {
c := markdown[i]
if c == '\r' {
if i+1 < len(markdown) && markdown[i+1] == '\n' {
return i + 2, skippedNonWhitespace
}
return i + 1, skippedNonWhitespace
} else if c == '\n' {
return i + 1, skippedNonWhitespace
} else if !isWhitespaceByte(c) {
skippedNonWhitespace = true
}
}
return len(markdown), skippedNonWhitespace
}
func countIndentation(markdown string, r Range) (spaces, bytes int) {
for i := r.Position; i < r.End; i++ {
if markdown[i] == ' ' {
spaces++
bytes++
} else if markdown[i] == '\t' {
spaces += 4
bytes++
} else {
break
}
}
return
}
func trimLeftSpace(markdown string, r Range) Range {
s := markdown[r.Position:r.End]
trimmed := strings.TrimLeftFunc(s, isWhitespace)
return Range{r.Position, r.End - (len(s) - len(trimmed))}
}
func trimRightSpace(markdown string, r Range) Range {
s := markdown[r.Position:r.End]
trimmed := strings.TrimRightFunc(s, isWhitespace)
return Range{r.Position, r.End - (len(s) - len(trimmed))}
}
func relativeToAbsolutePosition(ranges []Range, position int) int {
rem := position
for _, r := range ranges {
l := r.End - r.Position
if rem < l {
return r.Position + rem
}
rem -= l
}
if len(ranges) == 0 {
return 0
}
return ranges[len(ranges)-1].End
}
func trimBytesFromRanges(ranges []Range, bytes int) (result []Range) {
rem := bytes
for _, r := range ranges {
if rem == 0 {
result = append(result, r)
continue
}
l := r.End - r.Position
if rem < l {
result = append(result, Range{r.Position + rem, r.End})
rem = 0
continue
}
rem -= l
}
return
}
func Parse(markdown string) (*Document, []*ReferenceDefinition) {
lines := ParseLines(markdown)
return ParseBlocks(markdown, lines)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
import (
"strings"
)
type Paragraph struct {
blockBase
markdown string
Text []Range
ReferenceDefinitions []*ReferenceDefinition
}
func (b *Paragraph) ParseInlines(referenceDefinitions []*ReferenceDefinition) []Inline {
return ParseInlines(b.markdown, b.Text, referenceDefinitions)
}
func (b *Paragraph) Continuation(indentation int, r Range) *continuation {
s := b.markdown[r.Position:r.End]
if strings.TrimSpace(s) == "" {
return nil
}
return &continuation{
Indentation: indentation,
Remaining: r,
}
}
func (b *Paragraph) Close() {
for {
for i := 0; i < len(b.Text); i++ {
b.Text[i] = trimLeftSpace(b.markdown, b.Text[i])
if b.Text[i].Position < b.Text[i].End {
break
}
}
if len(b.Text) == 0 || b.Text[0].Position < b.Text[0].End && b.markdown[b.Text[0].Position] != '[' {
break
}
definition, remaining := parseReferenceDefinition(b.markdown, b.Text)
if definition == nil {
break
}
b.ReferenceDefinitions = append(b.ReferenceDefinitions, definition)
b.Text = remaining
}
for i := len(b.Text) - 1; i >= 0; i-- {
b.Text[i] = trimRightSpace(b.markdown, b.Text[i])
if b.Text[i].Position < b.Text[i].End {
break
}
}
}
func newParagraph(markdown string, r Range) *Paragraph {
s := markdown[r.Position:r.End]
if strings.TrimSpace(s) == "" {
return nil
}
return &Paragraph{
markdown: markdown,
Text: []Range{r},
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package markdown
type ReferenceDefinition struct {
RawDestination Range
markdown string
rawLabel string
rawTitle string
}
func (d *ReferenceDefinition) Destination() string {
return Unescape(d.markdown[d.RawDestination.Position:d.RawDestination.End])
}
func (d *ReferenceDefinition) Label() string {
return d.rawLabel
}
func (d *ReferenceDefinition) Title() string {
return Unescape(d.rawTitle)
}
func parseReferenceDefinition(markdown string, ranges []Range) (*ReferenceDefinition, []Range) {
raw := ""
for _, r := range ranges {
raw += markdown[r.Position:r.End]
}
label, next, ok := parseLinkLabel(raw, 0)
if !ok {
return nil, nil
}
position := next
if position >= len(raw) || raw[position] != ':' {
return nil, nil
}
position++
destination, next, ok := parseLinkDestination(raw, nextNonWhitespace(raw, position))
if !ok {
return nil, nil
}
position = next
absoluteDestination := relativeToAbsolutePosition(ranges, destination.Position)
ret := &ReferenceDefinition{
RawDestination: Range{absoluteDestination, absoluteDestination + destination.End - destination.Position},
markdown: markdown,
rawLabel: raw[label.Position:label.End],
}
if position < len(raw) && isWhitespaceByte(raw[position]) {
title, next, ok := parseLinkTitle(raw, nextNonWhitespace(raw, position))
if !ok {
if nextLine, skippedNonWhitespace := nextLine(raw, position); !skippedNonWhitespace {
return ret, trimBytesFromRanges(ranges, nextLine)
}
return nil, nil
}
if nextLine, skippedNonWhitespace := nextLine(raw, next); !skippedNonWhitespace {
ret.rawTitle = raw[title.Position:title.End]
return ret, trimBytesFromRanges(ranges, nextLine)
}
}
if nextLine, skippedNonWhitespace := nextLine(raw, position); !skippedNonWhitespace {
return ret, trimBytesFromRanges(ranges, nextLine)
}
return nil, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mfa
import (
"crypto/rand"
"encoding/base32"
"fmt"
"net/url"
"strings"
"github.com/dgryski/dgoogauth"
"github.com/mattermost/rsc/qr"
"github.com/pkg/errors"
)
// InvalidToken indicates the case where the token validation has failed.
var InvalidToken = errors.New("invalid mfa token")
const (
// This will result in 160 bits of entropy (base32 encoded), as recommended by rfc4226.
mfaSecretSize = 20
)
type Store interface {
UpdateMfaActive(userId string, active bool) error
UpdateMfaSecret(userId, secret string) error
}
type MFA struct {
store Store
}
func New(store Store) *MFA {
return &MFA{store}
}
// newRandomBase32String returns a base32 encoded string of a random slice
// of bytes of the given size. The resulting entropy will be (8 * size) bits.
func newRandomBase32String(size int) string {
data := make([]byte, size)
rand.Read(data)
return base32.StdEncoding.EncodeToString(data)
}
func getIssuerFromURL(uri string) string {
issuer := "Mattermost"
siteURL := strings.TrimSpace(uri)
if siteURL != "" {
siteURL = strings.TrimPrefix(siteURL, "https://")
siteURL = strings.TrimPrefix(siteURL, "http://")
issuer = strings.TrimPrefix(siteURL, "www.")
}
return url.QueryEscape(issuer)
}
// GenerateSecret generates a new user mfa secret and store it with the StoreSecret function provided
func (m *MFA) GenerateSecret(siteURL, userEmail, userID string) (string, []byte, error) {
issuer := getIssuerFromURL(siteURL)
secret := newRandomBase32String(mfaSecretSize)
authLink := fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s", issuer, userEmail, secret, issuer)
code, err := qr.Encode(authLink, qr.H)
if err != nil {
return "", nil, errors.Wrap(err, "unable to generate qr code")
}
img := code.PNG()
if err := m.store.UpdateMfaSecret(userID, secret); err != nil {
return "", nil, errors.Wrap(err, "unable to store mfa secret")
}
return secret, img, nil
}
// Activate set the mfa as active and store it with the StoreActive function provided
func (m *MFA) Activate(userMfaSecret, userID string, token string) error {
otpConfig := &dgoogauth.OTPConfig{
Secret: userMfaSecret,
WindowSize: 3,
HotpCounter: 0,
}
trimmedToken := strings.TrimSpace(token)
ok, err := otpConfig.Authenticate(trimmedToken)
if err != nil {
return errors.Wrap(err, "unable to parse the token")
}
if !ok {
return InvalidToken
}
if err := m.store.UpdateMfaActive(userID, true); err != nil {
return errors.Wrap(err, "unable to store mfa active")
}
return nil
}
// Deactivate set the mfa as deactivated, remove the mfa secret, store it with the StoreActive and StoreSecret functions provided
func (m *MFA) Deactivate(userId string) error {
if err := m.store.UpdateMfaActive(userId, false); err != nil {
return errors.Wrap(err, "unable to store mfa active")
}
if err := m.store.UpdateMfaSecret(userId, ""); err != nil {
return errors.Wrap(err, "unable to store mfa secret")
}
return nil
}
// Validate the provide token using the secret provided
func (m *MFA) ValidateToken(secret, token string) (bool, error) {
otpConfig := &dgoogauth.OTPConfig{
Secret: secret,
WindowSize: 3,
HotpCounter: 0,
}
trimmedToken := strings.TrimSpace(token)
ok, err := otpConfig.Authenticate(trimmedToken)
if err != nil {
return false, errors.Wrap(err, "unable to parse the token")
}
return ok, nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mlog
import (
"bytes"
"encoding/json"
"fmt"
"os"
)
// defaultLog manually encodes the log to STDERR, providing a basic, default logging implementation
// before mlog is fully configured.
func defaultLog(level Level, msg string, fields ...Field) {
mFields := make(map[string]string)
buf := &bytes.Buffer{}
for _, fld := range fields {
buf.Reset()
fld.ValueString(buf, shouldQuote)
mFields[fld.Key] = buf.String()
}
log := struct {
Level string `json:"level"`
Message string `json:"msg"`
Fields map[string]string `json:"fields,omitempty"`
}{
level.Name,
msg,
mFields,
}
if b, err := json.Marshal(log); err != nil {
fmt.Fprintf(os.Stderr, `{"level":"error","msg":"failed to encode log message"}%s`, "\n")
} else {
fmt.Fprintf(os.Stderr, "%s\n", b)
}
}
func defaultIsLevelEnabled(level Level) bool {
return true
}
func defaultCustomMultiLog(lvl []Level, msg string, fields ...Field) {
for _, level := range lvl {
defaultLog(level, msg, fields...)
}
}
// shouldQuote returns true if val contains any characters that require quotations.
func shouldQuote(val string) bool {
for _, c := range val {
if !((c >= '0' && c <= '9') ||
(c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
c == '-' || c == '.' || c == '_' || c == '/' || c == '@' || c == '^' || c == '+') {
return true
}
}
return false
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mlog
import (
"sync"
)
var (
globalLogger *Logger
muxGlobalLogger sync.RWMutex
)
func InitGlobalLogger(logger *Logger) {
muxGlobalLogger.Lock()
defer muxGlobalLogger.Unlock()
globalLogger = logger
}
func getGlobalLogger() *Logger {
muxGlobalLogger.RLock()
defer muxGlobalLogger.RUnlock()
return globalLogger
}
// IsLevelEnabled returns true only if at least one log target is
// configured to emit the specified log level. Use this check when
// gathering the log info may be expensive.
//
// Note, transformations and serializations done via fields are already
// lazily evaluated and don't require this check beforehand.
func IsLevelEnabled(level Level) bool {
logger := getGlobalLogger()
if logger == nil {
return defaultIsLevelEnabled(level)
}
return logger.IsLevelEnabled(level)
}
// Log emits the log record for any targets configured for the specified level.
func Log(level Level, msg string, fields ...Field) {
logger := getGlobalLogger()
if logger == nil {
defaultLog(level, msg, fields...)
return
}
logger.Log(level, msg, fields...)
}
// LogM emits the log record for any targets configured for the specified levels.
// Equivalent to calling `Log` once for each level.
func LogM(levels []Level, msg string, fields ...Field) {
logger := getGlobalLogger()
if logger == nil {
defaultCustomMultiLog(levels, msg, fields...)
return
}
logger.LogM(levels, msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Trace` level.
func Trace(msg string, fields ...Field) {
logger := getGlobalLogger()
if logger == nil {
defaultLog(LvlTrace, msg, fields...)
return
}
logger.Trace(msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Debug` level.
func Debug(msg string, fields ...Field) {
logger := getGlobalLogger()
if logger == nil {
defaultLog(LvlDebug, msg, fields...)
return
}
logger.Debug(msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Info` level.
func Info(msg string, fields ...Field) {
logger := getGlobalLogger()
if logger == nil {
defaultLog(LvlInfo, msg, fields...)
return
}
logger.Info(msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Warn` level.
func Warn(msg string, fields ...Field) {
logger := getGlobalLogger()
if logger == nil {
defaultLog(LvlWarn, msg, fields...)
return
}
logger.Warn(msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Error` level.
func Error(msg string, fields ...Field) {
logger := getGlobalLogger()
if logger == nil {
defaultLog(LvlError, msg, fields...)
return
}
logger.Error(msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Critical` level.
// DEPRECATED: Either use Error or Fatal.
// Critical level isn't added in mlog/levels.go:StdAll so calling this doesn't
// really work. For now we just call Fatal to atleast print something.
func Critical(msg string, fields ...Field) {
Fatal(msg, fields...)
}
func Fatal(msg string, fields ...Field) {
logger := getGlobalLogger()
if logger == nil {
defaultLog(LvlFatal, msg, fields...)
return
}
logger.Fatal(msg, fields...)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mlog
import (
"context"
)
// GraphQLLogger is used to log panics that occur during query execution.
type GraphQLLogger struct {
logger *Logger
}
func NewGraphQLLogger(logger *Logger) *GraphQLLogger {
return &GraphQLLogger{logger: logger}
}
// LogPanic satisfies the graphql/log.Logger interface.
// It converts the panic into an error.
func (l *GraphQLLogger) LogPanic(_ context.Context, value any) {
l.logger.Error("Error while executing GraphQL query", Any("error", value))
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Package mlog provides a simple wrapper around Logr.
package mlog
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"strings"
"sync/atomic"
"time"
"github.com/mattermost/logr/v2"
logrcfg "github.com/mattermost/logr/v2/config"
)
const (
ShutdownTimeout = time.Second * 15
FlushTimeout = time.Second * 15
DefaultMaxQueueSize = 1000
DefaultMetricsUpdateFreqMillis = 15000
)
type LoggerIFace interface {
IsLevelEnabled(Level) bool
Trace(string, ...Field)
Debug(string, ...Field)
Info(string, ...Field)
Warn(string, ...Field)
Error(string, ...Field)
Critical(string, ...Field)
Fatal(string, ...Field)
Log(Level, string, ...Field)
LogM([]Level, string, ...Field)
With(fields ...Field) *Logger
Flush() error
StdLogger(level Level) *log.Logger
}
// Type and function aliases from Logr to limit the spread of dependencies.
type Field = logr.Field
type Level = logr.Level
type Option = logr.Option
type Target = logr.Target
type TargetInfo = logr.TargetInfo
type LogRec = logr.LogRec
type LogCloner = logr.LogCloner
type MetricsCollector = logr.MetricsCollector
type TargetCfg = logrcfg.TargetCfg
type TargetFactory = logrcfg.TargetFactory
type FormatterFactory = logrcfg.FormatterFactory
type Factories = logrcfg.Factories
type Sugar = logr.Sugar
// LoggerConfiguration is a map of LogTarget configurations.
type LoggerConfiguration map[string]TargetCfg
func (lc LoggerConfiguration) Append(cfg LoggerConfiguration) {
for k, v := range cfg {
lc[k] = v
}
}
func (lc LoggerConfiguration) toTargetCfg() map[string]logrcfg.TargetCfg {
tcfg := make(map[string]logrcfg.TargetCfg)
for k, v := range lc {
tcfg[k] = v
}
return tcfg
}
// Any picks the best supported field type based on type of val.
// For best performance when passing a struct (or struct pointer),
// implement `logr.LogWriter` on the struct, otherwise reflection
// will be used to generate a string representation.
var Any = logr.Any
// Int64 constructs a field containing a key and Int64 value.
var Int64 = logr.Int64
// Int32 constructs a field containing a key and Int32 value.
var Int32 = logr.Int32
// Int constructs a field containing a key and Int value.
var Int = logr.Int
// Uint64 constructs a field containing a key and Uint64 value.
var Uint64 = logr.Uint64
// Uint32 constructs a field containing a key and Uint32 value.
var Uint32 = logr.Uint32
// Uint constructs a field containing a key and Uint value.
var Uint = logr.Uint
// Float64 constructs a field containing a key and Float64 value.
var Float64 = logr.Float64
// Float32 constructs a field containing a key and Float32 value.
var Float32 = logr.Float32
// String constructs a field containing a key and String value.
var String = logr.String
// Stringer constructs a field containing a key and a fmt.Stringer value.
// The fmt.Stringer's `String` method is called lazily.
var Stringer = func(key string, s fmt.Stringer) logr.Field {
if s == nil {
return Field{Key: key, Type: logr.StringType, String: ""}
}
return Field{Key: key, Type: logr.StringType, String: s.String()}
}
// Err constructs a field containing a default key ("error") and error value.
var Err = func(err error) logr.Field {
return NamedErr("error", err)
}
// NamedErr constructs a field containing a key and error value.
var NamedErr = func(key string, err error) logr.Field {
if err == nil {
return Field{Key: key, Type: logr.StringType, String: ""}
}
return Field{Key: key, Type: logr.StringType, String: err.Error()}
}
// Bool constructs a field containing a key and bool value.
var Bool = logr.Bool
// Time constructs a field containing a key and time.Time value.
var Time = logr.Time
// Duration constructs a field containing a key and time.Duration value.
var Duration = logr.Duration
// Millis constructs a field containing a key and timestamp value.
// The timestamp is expected to be milliseconds since Jan 1, 1970 UTC.
var Millis = logr.Millis
// Array constructs a field containing a key and array value.
var Array = logr.Array
// Map constructs a field containing a key and map value.
var Map = logr.Map
// Logger provides a thin wrapper around a Logr instance. This is a struct instead of an interface
// so that there are no allocations on the heap each interface method invocation. Normally not
// something to be concerned about, but logging calls for disabled levels should have as little CPU
// and memory impact as possible. Most of these wrapper calls will be inlined as well.
type Logger struct {
log *logr.Logger
lockConfig *int32
}
// NewLogger creates a new Logger instance which can be configured via `(*Logger).Configure`.
// Some options with invalid values can cause an error to be returned, however `NewLogger()`
// using just defaults never errors.
func NewLogger(options ...Option) (*Logger, error) {
options = append(options, logr.StackFilter(logr.GetPackageName("NewLogger")))
lgr, err := logr.New(options...)
if err != nil {
return nil, err
}
log := lgr.NewLogger()
var lockConfig int32
return &Logger{
log: &log,
lockConfig: &lockConfig,
}, nil
}
// Configure provides a new configuration for this logger.
// Zero or more sources of config can be provided:
//
// cfgFile - path to file containing JSON
// cfgEscaped - JSON string probably from ENV var
//
// For each case JSON containing log targets is provided. Target name collisions are resolved
// using the following precedence:
//
// cfgFile > cfgEscaped
//
// An optional set of factories can be provided which will be called to create any target
// types or formatters not built-in.
func (l *Logger) Configure(cfgFile string, cfgEscaped string, factories *Factories) error {
if atomic.LoadInt32(l.lockConfig) != 0 {
return ErrConfigurationLock
}
cfgMap := make(LoggerConfiguration)
// Add config from file
if cfgFile != "" {
b, err := os.ReadFile(cfgFile)
if err != nil {
return fmt.Errorf("error reading logger config file %s: %w", cfgFile, err)
}
var mapCfgFile LoggerConfiguration
if err := json.Unmarshal(b, &mapCfgFile); err != nil {
return fmt.Errorf("error decoding logger config file %s: %w", cfgFile, err)
}
cfgMap.Append(mapCfgFile)
}
// Add config from escaped json string
if cfgEscaped != "" {
var mapCfgEscaped LoggerConfiguration
if err := json.Unmarshal([]byte(cfgEscaped), &mapCfgEscaped); err != nil {
return fmt.Errorf("error decoding logger config as escaped json: %w", err)
}
cfgMap.Append(mapCfgEscaped)
}
if len(cfgMap) == 0 {
return nil
}
return logrcfg.ConfigureTargets(l.log.Logr(), cfgMap.toTargetCfg(), factories)
}
// ConfigureTargets provides a new configuration for this logger via a `LoggerConfig` map.
// Typically `mlog.Configure` is used instead which accepts JSON formatted configuration.
// An optional set of factories can be provided which will be called to create any target
// types or formatters not built-in.
func (l *Logger) ConfigureTargets(cfg LoggerConfiguration, factories *Factories) error {
if atomic.LoadInt32(l.lockConfig) != 0 {
return ErrConfigurationLock
}
return logrcfg.ConfigureTargets(l.log.Logr(), cfg.toTargetCfg(), factories)
}
// LockConfiguration disallows further configuration changes until `UnlockConfiguration`
// is called. The previous locked stated is returned.
func (l *Logger) LockConfiguration() bool {
old := atomic.SwapInt32(l.lockConfig, 1)
return old != 0
}
// UnlockConfiguration allows configuration changes. The previous locked stated is returned.
func (l *Logger) UnlockConfiguration() bool {
old := atomic.SwapInt32(l.lockConfig, 0)
return old != 0
}
// IsConfigurationLocked returns the current state of the configuration lock.
func (l *Logger) IsConfigurationLocked() bool {
return atomic.LoadInt32(l.lockConfig) != 0
}
// With creates a new Logger with the specified fields. This is a light-weight
// operation and can be called on demand.
func (l *Logger) With(fields ...Field) *Logger {
logWith := l.log.With(fields...)
return &Logger{
log: &logWith,
lockConfig: l.lockConfig,
}
}
// IsLevelEnabled returns true only if at least one log target is
// configured to emit the specified log level. Use this check when
// gathering the log info may be expensive.
//
// Note, transformations and serializations done via fields are already
// lazily evaluated and don't require this check beforehand.
func (l *Logger) IsLevelEnabled(level Level) bool {
return l.log.IsLevelEnabled(level)
}
// Log emits the log record for any targets configured for the specified level.
func (l *Logger) Log(level Level, msg string, fields ...Field) {
l.log.Log(level, msg, fields...)
}
// LogM emits the log record for any targets configured for the specified levels.
// Equivalent to calling `Log` once for each level.
func (l *Logger) LogM(levels []Level, msg string, fields ...Field) {
l.log.LogM(levels, msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Trace` level.
func (l *Logger) Trace(msg string, fields ...Field) {
l.log.Trace(msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Debug` level.
func (l *Logger) Debug(msg string, fields ...Field) {
l.log.Debug(msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Info` level.
func (l *Logger) Info(msg string, fields ...Field) {
l.log.Info(msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Warn` level.
func (l *Logger) Warn(msg string, fields ...Field) {
l.log.Warn(msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Error` level.
func (l *Logger) Error(msg string, fields ...Field) {
l.log.Error(msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Critical` level.
func (l *Logger) Critical(msg string, fields ...Field) {
l.log.Log(LvlCritical, msg, fields...)
}
// Convenience method equivalent to calling `Log` with the `Fatal` level,
// followed by `os.Exit(1)`.
func (l *Logger) Fatal(msg string, fields ...Field) {
l.log.Log(logr.Fatal, msg, fields...)
_ = l.Shutdown()
os.Exit(1)
}
// HasTargets returns true if at least one log target has been added.
func (l *Logger) HasTargets() bool {
return l.log.Logr().HasTargets()
}
// StdLogger creates a standard logger backed by this logger.
// All log records are output with the specified level.
func (l *Logger) StdLogger(level Level) *log.Logger {
return l.log.StdLogger(level)
}
// StdLogWriter returns a writer that can be hooked up to the output of a golang standard logger
// anything written will be interpreted as log entries and passed to this logger.
func (l *Logger) StdLogWriter() io.Writer {
return &logWriter{
logger: l,
}
}
// RedirectStdLog redirects output from the standard library's package-global logger
// to this logger at the specified level and with zero or more Field's. Since this logger already
// handles caller annotations, timestamps, etc., it automatically disables the standard
// library's annotations and prefixing.
// A function is returned that restores the original prefix and flags and resets the standard
// library's output to os.Stdout.
func (l *Logger) RedirectStdLog(level Level, fields ...Field) func() {
return l.log.Logr().RedirectStdLog(level, fields...)
}
// RemoveTargets safely removes one or more targets based on the filtering method.
// `f` should return true to delete the target, false to keep it.
// When removing a target, best effort is made to write any queued log records before
// closing, with ctx determining how much time can be spent in total.
// Note, keep the timeout short since this method blocks certain logging operations.
func (l *Logger) RemoveTargets(ctx context.Context, f func(ti TargetInfo) bool) error {
return l.log.Logr().RemoveTargets(ctx, f)
}
// SetMetricsCollector sets (or resets) the metrics collector to be used for gathering
// metrics for all targets. Only targets added after this call will use the collector.
//
// To ensure all targets use a collector, use the `SetMetricsCollector` option when
// creating the Logger instead, or configure/reconfigure the Logger after calling this method.
func (l *Logger) SetMetricsCollector(collector MetricsCollector, updateFrequencyMillis int64) {
l.log.Logr().SetMetricsCollector(collector, updateFrequencyMillis)
}
// Sugar creates a new `Logger` with a less structured API. Any fields are preserved.
func (l *Logger) Sugar(fields ...Field) Sugar {
return l.log.Sugar(fields...)
}
// Flush forces all targets to write out any queued log records with a default timeout.
func (l *Logger) Flush() error {
ctx, cancel := context.WithTimeout(context.Background(), FlushTimeout)
defer cancel()
return l.log.Logr().FlushWithTimeout(ctx)
}
// Flush forces all targets to write out any queued log records with the specified timeout.
func (l *Logger) FlushWithTimeout(ctx context.Context) error {
return l.log.Logr().FlushWithTimeout(ctx)
}
// Shutdown shuts down the logger after making best efforts to flush any
// remaining records.
func (l *Logger) Shutdown() error {
ctx, cancel := context.WithTimeout(context.Background(), ShutdownTimeout)
defer cancel()
return l.log.Logr().ShutdownWithTimeout(ctx)
}
// Shutdown shuts down the logger after making best efforts to flush any
// remaining records.
func (l *Logger) ShutdownWithTimeout(ctx context.Context) error {
return l.log.Logr().ShutdownWithTimeout(ctx)
}
// GetPackageName reduces a fully qualified function name to the package name
// By sirupsen: https://github.com/sirupsen/logrus/blob/master/entry.go
func GetPackageName(f string) string {
for {
lastPeriod := strings.LastIndex(f, ".")
lastSlash := strings.LastIndex(f, "/")
if lastPeriod > lastSlash {
f = f[:lastPeriod]
} else {
break
}
}
return f
}
// ShouldQuote returns true if val contains any characters that might be unsafe
// when injecting log output into an aggregator, viewer or report.
// Returning true means that val should be surrounded by quotation marks before being
// output into logs.
func ShouldQuote(val string) bool {
for _, c := range val {
if !((c >= '0' && c <= '9') ||
(c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
c == '-' || c == '.' || c == '_' || c == '/' || c == '@' || c == '^' || c == '+') {
return true
}
}
return false
}
type logWriter struct {
logger *Logger
}
func (lw *logWriter) Write(p []byte) (int, error) {
lw.logger.Info(string(p))
return len(p), nil
}
// ErrConfigurationLock is returned when one of a logger's configuration APIs is called
// while the configuration is locked.
var ErrConfigurationLock = errors.New("configuration is locked")
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mlog
import "github.com/mattermost/logr/v2"
// MaxQueueSize is the maximum number of log records that can be queued.
// If exceeded, `OnQueueFull` is called which determines if the log
// record will be dropped or block until add is successful.
// Defaults to DefaultMaxQueueSize.
func MaxQueueSize(size int) Option {
return logr.MaxQueueSize(size)
}
// OnLoggerError, when not nil, is called any time an internal
// logging error occurs. For example, this can happen when a
// target cannot connect to its data sink.
func OnLoggerError(f func(error)) Option {
return logr.OnLoggerError(f)
}
// OnQueueFull, when not nil, is called on an attempt to add
// a log record to a full Logr queue.
// `MaxQueueSize` can be used to modify the maximum queue size.
// This function should return quickly, with a bool indicating whether
// the log record should be dropped (true) or block until the log record
// is successfully added (false). If nil then blocking (false) is assumed.
func OnQueueFull(f func(rec *LogRec, maxQueueSize int) bool) Option {
return logr.OnQueueFull(f)
}
// OnTargetQueueFull, when not nil, is called on an attempt to add
// a log record to a full target queue provided the target supports reporting
// this condition.
// This function should return quickly, with a bool indicating whether
// the log record should be dropped (true) or block until the log record
// is successfully added (false). If nil then blocking (false) is assumed.
func OnTargetQueueFull(f func(target Target, rec *LogRec, maxQueueSize int) bool) Option {
return logr.OnTargetQueueFull(f)
}
// SetMetricsCollector enables metrics collection by supplying a MetricsCollector.
// The MetricsCollector provides counters and gauges that are updated by log targets.
// `updateFreqMillis` determines how often polled metrics are updated. Defaults to 15000 (15 seconds)
// and must be at least 250 so we don't peg the CPU.
func SetMetricsCollector(collector MetricsCollector, updateFreqMillis int64) Option {
return logr.SetMetricsCollector(collector, updateFreqMillis)
}
// StackFilter provides a list of package names to exclude from the top of
// stack traces. The Logr packages are automatically filtered.
func StackFilter(pkg ...string) Option {
return logr.StackFilter(pkg...)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mlog
import (
"bytes"
"io"
"os"
"sync"
"github.com/mattermost/logr/v2"
"github.com/mattermost/logr/v2/formatters"
"github.com/mattermost/logr/v2/targets"
)
// AddWriterTarget adds a simple io.Writer target to an existing Logger.
// The `io.Writer` can be a buffer which is useful for testing.
// When adding a buffer to collect logs make sure to use `mlog.Buffer` which is
// a thread safe version of `bytes.Buffer`.
func AddWriterTarget(logger *Logger, w io.Writer, useJSON bool, levels ...Level) error {
filter := logr.NewCustomFilter(levels...)
var formatter logr.Formatter
if useJSON {
formatter = &formatters.JSON{EnableCaller: true}
} else {
formatter = &formatters.Plain{EnableCaller: true}
}
target := targets.NewWriterTarget(w)
return logger.log.Logr().AddTarget(target, "_testWriter", filter, formatter, 1000)
}
// CreateConsoleTestLogger creates a logger for unit tests. Log records are output to `os.Stdout`.
// Logs can also be mirrored to the optional `io.Writer`.
func CreateConsoleTestLogger(useJSON bool, level Level) *Logger {
logger, _ := NewLogger()
filter := logr.StdFilter{
Lvl: level,
Stacktrace: LvlPanic,
}
var formatter logr.Formatter
if useJSON {
formatter = &formatters.JSON{EnableCaller: true}
} else {
formatter = &formatters.Plain{EnableCaller: true}
}
target := targets.NewWriterTarget(os.Stdout)
if err := logger.log.Logr().AddTarget(target, "_testcon", filter, formatter, 1000); err != nil {
panic(err)
}
return logger
}
// Buffer provides a thread-safe buffer useful for logging to memory in unit tests.
type Buffer struct {
buf bytes.Buffer
mux sync.Mutex
}
func (b *Buffer) Read(p []byte) (n int, err error) {
b.mux.Lock()
defer b.mux.Unlock()
return b.buf.Read(p)
}
func (b *Buffer) Write(p []byte) (n int, err error) {
b.mux.Lock()
defer b.mux.Unlock()
return b.buf.Write(p)
}
func (b *Buffer) String() string {
b.mux.Lock()
defer b.mux.Unlock()
return b.buf.String()
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package templates
import (
"bytes"
"html/template"
"io"
"os"
"path/filepath"
"sync"
"github.com/fsnotify/fsnotify"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
)
// Container represents a set of templates that can be render
type Container struct {
templates *template.Template
mutex sync.RWMutex
stop chan struct{}
stopped chan struct{}
watch bool
}
// Data contains the data used to populate the template variables, it has Props
// that can be of any type and HTML that only can be `template.HTML` types.
type Data struct {
Props map[string]any
HTML map[string]template.HTML
}
func GetTemplateDirectory() (string, bool) {
templatesDir := "templates"
if mattermostPath := os.Getenv("MM_SERVER_PATH"); mattermostPath != "" {
templatesDir = filepath.Join(mattermostPath, templatesDir)
}
return fileutils.FindDir(templatesDir)
}
// NewFromTemplates creates a new templates container using a
// `template.Template` object
func NewFromTemplate(templates *template.Template) *Container {
return &Container{templates: templates}
}
// New creates a new templates container scanning a directory.
func New(directory string) (*Container, error) {
c := &Container{}
htmlTemplates, err := template.ParseGlob(filepath.Join(directory, "*.html"))
if err != nil {
return nil, err
}
c.templates = htmlTemplates
return c, nil
}
// NewWithWatcher creates a new templates container scanning a directory and
// watch the directory filesystem changes to apply them to the loaded
// templates. This function returns the container and an errors channel to pass
// all errors that can happen during the watch process, or an regular error if
// we fail to create the templates or the watcher. The caller must consume the
// returned errors channel to ensure not blocking the watch process.
func NewWithWatcher(directory string) (*Container, <-chan error, error) {
htmlTemplates, err := template.ParseGlob(filepath.Join(directory, "*.html"))
if err != nil {
return nil, nil, err
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, nil, err
}
err = watcher.Add(directory)
if err != nil {
watcher.Close()
return nil, nil, err
}
c := &Container{
templates: htmlTemplates,
watch: true,
stop: make(chan struct{}),
stopped: make(chan struct{}),
}
errors := make(chan error)
go func() {
defer close(errors)
defer close(c.stopped)
defer watcher.Close()
for {
select {
case <-c.stop:
return
case event := <-watcher.Events:
if event.Op&fsnotify.Write == fsnotify.Write {
if htmlTemplates, err := template.ParseGlob(filepath.Join(directory, "*.html")); err != nil {
errors <- err
} else {
c.mutex.Lock()
c.templates = htmlTemplates
c.mutex.Unlock()
}
}
case err := <-watcher.Errors:
errors <- err
}
}
}()
return c, errors, nil
}
// Close stops the templates watcher of the container in case you have created
// it with watch parameter set to true
func (c *Container) Close() {
c.mutex.RLock()
defer c.mutex.RUnlock()
if c.watch {
close(c.stop)
<-c.stopped
}
}
// RenderToString renders the template referenced with the template name using
// the data provided and return a string with the result
func (c *Container) RenderToString(templateName string, data Data) (string, error) {
var text bytes.Buffer
if err := c.Render(&text, templateName, data); err != nil {
return "", err
}
return text.String(), nil
}
// RenderToString renders the template referenced with the template name using
// the data provided and write it to the writer provided
func (c *Container) Render(w io.Writer, templateName string, data Data) error {
c.mutex.RLock()
htmlTemplates := c.templates
c.mutex.RUnlock()
if err := htmlTemplates.ExecuteTemplate(w, templateName, data); err != nil {
return err
}
return nil
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package web
import (
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
var UnsafeContentTypes = [...]string{
"application/javascript",
"application/ecmascript",
"text/javascript",
"text/ecmascript",
"application/x-javascript",
"text/html",
}
var MediaContentTypes = [...]string{
"image/jpeg",
"image/png",
"image/bmp",
"image/gif",
"image/tiff",
"video/avi",
"video/mpeg",
"video/mp4",
"audio/mpeg",
"audio/wav",
}
func WriteFileResponse(filename string, contentType string, contentSize int64, lastModification time.Time, webserverMode string, fileReader io.ReadSeeker, forceDownload bool, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "private, no-cache")
w.Header().Set("X-Content-Type-Options", "nosniff")
if contentSize > 0 {
contentSizeStr := strconv.Itoa(int(contentSize))
if webserverMode == "gzip" {
w.Header().Set("X-Uncompressed-Content-Length", contentSizeStr)
} else {
w.Header().Set("Content-Length", contentSizeStr)
}
}
if contentType == "" {
contentType = "application/octet-stream"
} else {
for _, unsafeContentType := range UnsafeContentTypes {
if strings.HasPrefix(contentType, unsafeContentType) {
contentType = "text/plain"
break
}
}
}
w.Header().Set("Content-Type", contentType)
var toDownload bool
if forceDownload {
toDownload = true
} else {
isMediaType := false
for _, mediaContentType := range MediaContentTypes {
if strings.HasPrefix(contentType, mediaContentType) {
isMediaType = true
break
}
}
toDownload = !isMediaType
}
filename = url.PathEscape(filename)
if toDownload {
w.Header().Set("Content-Disposition", "attachment;filename=\""+filename+"\"; filename*=UTF-8''"+filename)
} else {
w.Header().Set("Content-Disposition", "inline;filename=\""+filename+"\"; filename*=UTF-8''"+filename)
}
// prevent file links from being embedded in iframes
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Content-Security-Policy", "Frame-ancestors 'none'")
http.ServeContent(w, r, filename, lastModification, fileReader)
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/mattermost/mattermost-server/v6/model"
)
// generateDefaultConfig writes default config to outputFile.
func generateDefaultConfig(outputFile *os.File) error {
defaultCfg := &model.Config{}
defaultCfg.SetDefaults()
if data, err := json.MarshalIndent(defaultCfg, "", " "); err != nil {
return err
} else if _, err := outputFile.Write(data); err != nil {
return err
}
return nil
}
func main() {
outputFile := os.Getenv("OUTPUT_CONFIG")
if outputFile == "" {
fmt.Println("Output file name is missing. Please set OUTPUT_CONFIG env variable to absolute path")
os.Exit(2)
}
if _, err := os.Stat(outputFile); !os.IsNotExist(err) {
_, _ = fmt.Fprintf(os.Stderr, "File %s already exists. Not overwriting!\n", outputFile)
os.Exit(2)
}
if file, err := os.Create(outputFile); err == nil {
err = generateDefaultConfig(file)
_ = file.Close()
if err != nil {
panic(err)
}
} else {
panic(err)
}
}